Repository navigation
CI: only deploy the website from the head of main - #4230
Merged
Merged
Conversation
Re-running the CI workflow of an older commit on `main` deployed that commit's website over the newer one: a re-run keeps the original event's context, so the `deploy` job's condition still passed. Tests finishing out of order, after two quick pushes, had the same effect. The `pages` concurrency group doesn't prevent it either: a job queued in a group cancels the pending one, so an older commit's deployment could cancel a newer commit's pending deployment and leave the website outdated until the next push. A new `check-head` job reads the head of `main` and only lets `deploy` run when it is still the workflow's commit. It runs before `deploy`, so a skipped deployment never queues in the `pages` group, and it skips with a notice instead of failing the run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The repository is public, so `git ls-remote` needs no token, and `check-head` no longer needs any permission. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Unlike the default `bash -e`, `shell: bash` also sets `pipefail`, so a failing command in a pipe fails the step. `check-head` relied on it for `git ls-remote`, and `update-screenshots.yml`'s `git ls-files` pipes no longer hide a failing `git ls-files`. No existing step behaves differently otherwise. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The check moves to the last step of the `test` job, which saves starting a runner on every push to `main`, and passes its result to `deploy` through a job output, like the artifact name. The `pages` concurrency group now uses `queue: max`, so deployments wait in turn instead of each queued one canceling the pending one. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The head check now runs before `Upload website`, which only runs when the commit is still the head of `main`, and it reads the head through the `origin` remote that `actions/checkout` configured. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Re-running the CI workflow of an older commit on
maindeploys that commit's website over the newer one. A re-run keeps the original event's context (push,refs/heads/main, the oldgithub.sha), so thedeployjob's condition still passes, andactions/deploy-pagesdoesn't check whether the commit is still the head ofmain. Tests finishing out of order, after two quick pushes, have the same effect.The
pagesconcurrency group didn't prevent it either. By default, a group keeps at most one pending job, and a newly queued job cancels it, so an older commit's deployment could cancel a newer commit's pending deployment, leaving the website outdated until the next push.Before uploading the website, the
testjob now reads the head ofmainwithgit ls-remote origin, which needs no token since the repository is public. It only uploads the website, and letsdeployrun, when the head is still the workflow's commit. Whenmainhas moved on, it skips the deployment with a notice instead of failing the run. GitHub has no built-in setting for this, and other projects use the same kind of check, e.g., souravmondalshuvo/Shohoj#856.The check runs before
deployrather than in it, because a job that references an environment records a deployment whenever it runs: a check insidedeploywould still list each skipped commit as the latestgithub-pagesdeployment. A job'sif:can't run commands, so the result reachesdeploythrough atestoutput, like the artifact name.The
pagesgroup also usesqueue: maxnow (since May 2026), so up to 100 deployments wait in turn instead of each queued one canceling the pending one.shell: bashAll workflows now default to
shell: bash. Unlike the defaultbash -e, it also setspipefail, so a failing command in a pipe fails the step: the head check relies on it, so a failinggit ls-remotedoesn't look likemainhaving moved on. The only other pipes, inupdate-screenshots.yml, behave the same unlessgit ls-filesfails.Alternatives
A separate job for the check would keep
testfocused on testing, but starts another runner on every push tomain.Comparing with the deployed commit, rather than with the head of
main, would keep deploying an older passing commit when a newer one fails its tests. But it needs the check to run insidedeploy, which records a deployment as described above, and more API calls.The default
github-pagesartifact name would still break re-running all jobs of the head's own run, within the artifact's one-day retention: the new attempt uploads a second artifact with the same name in the same run, whichdeploy-pagesrejects (Allow deploy by artifact_id actions/deploy-pages#338). Neither action can work around it:upload-pages-artifacthas nooverwriteinput, anddeploy-pagesonly takes an artifact name, not theartifact_idthe upload outputs.Limitations
deployjob reuses thetestjob's output from the earlier attempt. That artifact only lasts a day, though (upload-pages-artifact's default retention).mainis fixed, instead of deploying an older commit that passed.queueyet, likecache-mode, but it's documented for job-levelconcurrency.The deployment steps only run on pushes to
main, so this can only be verified after merging: after the next push tomain, re-run this PR's merge commit's workflow with "Re-run all jobs", and check thatdeployis skipped.🤖 Generated with Claude Code