diff --git a/.github/RELEASE_WORKFLOW.md b/.github/RELEASE_WORKFLOW.md index 867ca05..d2db485 100644 --- a/.github/RELEASE_WORKFLOW.md +++ b/.github/RELEASE_WORKFLOW.md @@ -1,8 +1,26 @@ # Release Workflow Documentation -This module uses two separate GitHub Actions workflows for releases: +This module publishes **two parallel version lines to the same Foundry package +listing**: a legacy v13 line (`1.x`, from `main`) and a v14 line (`2.x`, from +`release/v14`). Foundry's registry natively supports multiple versions per +package, each with its own compatibility range โ€” the Foundry client picks +whichever version matches the user's installed core version, so no separate +listing/package id is needed. -## ๐Ÿš€ Production Release (main branch) +There are three GitHub Actions workflows involved: + +| Branch | Workflow | Publishes to Foundry? | Version family | +|---|---|---|---| +| `main` | `auto-release.yml` | Yes | `1.x`, compat `13.x` | +| `release/v14` | `auto-release-v14.yml` | Yes | `2.x`, compat `14.x` | +| `staging` | `beta-release.yml` | No (GitHub pre-release only) | v14 betas | + +Both stable workflows refuse to run if `module.json`'s version/compatibility +don't match their expected family โ€” this guards against the exact incident +that previously broke v13 users (a v14-shaped build publishing under a v13 +version string via `main`). + +## ๐Ÿš€ Production Release โ€” v13 (main branch) **Workflow:** `.github/workflows/auto-release.yml` @@ -10,14 +28,16 @@ This module uses two separate GitHub Actions workflows for releases: - Automatically triggered when `module.json` or `package.json` is pushed to the `main` branch ### What it does: -1. โœ… Verifies version consistency between `module.json` and `package.json` -2. โœ… Checks that `CHANGELOG.md` has been updated for the new version -3. โœ… Creates a GitHub release with tag `vX.Y.Z` -4. โœ… Uploads `module.zip` and `module.json` as release assets -5. โœ… **Publishes to Foundry VTT package repository** (visible to all users) +1. โœ… Verifies `module.json` version is `1.x` and `compatibility.minimum` is `13.x` โ€” refuses to publish otherwise +2. โœ… Verifies version consistency between `module.json` and `package.json` +3. โœ… Checks that `CHANGELOG.md` has been updated for the new version +4. โœ… Creates a GitHub release with tag `vX.Y.Z` +5. โœ… Updates the moving `v13-latest` tag/release so direct-manifest-URL installs auto-update +6. โœ… Uploads `module.zip` and `module.json` as release assets +7. โœ… **Publishes to Foundry VTT package repository** (visible to all users) ### How to create a production release: -1. Update version in both `module.json` and `package.json` +1. Update version in both `module.json` and `package.json` (keep it in the `1.x` family) 2. Add a section for the new version in `CHANGELOG.md`: ```markdown ## [1.2.0] - 2025-01-15 @@ -29,6 +49,47 @@ This module uses two separate GitHub Actions workflows for releases: --- +## ๐Ÿš€ Production Release โ€” v14 (release/v14 branch) + +**Workflow:** `.github/workflows/auto-release-v14.yml` + +### When it runs: +- Automatically triggered when `module.json` or `package.json` is pushed to the `release/v14` branch + +### What it does: +Same steps as the v13 workflow above, but requires `module.json` version to be +`2.x` and `compatibility.minimum` to be `14.x`, and maintains its own moving +`v14-latest` tag/release instead of `v13-latest`. Publishes to the **same** +Foundry package listing as `main` (same `FOUNDRY_ADMIN_MODULE_ID`), as a +separate version entry with its own compatibility range. + +Its GitHub releases are created with `make_latest: false`, so the repo-wide +`/releases/latest` URL always stays on the v13 track. This protects v13 users +who installed before `v13-latest` existed and still have +`/releases/latest/download/module.json` stored as their manifest URL โ€” without +it, each v14 publish would silently repoint those installs at v14. + +### How to create a v14 production release: +1. If `release/v14` doesn't exist yet, create it from a commit that already + contains `.github/workflows/auto-release-v14.yml` โ€” i.e. from `main` after + the dual-publish CI merged, or cherry-pick that workflow file onto the new + branch. + > โš ๏ธ Do **not** branch `release/v14` off `staging` alone. GitHub evaluates + > workflows from the ref being pushed, so a `release/v14` without + > `auto-release-v14.yml` accepts version bumps and silently publishes + > nothing. +2. Merge/promote tested `staging` work into `release/v14` +3. Resolve the version in both `module.json` and `package.json` (keep it in the + `2.x` family, compat `14.x`) โ€” `staging` merges often carry beta or `1.x` + values that the workflow will reject +4. Add a section for the new version in `CHANGELOG.md` +5. Push to `release/v14` โ€” the workflow creates the release and publishes to Foundry + +Before the *first* v14 stable release, complete the one-time +[Pre-v14 cutover](#-pre-v14-cutover-required-once) checklist. + +--- + ## ๐Ÿงช Beta Release (staging branch) **Workflow:** `.github/workflows/beta-release.yml` @@ -77,28 +138,70 @@ In Foundry VTT: ## ๐Ÿ“‹ Branch Strategy ``` -staging (beta releases) - โ†“ - โ†“ (merge when ready) - โ†“ -main (production releases) + staging (v14 beta releases) + โ†“ + โ†“ (merge tested work when ready) + โ†“ + release/v14 (v14 production releases) โ”€โ”€โ” + โ”œโ”€โ†’ same Foundry listing, + main (v13 production releases) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ different version entries ``` +`main` and `staging`/`release/v14` are independent lines โ€” `main` no longer +receives the v14 rewrite. Fixes that apply to both must be ported by hand +(cherry-pick or reimplement), not by merging the branches into each other. + ### Typical workflow: -1. **Development:** Make changes on feature branches, merge to `staging` -2. **Beta Testing:** Push version bump to `staging` โ†’ creates beta release -3. **Testing:** Testers install beta via manifest URL and provide feedback -4. **Release:** When ready, merge `staging` to `main` โ†’ creates production release +1. **v14 development:** Make changes on feature branches, merge to `staging` +2. **v14 Beta Testing:** Push version bump to `staging` โ†’ creates beta release +3. **v14 Release:** When ready, merge `staging` to `release/v14` โ†’ creates v14 production release +4. **v13 maintenance:** Fixes/parity changes for the legacy line go directly to `main` โ†’ creates v13 production release + +--- + +## ๐Ÿ”€ Pre-v14 cutover (required once) + +Installs made before `v13-latest` existed still store +`/releases/latest/download/module.json` as their manifest URL. That URL is +repo-wide, so whichever release GitHub last marked "latest" owns those users. +Run this checklist **once, before the first `release/v14` publish**: + +1. **Reclaim `/releases/latest` for v13.** Push a normal v13 production release + on `main`. Its release is created with `make_latest: true`, which moves the + repo-wide latest pointer back onto the v13 track. +2. **Confirm it took.** Open the repo's Releases page (or + `/releases/latest`) and verify the release badged "Latest" is a `v1.x` + release. +3. **Only then publish v14.** Create/push the first `release/v14` stable + release. Its workflow uses `make_latest: false`, so it never touches the + repo-wide latest pointer. +4. **Optional user migration.** Legacy installs stay safe as long as `main` + keeps reclaiming latest and `release/v14` never sets `make_latest`. Users + who *want* the moving v13 track explicitly can reinstall with + `https://github.com/camrun91/archivist-sync/releases/download/v13-latest/module.json`. --- ## ๐Ÿ”ง Version Numbering -### Production (main): +### Production v13 (main): - Tag: `v1.2.0` - Version in module.json: `1.2.0` -- Manifest URL: `/releases/latest/download/module.json` (auto-updates) -- Download URL: `/releases/download/v1.2.0/module.zip` (specific version) +- Manifest URL: `/releases/download/v13-latest/module.json` (auto-updates) +- Download URL in the published manifest: `/releases/download/v1.2.0/module.zip` (immutable for that release) + +### Production v14 (release/v14): +- Tag: `v2.1.0` +- Version in module.json: `2.1.0` +- Manifest URL: `/releases/download/v14-latest/module.json` (auto-updates) +- Download URL in the published manifest: `/releases/download/v2.1.0/module.zip` (immutable for that release) + +**Manifest vs. download:** only the *manifest* URL moves. Each track's +`*-latest` release also hosts copies of `module.json` and `module.zip` so +Foundry's update check always reads the newest version for that track โ€” but the +`download` field inside every published `module.json` points at that release's +own versioned zip. A given version therefore always installs the exact bits it +was built from, even after a newer release moves the `*-latest` tag. ### Beta (staging): - **Versioned tag:** `v1.2.0-beta.5` (specific beta with full changelog) @@ -132,28 +235,30 @@ main (production releases) - Make changes but don't update the version numbers - Or use `[skip ci]` in your commit message -### Merging staging to main +### Promoting staging to release/v14 - The `staging` branch's `module.json` will have beta URLs: ```json "manifest": "https://github.com/camrun91/archivist-sync/releases/download/beta-latest/module.json", "download": "https://github.com/camrun91/archivist-sync/releases/download/beta-latest/module.zip" ``` -- Before merging to main, update these to production URLs: - ```json - "manifest": "https://github.com/camrun91/archivist-sync/releases/latest/download/module.json", - "download": "https://github.com/camrun91/archivist-sync/releases/download/v1.2.0/module.zip" - ``` -- The main branch should use `releases/latest/download/` for manifest (auto-updates) -- Update the download URL to match the version you're releasing +- You do **not** need to fix these by hand: both stable workflows rewrite these + fields before packaging, precisely so leaked beta URLs can't ship โ€” `manifest` + becomes their own track's moving tag (`v13-latest` or `v14-latest`), and + `download` becomes that release's versioned zip (`/releases/download/v2.1.0/module.zip`). +- Do **not** point any manifest at `releases/latest/download/`, and do **not** + hand-edit which release GitHub marks "latest". That pointer is repo-wide and is + managed by the workflows: `main` claims it (`make_latest: true`), `release/v14` + leaves it alone (`make_latest: false`). See + [Pre-v14 cutover](#-pre-v14-cutover-required-once). --- ## ๐ŸŽฏ Best Practices 1. **Always update CHANGELOG.md** before releasing (production) -2. **Test on staging** before merging to main +2. **Test on staging** before promoting to `release/v14` 3. **Keep versions in sync** between `module.json` and `package.json` 4. **Use semantic versioning**: `MAJOR.MINOR.PATCH` 5. **Beta testing**: Share the beta manifest URL with trusted testers -6. **Production release**: Only merge to main when ready for public release +6. **Production release**: Only push to `main` (v13) or `release/v14` (v14) when ready for public release diff --git a/.github/workflows/auto-release-v14.yml b/.github/workflows/auto-release-v14.yml new file mode 100644 index 0000000..0189e08 --- /dev/null +++ b/.github/workflows/auto-release-v14.yml @@ -0,0 +1,298 @@ +name: Auto Release v14 on Version Bump + +# Stable release channel for the Foundry v14 line, parallel to auto-release.yml +# (which handles the legacy v13 line on `main`). Both publish to the SAME +# Foundry package listing (same FOUNDRY_ADMIN_MODULE_ID) as separate versions +# with independent compatibility ranges โ€” Foundry's client picks whichever +# version matches the user's installed core version. `staging` remains the +# beta/dev channel and does not publish to Foundry; this workflow is what +# actually ships a tested v14 build to the live registry. + +on: + push: + branches: + - release/v14 + paths: + - "module.json" + - "package.json" + +# Serialize v14 stable releases. This job force-moves the shared `v14-latest` +# tag; two overlapping runs would race on delete/recreate and could leave the +# tag pointing at the older release. Queue instead of cancelling โ€” cancelling +# mid-publish could leave a GitHub release without its Foundry registry entry. +# Separate group from the v13 track: the two never touch the same tags, so +# they are free to publish concurrently. +concurrency: + group: stable-release-v14 + cancel-in-progress: false + +jobs: + check-and-release: + name: Check Version and Release + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get current version from module.json + id: get_version + run: | + VERSION=$(jq -r '.version' module.json) + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "TAG=v$VERSION" >> $GITHUB_OUTPUT + echo "๐Ÿ“ฆ Current version in module.json: $VERSION" + + - name: Guard against publishing the wrong version line + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + COMPAT_MIN=$(jq -r '.compatibility.minimum' module.json) + + # Reject empty, prerelease, or non-semver stable versions before + # the track-family check. Bash glob `1.*`/`2.*` would otherwise + # accept strings like `2.0.0-beta.28` as valid for a stable publish. + if [[ -z "$VERSION" ]]; then + echo "โŒ Refusing to publish: module.json version is empty." + exit 1 + fi + if [[ "$VERSION" == *-* ]]; then + echo "โŒ Refusing to publish: prerelease version '$VERSION' cannot be published as stable." + echo " Strip any -beta/-rc/-alpha suffix from module.json before releasing." + exit 1 + fi + if [[ ! "$VERSION" =~ ^[12]\.[0-9]+\.[0-9]+$ ]]; then + echo "โŒ Refusing to publish: version '$VERSION' is not a stable X.Y.Z semver." + exit 1 + fi + + # release/v14 publishes the v14 track (2.x / compat 14.x). Refuse to + # publish a v13-shaped build here โ€” same mistake as main, mirrored. + if [[ "$VERSION" != 2.* ]]; then + echo "โŒ Refusing to publish: module.json version '$VERSION' is not in the 2.x (v14) family expected on release/v14." + echo " If this is intentionally a v13 release, it belongs on main, not release/v14." + exit 1 + fi + if [[ "$COMPAT_MIN" != 14.* ]]; then + echo "โŒ Refusing to publish: compatibility.minimum '$COMPAT_MIN' is not in the 14.x range expected on release/v14." + exit 1 + fi + echo "โœ… Version $VERSION / compatibility.minimum $COMPAT_MIN match the v14 track." + + - name: Check if package.json version matches + env: + MODULE_VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + PACKAGE_VERSION=$(jq -r '.version' package.json) + + if [ "$PACKAGE_VERSION" != "$MODULE_VERSION" ]; then + echo "โŒ Version mismatch!" + echo " module.json: $MODULE_VERSION" + echo " package.json: $PACKAGE_VERSION" + exit 1 + fi + + echo "โœ… Versions match: $MODULE_VERSION" + + - name: Check if CHANGELOG has been updated + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + if ! grep -q "## \[$VERSION\]" CHANGELOG.md; then + echo "โŒ CHANGELOG.md has not been updated for version $VERSION" + echo " Please add a section for version $VERSION in CHANGELOG.md" + exit 1 + fi + + echo "โœ… CHANGELOG.md contains section for version $VERSION" + + - name: Check if release already exists + id: check_release + env: + TAG: ${{ steps.get_version.outputs.TAG }} + run: | + if git rev-parse "$TAG" >/dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "โ„น๏ธ Tag $TAG already exists, skipping release" + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "โœจ Tag $TAG does not exist, will create release" + fi + + - name: Setup Node.js + if: steps.check_release.outputs.exists == 'false' + uses: actions/setup-node@v4 + with: + node-version: "20.x" + cache: "npm" + + - name: Install dependencies + if: steps.check_release.outputs.exists == 'false' + run: npm ci + + - name: Ensure production URLs in module.json + if: steps.check_release.outputs.exists == 'false' + env: + REPO: ${{ github.repository }} + TAG: ${{ steps.get_version.outputs.TAG }} + run: | + # Point manifest at the moving "v14-latest" tag, not GitHub's built-in + # "releases/latest" โ€” main's auto-release.yml also publishes + # non-prerelease GitHub releases to this same repo, so + # "releases/latest" would flip between the v13 and v14 tracks. + # "v14-latest" is a tag this workflow owns and moves itself + # (mirroring beta-release.yml's beta-latest and auto-release.yml's + # new v13-latest). Download stays on the immutable version tag so + # each release's zip URL never changes when v14-latest moves. + jq --arg repo "$REPO" --arg tag "$TAG" \ + '.manifest = "https://github.com/\($repo)/releases/download/v14-latest/module.json" | + .download = "https://github.com/\($repo)/releases/download/\($tag)/module.zip"' \ + module.json > module.json.tmp && mv module.json.tmp module.json + + echo "โœ… Ensured production URLs in module.json" + echo " Manifest: $(jq -r '.manifest' module.json)" + echo " Download: $(jq -r '.download' module.json)" + + - name: Create module archive + if: steps.check_release.outputs.exists == 'false' + run: | + # Create a clean zip of the module + zip -r module.zip \ + module.json \ + README.md \ + CHANGELOG.md \ + CONTRIBUTING.md \ + assets/ \ + lang/ \ + scripts/ \ + styles/ \ + templates/ \ + -x "*.git*" -x "*node_modules*" -x "*.DS_Store" + + echo "๐Ÿ“ฆ Archive created: module.zip" + ls -lh module.zip + + - name: Extract changelog for this version + if: steps.check_release.outputs.exists == 'false' + id: changelog + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + # Extract changelog section for this version + if [ -f CHANGELOG.md ]; then + awk "/## \[$VERSION\]/,/## \[/" CHANGELOG.md | sed '$d' > release_notes.md + fi + + # If release notes are empty, use a default message + if [ ! -s release_notes.md ]; then + echo "Release $VERSION" > release_notes.md + echo "" >> release_notes.md + echo "See [CHANGELOG.md](CHANGELOG.md) for details." >> release_notes.md + fi + + echo "๐Ÿ“ Release notes:" + cat release_notes.md + + - name: Create GitHub Release + if: steps.check_release.outputs.exists == 'false' + id: create_release + uses: softprops/action-gh-release@v2 + with: + tag_name: ${{ steps.get_version.outputs.TAG }} + target_commitish: ${{ github.sha }} + name: Release ${{ steps.get_version.outputs.VERSION }} (v14) + body_path: release_notes.md + draft: false + prerelease: false + # Keep GitHub's repo-wide /releases/latest pinned to the v13 track. + # v13 users who installed before v13-latest existed still have + # /releases/latest/download/module.json stored as their manifest URL; + # if a v14 release ever became the repo's "latest", Foundry would + # auto-update those users onto v14 โ€” the exact incident the version + # guards exist to prevent. + make_latest: false + files: | + module.zip + module.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Set release environment variables + if: steps.check_release.outputs.exists == 'false' + env: + TAG: ${{ steps.get_version.outputs.TAG }} + run: | + echo "RELEASE_NAME=$TAG" >> $GITHUB_ENV + echo "MANIFEST_FILE_PATH=module.json" >> $GITHUB_ENV + + - name: Configure Git + if: steps.check_release.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update v14-latest tag + if: steps.check_release.outputs.exists == 'false' + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + git tag -d v14-latest 2>/dev/null || true + git push origin :refs/tags/v14-latest 2>/dev/null || true + git tag -a v14-latest -m "Latest v14 stable release: $VERSION" + git push origin v14-latest + + - name: Create/Update v14-latest Release + if: steps.check_release.outputs.exists == 'false' + uses: softprops/action-gh-release@v2 + with: + tag_name: v14-latest + target_commitish: ${{ github.sha }} + name: Latest v14 Stable Release + body: | + This is an automatically updated release that always points to the + most recent v14 stable version. + + **Current Version:** ${{ steps.get_version.outputs.VERSION }} + + Manifest URL for direct install: + ``` + https://github.com/${{ github.repository }}/releases/download/v14-latest/module.json + ``` + + For the full release notes, see [${{ steps.get_version.outputs.TAG }}](https://github.com/${{ github.repository }}/releases/tag/${{ steps.get_version.outputs.TAG }}). + draft: false + prerelease: false + # Same reason as the versioned release above: repo-wide + # /releases/latest must stay on the v13 track for legacy installs. + make_latest: false + files: | + module.zip + module.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish Module to FoundryVTT Website + if: steps.check_release.outputs.exists == 'false' + uses: Varriount/fvtt-autopublish@v2.0.2 + with: + username: ${{ secrets.FOUNDRY_ADMIN_USERNAME }} + password: ${{ secrets.FOUNDRY_ADMIN_PASSWORD }} + module-id: ${{ secrets.FOUNDRY_ADMIN_MODULE_ID }} + manifest-url: https://github.com/${{ github.repository }}/releases/download/${{ env.RELEASE_NAME }}/module.json + manifest-file: ${{ env.MANIFEST_FILE_PATH }} + + - name: Notify on Success + if: steps.check_release.outputs.exists == 'false' + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + TAG: ${{ steps.get_version.outputs.TAG }} + REPO: ${{ github.repository }} + run: | + echo "โœ… Release $VERSION (v14) published successfully!" + echo "๐Ÿ“ฆ Module archive: https://github.com/$REPO/releases/download/$TAG/module.zip" + echo "๐Ÿ“„ Manifest: https://github.com/$REPO/releases/download/$TAG/module.json" + echo "๐ŸŒ Published to Foundry VTT package repository (v14 track)" diff --git a/.github/workflows/auto-release.yml b/.github/workflows/auto-release.yml index 2fc088d..05e3f01 100644 --- a/.github/workflows/auto-release.yml +++ b/.github/workflows/auto-release.yml @@ -8,6 +8,14 @@ on: - "module.json" - "package.json" +# Serialize v13 stable releases. This job force-moves the shared `v13-latest` +# tag; two overlapping runs would race on delete/recreate and could leave the +# tag pointing at the older release. Queue instead of cancelling โ€” cancelling +# mid-publish could leave a GitHub release without its Foundry registry entry. +concurrency: + group: stable-release-v13 + cancel-in-progress: false + jobs: check-and-release: name: Check Version and Release @@ -29,10 +37,49 @@ jobs: echo "TAG=v$VERSION" >> $GITHUB_OUTPUT echo "๐Ÿ“ฆ Current version in module.json: $VERSION" + - name: Guard against publishing the wrong version line + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + COMPAT_MIN=$(jq -r '.compatibility.minimum' module.json) + + # Reject empty, prerelease, or non-semver stable versions before + # the track-family check. Bash glob `1.*`/`2.*` would otherwise + # accept strings like `2.0.0-beta.28` as valid for a stable publish. + if [[ -z "$VERSION" ]]; then + echo "โŒ Refusing to publish: module.json version is empty." + exit 1 + fi + if [[ "$VERSION" == *-* ]]; then + echo "โŒ Refusing to publish: prerelease version '$VERSION' cannot be published as stable." + echo " Strip any -beta/-rc/-alpha suffix from module.json before releasing." + exit 1 + fi + if [[ ! "$VERSION" =~ ^[12]\.[0-9]+\.[0-9]+$ ]]; then + echo "โŒ Refusing to publish: version '$VERSION' is not a stable X.Y.Z semver." + exit 1 + fi + + # main publishes the legacy Foundry v13 track (1.x / compat 13.x). + # A v14-shaped commit landing here and auto-publishing under a v13 + # version string is exactly the incident that broke v13 users + # previously โ€” refuse instead of repeating it. + if [[ "$VERSION" != 1.* ]]; then + echo "โŒ Refusing to publish: module.json version '$VERSION' is not in the 1.x (v13) family expected on main." + echo " If this is intentionally a v14 release, it belongs on the release/v14 branch, not main." + exit 1 + fi + if [[ "$COMPAT_MIN" != 13.* ]]; then + echo "โŒ Refusing to publish: compatibility.minimum '$COMPAT_MIN' is not in the 13.x range expected on main." + exit 1 + fi + echo "โœ… Version $VERSION / compatibility.minimum $COMPAT_MIN match the v13 track." + - name: Check if package.json version matches + env: + MODULE_VERSION: ${{ steps.get_version.outputs.VERSION }} run: | PACKAGE_VERSION=$(jq -r '.version' package.json) - MODULE_VERSION="${{ steps.get_version.outputs.VERSION }}" if [ "$PACKAGE_VERSION" != "$MODULE_VERSION" ]; then echo "โŒ Version mismatch!" @@ -44,9 +91,9 @@ jobs: echo "โœ… Versions match: $MODULE_VERSION" - name: Check if CHANGELOG has been updated + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - if ! grep -q "## \[$VERSION\]" CHANGELOG.md; then echo "โŒ CHANGELOG.md has not been updated for version $VERSION" echo " Please add a section for version $VERSION in CHANGELOG.md" @@ -57,9 +104,9 @@ jobs: - name: Check if release already exists id: check_release + env: + TAG: ${{ steps.get_version.outputs.TAG }} run: | - TAG="${{ steps.get_version.outputs.TAG }}" - if git rev-parse "$TAG" >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "โ„น๏ธ Tag $TAG already exists, skipping release" @@ -85,13 +132,23 @@ jobs: - name: Ensure production URLs in module.json if: steps.check_release.outputs.exists == 'false' + env: + REPO: ${{ github.repository }} + TAG: ${{ steps.get_version.outputs.TAG }} run: | - REPO="${{ github.repository }}" - - # Reset manifest and download URLs to production (latest) in case beta URLs leaked from staging merge - jq --arg repo "$REPO" \ - '.manifest = "https://github.com/\($repo)/releases/latest/download/module.json" | - .download = "https://github.com/\($repo)/releases/latest/download/module.zip"' \ + # Point manifest at the moving "v13-latest" tag, not GitHub's built-in + # "releases/latest" โ€” now that a second stable track (release/v14) + # also publishes non-prerelease GitHub releases to this same repo, + # "releases/latest" would flip between the v13 and v14 tracks + # depending on which published most recently, silently pointing a + # v13 user's stored manifest URL at v14's module.json (or vice + # versa). "v13-latest" is a tag this workflow owns and moves itself + # (mirroring how beta-release.yml already does this for beta-latest). + # Download stays on the immutable version tag so each release's zip URL + # never changes when v13-latest moves. + jq --arg repo "$REPO" --arg tag "$TAG" \ + '.manifest = "https://github.com/\($repo)/releases/download/v13-latest/module.json" | + .download = "https://github.com/\($repo)/releases/download/\($tag)/module.zip"' \ module.json > module.json.tmp && mv module.json.tmp module.json echo "โœ… Ensured production URLs in module.json" @@ -120,9 +177,9 @@ jobs: - name: Extract changelog for this version if: steps.check_release.outputs.exists == 'false' id: changelog + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} run: | - VERSION="${{ steps.get_version.outputs.VERSION }}" - # Extract changelog section for this version if [ -f CHANGELOG.md ]; then awk "/## \[$VERSION\]/,/## \[/" CHANGELOG.md | sed '$d' > release_notes.md @@ -141,13 +198,18 @@ jobs: - name: Create GitHub Release if: steps.check_release.outputs.exists == 'false' id: create_release - uses: softprops/action-gh-release@v1 + uses: softprops/action-gh-release@v2 with: tag_name: ${{ steps.get_version.outputs.TAG }} + target_commitish: ${{ github.sha }} name: Release ${{ steps.get_version.outputs.VERSION }} body_path: release_notes.md draft: false prerelease: false + # Reclaim repo-wide /releases/latest for the v13 track. Legacy installs + # still use /releases/latest/download/module.json; v14 releases set + # make_latest: false, but each v13 publish must actively pin latest here. + make_latest: true files: | module.zip module.json @@ -156,10 +218,58 @@ jobs: - name: Set release environment variables if: steps.check_release.outputs.exists == 'false' + env: + TAG: ${{ steps.get_version.outputs.TAG }} run: | - echo "RELEASE_NAME=${{ steps.get_version.outputs.TAG }}" >> $GITHUB_ENV + echo "RELEASE_NAME=$TAG" >> $GITHUB_ENV echo "MANIFEST_FILE_PATH=module.json" >> $GITHUB_ENV + - name: Configure Git + if: steps.check_release.outputs.exists == 'false' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Update v13-latest tag + if: steps.check_release.outputs.exists == 'false' + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + run: | + git tag -d v13-latest 2>/dev/null || true + git push origin :refs/tags/v13-latest 2>/dev/null || true + git tag -a v13-latest -m "Latest v13 stable release: $VERSION" + git push origin v13-latest + + - name: Create/Update v13-latest Release + if: steps.check_release.outputs.exists == 'false' + uses: softprops/action-gh-release@v2 + with: + tag_name: v13-latest + target_commitish: ${{ github.sha }} + name: Latest v13 Stable Release + body: | + This is an automatically updated release that always points to the + most recent v13 (legacy) stable version. + + **Current Version:** ${{ steps.get_version.outputs.VERSION }} + + Manifest URL for direct install: + ``` + https://github.com/${{ github.repository }}/releases/download/v13-latest/module.json + ``` + + For the full release notes, see [${{ steps.get_version.outputs.TAG }}](https://github.com/${{ github.repository }}/releases/tag/${{ steps.get_version.outputs.TAG }}). + draft: false + prerelease: false + # The versioned release above already claimed /releases/latest; this + # moving-tag release is an alias only โ€” do not steal the Latest badge. + make_latest: false + files: | + module.zip + module.json + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + - name: Publish Module to FoundryVTT Website if: steps.check_release.outputs.exists == 'false' uses: Varriount/fvtt-autopublish@v2.0.2 @@ -172,8 +282,12 @@ jobs: - name: Notify on Success if: steps.check_release.outputs.exists == 'false' + env: + VERSION: ${{ steps.get_version.outputs.VERSION }} + TAG: ${{ steps.get_version.outputs.TAG }} + REPO: ${{ github.repository }} run: | - echo "โœ… Release ${{ steps.get_version.outputs.VERSION }} published successfully!" - echo "๐Ÿ“ฆ Module archive: https://github.com/${{ github.repository }}/releases/download/${{ steps.get_version.outputs.TAG }}/module.zip" - echo "๐Ÿ“„ Manifest: https://github.com/${{ github.repository }}/releases/download/${{ steps.get_version.outputs.TAG }}/module.json" + echo "โœ… Release $VERSION published successfully!" + echo "๐Ÿ“ฆ Module archive: https://github.com/$REPO/releases/download/$TAG/module.zip" + echo "๐Ÿ“„ Manifest: https://github.com/$REPO/releases/download/$TAG/module.json" echo "๐ŸŒ Published to Foundry VTT package repository"