Hold staging for the whole deploy-and-verify sequence, not just the deploy - #381
Conversation
This comment has been minimized.
This comment has been minimized.
…eploy `deploy-staging` was the only job in the `staging-deploy` concurrency group. `staging-smoke-tests` and `e2e-staging` carried no concurrency at all, so the deploy was serialised while everything that verifies the deploy was not: a following run's deploy was free to land between one run's smoke tests and its E2E, restarting the box underneath a suite that was testing it. Three consecutive merges failed that way on 2026-09-10 (#348, #351, #352), and four runs within three minutes produced the same thing on 2026-08-21 (#223). None of them failed for a fault in the change. The failure does not present as a flake either: a test hitting a half-swapped application gets a specific, plausible assertion failure, which reads as a content regression and costs someone real time before they conclude otherwise. It also blocks releases, as a red staging E2E skips `deploy-production`. Adding the group to the two verification jobs does not close it. Concurrency is acquired and released per job, so a deploy can still slot in between one run's smoke tests and its E2E. The environment has to stay held across the sequence. So the three jobs move to `staging-deploy-verify.yml` and ci.yml calls it from a single `Staging` job that carries the group. A calling job is not complete until every job in the called workflow has finished, so the group is held from the first byte of the deploy to the last E2E assertion. The step bodies move unchanged; only the job headers differ. Two things the move forced: - `env` does not cross a `workflow_call` boundary and `with:` cannot read the `env` context, so APP_DIR is re-exported as a `changes` output rather than restated in the new file, keeping one definition of the deploy directory. - `playwright-image` joins the staging job's prerequisites. An unpublished image now fails the run before the droplet is touched rather than after a deploy and a smoke suite have landed on it. Two comments that named the old job IDs move with them: the DNS-tolerance notes in frontend/e2e/dashboard.spec.ts and deploy/staging-smoke-test.sh both explained themselves by their job being a direct `needs:` of deploy-production, which is now indirect through the calling job. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
827841e to
3b9f691
Compare
|
Claude finished @Babissimo's task in 8m 36s —— View job Review of PR #381
Note: What's new since the last reviewThe two stale comments I flagged previously are fixed in this update:
Both accurately reflect the new structure. Mechanics re-verified
One residual risk (not a bug, already acknowledged by the author)The entire fix hinges on GitHub holding the Nothing else stood out. The concurrency-holding mechanism, the secret/input contract, and the moved step bodies all check out. |
Closes ClickUp 123zgec26td.
The race
deploy-stagingwas the only job in thestaging-deployconcurrency group.staging-smoke-testsande2e-stagingcarried no concurrency at all, so thedeploy was serialised while everything that verifies the deploy was not. A
following run's deploy was free to land between one run's smoke tests and its
E2E, restarting the box underneath a suite that was testing it.
Three consecutive merges failed that way on 2026-09-10 (#348, #351, #352), and
four runs inside three minutes produced the same on 2026-08-21 (#223). None
failed for a fault in the change.
Adding
group: staging-deployto the two verification jobs does not close it:concurrency is acquired and released per job, so a deploy can still slot in
between one run's smoke tests and its E2E. The environment has to stay held
across the whole sequence.
The change
The three jobs move to
.github/workflows/staging-deploy-verify.yml, andci.ymlcalls it from oneStagingjob that carries the group. A calling jobis not complete until every job in the called workflow has finished, so the
group is held from the first byte of the deploy to the last E2E assertion.
jobs.<job_id>.concurrencyissupported on a job that calls a reusable workflow,
so this is documented behaviour rather than an inference.
The step bodies move unchanged — verified byte-identical against
origin/main(124 / 35 / 27 lines). Only the job headers differ.
Two things the move forced:
envdoes not cross aworkflow_callboundary, andwith:on a calling jobcannot read the
envcontext either.APP_DIRis therefore re-exported as achangesoutput rather than restated in the new file, so the deploydirectory keeps one definition. The re-export refuses an empty value:
cd ""succeeds into
$HOME, where every one of the deploy's pre-flights passesbefore
git fetchfails in a directory nothing is served from.playwright-imagejoins the staging job's prerequisites, so an unpublishedimage fails the run before the droplet is touched rather than after a deploy
and a smoke suite have already landed on it. The cost is that a registry blip
now blocks the staging deploy instead of only the E2E.
What this does not fix, deliberately
Stagingjob during a burst is expected. At most one run sitspending per concurrency group, so a third merge arriving while one run holds
staging and another is queued cancels the queued one.
mainis linear, sothe run that replaces it deploys a superset — but the cancelled commit's own
run never reaches production, and the last merge in a burst has no successor
to carry it. Documented in ONBOARDING rather than changed;
queue: maxwouldserialise every merge's full sequence instead, which is a worse trade.
minutes, against 30 before. A hung E2E used to block nothing and now blocks
staging. That is the cost of holding the environment, and it is the point.
Verification
actionlint1.7.12 clean on both files, including its shellcheck pass. Iconfirmed the lint is not a no-op by breaking the input name and watching it
report the
workflow-callcontract error, then restoring.needs.*.outputs.*references.backend/.venv/bin/pre-commit run --all-filespasses.mainlists no required status checks, so renaming thecheck contexts breaks nothing.
The one thing no pre-merge check can prove is the runtime semantic itself: the
staging chain is gated on push to
main, so this PR's own run does not executethe called workflow. The first burst of merges after this lands is what
confirms it, and the ticket records what to look for.
🤖 Generated with Claude Code