From 3c859ed91d3b7c51e13025f1de8153145c23be67 Mon Sep 17 00:00:00 2001 From: Marco Russo Date: Wed, 2 Sep 2026 17:42:14 +0200 Subject: [PATCH] Deploy the site from main, never from a release tag Publish site has failed on almost every release, and the retry added in #84 did not fix it: dev 3286 deployed twice from its tag and the site took neither. That run also identified the cause. A Pages deployment made from a tag is not served unless it is the first deployment of that commit; one made from a branch supersedes whatever is live, first or not. Commit f169379 settles it - deployed from main at 13:59 it went live, deployed from tag v1.2.2-dev.3286 at 14:20 it stalled, twice. By the time a release is published its commit has usually been deployed from main already, so the release's own deployment is created, reported as succeeded, and dropped. A release therefore no longer deploys. It asks this workflow to run on main and that run deploys, carrying the tag as an input so the manifests still wait for the release to appear. workflow_dispatch is one of the two events exempt from the rule that GITHUB_TOKEN cannot start another run, so this needs no personal access token. The retry is removed rather than left beside the real fix: it is known not to work for the only failure it ever targeted, and keeping it would make it unclear which of the two was doing the work. Also moves off Node 20, which the runners now force to Node 24 anyway: configure-pages v5 to v6, deploy-pages v4 to v5, upload-pages-artifact v3 to v5 - the last of which is what drops the transitive upload-artifact v4. site/ has no dotfiles, so the change in upload-pages-artifact v4 that excludes them from the artifact drops nothing. Co-Authored-By: Claude Opus 5 --- .github/workflows/publish-site.yml | 110 ++++++++++++++++++----------- docs/release-management.md | 38 ++++++---- scripts/verify-published-site.ps1 | 28 ++++---- 3 files changed, 106 insertions(+), 70 deletions(-) diff --git a/.github/workflows/publish-site.yml b/.github/workflows/publish-site.yml index 5b6fc4f..38879a2 100644 --- a/.github/workflows/publish-site.yml +++ b/.github/workflows/publish-site.yml @@ -31,10 +31,16 @@ on: - 'scripts/release-notes.ps1' - '.github/workflows/publish-site.yml' # A release changes what the manifests say without changing anything in site/, so it has - # to redeploy the page as well. This is what keeps the download links current. + # to redeploy the page as well. This is what keeps the download links current. It does + # not deploy from here, though - see the redeploy job. release: types: [published] workflow_dispatch: + inputs: + tag: + description: 'Release tag this deployment is for. Leave empty unless deploying for a release.' + required: false + type: string permissions: contents: read @@ -48,8 +54,35 @@ concurrency: cancel-in-progress: false jobs: + # A Pages deployment made from a tag does not become the one the site serves unless it is + # the first deployment of that commit - and by the time a release is published, that + # commit has almost always been deployed from main already. The deployment is created, + # reported as succeeded, and never served. Every release ran into this; deploying a second + # time from the same tag, which is what this workflow used to do, did not help either. + # + # A deployment from a branch supersedes whatever is live, first or not. So a release does + # not deploy: it asks this same workflow to run on main, and that run deploys. The tag + # goes with it, because the manifests still have to wait for the release to appear. + # + # workflow_dispatch is one of the two events GitHub deliberately exempts from the rule + # that GITHUB_TOKEN cannot start another workflow run, so this needs no personal token. + redeploy: + name: Redeploy the site from main + if: github.event_name == 'release' + runs-on: ubuntu-latest + permissions: + actions: write + steps: + - name: Ask for a deployment from main + shell: bash + run: gh workflow run publish-site.yml --repo "$GITHUB_REPOSITORY" --ref main --field "tag=$RELEASE_TAG" + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + deploy: name: Deploy to GitHub Pages + if: github.event_name != 'release' runs-on: ubuntu-latest environment: name: github-pages @@ -59,28 +92,42 @@ jobs: # Generated into the checkout, so the manifests ship with the page in one # deployment. Both files are gitignored: this is the only thing that writes them. - # ExpectTag is empty for every trigger except a release, and that is the point: a + # ExpectTag is empty except when deploying for a release, and that is the point: the # release event fires seconds before the releases API lists the release, so without # it this step reads the previous version and writes a stale manifest while - # reporting success. That happened to 0.9.5. + # reporting success. That happened to 0.9.5. The tag arrives as an input because the + # release itself does not deploy - the redeploy job passes it here. - name: Generate the release manifests shell: pwsh run: ./scripts/build-release-manifests.ps1 -Repository "$env:GITHUB_REPOSITORY" -OutputFolder site -Token "$env:GH_TOKEN" -ExpectTag "$env:EXPECT_TAG" env: GH_TOKEN: ${{ github.token }} - EXPECT_TAG: ${{ github.event.release.tag_name }} + EXPECT_TAG: ${{ inputs.tag }} # A released version with no entry in CHANGELOG.md would publish a What's new page # that does not mention the version people are being offered. Checked here as well as # on the pull request that bumps VersionPrefix, because a release does not have to - # have come from one. Pre-releases are exempt: Dev builds come from every merge and - # are deliberately not in the changelog. - - name: Check this release has notes - if: github.event_name == 'release' && github.event.release.prerelease == false + # have come from one. + # + # The shape of the tag is what says whether it is a pre-release: the pipeline gives + # released versions v.. and Dev builds a -dev. suffix. + # Dev builds come from every merge and are deliberately not in the changelog. + - name: Check the released version has notes shell: pwsh - run: ./scripts/release-notes.ps1 -Mode Verify -Version "$env:RELEASE_TAG" + run: | + if (-not $env:RELEASE_TAG) { + Write-Host 'Not deploying for a release, so there is nothing to check.' + return + } + + if ($env:RELEASE_TAG -notmatch '^v\d+\.\d+\.\d+$') { + Write-Host "$env:RELEASE_TAG is a pre-release; the changelog covers released versions only." + return + } + + ./scripts/release-notes.ps1 -Mode Verify -Version $env:RELEASE_TAG env: - RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_TAG: ${{ inputs.tag }} # Written into the page rather than fetched from the API by the page itself: the # notes are then in the HTML that ships, so they are readable without scripting and @@ -89,47 +136,26 @@ jobs: shell: pwsh run: ./scripts/release-notes.ps1 -Mode Html - - uses: actions/configure-pages@v5 + - uses: actions/configure-pages@v6 - - uses: actions/upload-pages-artifact@v3 + # v4 of this action stopped putting dotfiles in the artifact. site/ has none, so + # nothing is silently dropped; if one is ever added, it needs include-hidden-files. + - uses: actions/upload-pages-artifact@v5 with: path: site - id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 # A Pages deployment can report success, be recorded as the active deployment, and # still leave the site serving the previous one. Every step above went green for # 1.0.0 while the download page offered 0.9.5, and nothing said so. Reading the site - # back is the only place that failure is visible. - # - # It is neither rare nor random: it is what a release does to itself. A Pages - # deployment is identified by commit SHA - deploy-pages sends the SHA as - # pages_build_version and then polls a status keyed on that same SHA - and a release - # redeploys a commit Pages already holds, because the release tag, the pre-release - # tag and main are all one commit. The second deployment of that commit is reported - # as succeeded, since the status read back belongs to the first one, and is then - # never served. That is why release-triggered runs failed while pushes and manual - # runs passed, and why re-running by hand always fixed it. + # back is the only place that failure is visible, so it fails the run that caused it + # rather than waiting to be noticed. # - # So the first look is deliberately short - a deployment that is going to appear is - # serving within about ten seconds, and a long wait here only delays the thing that - # actually fixes it - and the deploy that a person used to do by hand is done here. - - id: firstlook - name: Verify the site is serving this deployment - continue-on-error: true - shell: pwsh - run: ./scripts/verify-published-site.ps1 -Folder site -TimeoutSeconds 90 - - - id: redeployment - name: Deploy again - if: steps.firstlook.outcome == 'failure' - uses: actions/deploy-pages@v4 - - # The full wait this time. If the same files are still not served after a second - # deployment then something other than the collision is wrong, and the run says so - # rather than passing quietly. - - name: Verify the second deployment - if: steps.firstlook.outcome == 'failure' + # Deploying a second time from the same run was tried as a way to recover from that + # and does not work: dev 3286 deployed twice from its tag and the site took neither. + # Only the ref matters, which is what the redeploy job is for. + - name: Verify the site is serving this deployment shell: pwsh run: ./scripts/verify-published-site.ps1 -Folder site diff --git a/docs/release-management.md b/docs/release-management.md index a9d10d2..54b4526 100644 --- a/docs/release-management.md +++ b/docs/release-management.md @@ -190,21 +190,29 @@ from the live domain after deploying and fails if they are not the ones it just release, because a pre-release deployment leaves `stable.json` untouched and that is correct. -**Why that kept happening on releases, and what the workflow now does about it.** It was -not bad luck. A Pages deployment is identified by commit SHA: `deploy-pages` sends the SHA -as `pages_build_version` and then polls a deployment status keyed on that same SHA. A -release redeploys a commit Pages already holds — the release tag, the pre-release tag and -`main` are all one commit — so the second deployment of that commit reads the *first* one's -status, reports success within seconds, and is then never served. The numbers said so -plainly: pushes passed 9 out of 9 and manual runs 8 out of 8, while release-triggered runs -passed 7 out of 23, and the stalled deployment sat at `in_progress` for five minutes where -a healthy one reaches `success` in about ten seconds. - -Deploying the same files again clears it, so the workflow does that itself rather than -waiting to be asked: a short first check, a second `deploy-pages` if the site is still -stale, then the full check. A run that fails now has failed twice, and the collision is not -the explanation — look under **Settings → Environments → `github-pages`** for a deployment -stuck in progress on that commit. +**Why that kept happening on releases: the ref, not the repetition.** A Pages deployment +made from a **tag** is not served unless it is the first deployment of that commit. A +deployment from a **branch** supersedes whatever is live, first or not. By the time a +release is published, its commit has usually been deployed from `main` already — the tag, +the pre-release tag and `main` are all one commit — so the release's own deployment is +created, reported as succeeded by `deploy-pages`, and never served. + +The evidence, once the verify step existed to make it visible: pushes passed 9 of 9 and +manual runs 8 of 8, while release-triggered runs passed 7 of 23. Commit `f169379` settles +it — deployed from `main` at 13:59 it went live; deployed from tag `v1.2.2-dev.3286` at +14:20 it stalled, and stalled again on a second attempt in the same run. Deploying twice +was tried as a fix and does not work, because both attempts are on the same tag. + +So **a release does not deploy the site.** The `redeploy` job asks this same workflow to +run on `main`, passing the release tag as an input so the manifests still wait for the +release to appear. `workflow_dispatch` is one of the two events GitHub exempts from the +rule that `GITHUB_TOKEN` cannot start another workflow run, so this needs no personal +access token. A release therefore produces two runs: a three-line dispatcher on the tag, +and the deployment on `main`. + +A failure in the verify step should now mean something genuinely new. Re-run **Publish +site** on `main` with the tag left empty; if that fails too, look under **Settings → +Environments → `github-pages`** for a deployment stuck in progress on that commit. ### Azure Pipelines diff --git a/scripts/verify-published-site.ps1 b/scripts/verify-published-site.ps1 index 1b072d9..3afdd8d 100644 --- a/scripts/verify-published-site.ps1 +++ b/scripts/verify-published-site.ps1 @@ -16,15 +16,15 @@ deployment leaves stable.json untouched, which is correct, and a check written against the newest release would fail on every one of them. - The usual cause is a release redeploying a commit Pages already holds: a Pages - deployment is identified by commit SHA, and the release tag, the pre-release tag and - main are all one commit, so the second deployment of it reports the first one's status - and is never served. The workflow answers that by deploying again, which is why this - runs twice there - once on a short timeout before the retry, once on the full one - after. + The cause was found in September 2026: a Pages deployment made from a tag is not served + unless it is the first deployment of that commit, and by the time a release is + published the commit has usually been deployed from main already. Deploying a second + time from the same tag does not help - dev 3286 did exactly that and the site took + neither. publish-site.yml therefore deploys from main and never from a release tag, so + a failure here should now mean something genuinely new. - Read-only. It cannot repair a deployment - the remedy is to deploy the same files - again - so its whole job is to make a silent staleness loud. + Read-only. It cannot repair a deployment - the remedy is to deploy again from main - so + its whole job is to make a silent staleness loud. .PARAMETER Folder Folder holding the manifests that were deployed, and the CNAME naming where they were @@ -120,9 +120,11 @@ throw @" $BaseUrl is still serving an older deployment after $TimeoutSeconds seconds ($stale). The deployment this run made succeeded, so the files are right and the site has not -picked them up. Deploying the same files again clears it, and the workflow already does -that once by itself - so if this is the second attempt, the collision it works around is -not the whole story. Run it again by hand - Actions -> Publish site -> Run workflow - and -look for a deployment still in progress on this commit under the github-pages -environment. +picked them up. + +Deployments from a release tag behave this way and are the reason this check exists, but +publish-site.yml no longer makes any - it deploys from main - so this is something else. +Run it again by hand: Actions -> Publish site -> Run workflow, on main, leaving the tag +empty. If that also fails, look under Settings -> Environments -> github-pages for a +deployment still in progress on this commit. "@