Hold the production lock across production's verification and rollbacks - #648
Conversation
This comment has been minimized.
This comment has been minimized.
|
I checked both of the review's main findings, and neither holds. " actionlint encodes the same table, and I checked it against a scratch
The same actionlint passes this PR's workflows in the lint job. The comment in "A Playwright image failure now skips the whole production deploy". It already did. A failed image build therefore skips The minor notes. |
rollback.sh can rebuild the image it restores. The deploy-failure rollbacks on every droplet allow 15 minutes for that, but the rollbacks after production's smoke tests and E2E set no command_timeout, so the ssh action's 10-minute default would cut a slow rollback off part way, leaving production neither on the new build nor back on the old one. test_deploy_workflows.py holds every ssh step in any workflow that runs rollback.sh to at least 15 minutes, and its job's cap to room for that after the capped steps ahead of it. It failed on these two steps before this change. Every deploying workflow must still show a rollback it checks, so one written in a form it does not recognise cannot drop out unnoticed. ClickUp 123zgec4mxd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Only deploy-production and its deploy-failure rollback took production-deploy. Concurrency is taken and released per job, so a following run's production deploy could start while this run's smoke tests and E2E were still testing production, and a rollback from either would then act on that deploy. Seen on 2026-09-23: run 35841886234's production deploy began while run 35841881231's production E2E was still running. Production now mirrors staging. Its deploy, both suites and all three rollbacks move into production-deploy-verify.yml, called from one Production job that holds production-deploy. A calling job is not complete until everything it called has finished, so the next run's production deploy waits for this run's verification and any rollback. The jobs inside hold no group of their own; the caller holds it for them, and one waiting on it would wait on its caller. Staging keeps its own group, so a following run's staging chain still runs while this run verifies production. The job bodies move unchanged but for their wiring: APP_DIR arrives as an input, the main-push gate is repeated on every job that can reach production, and the new workflow declares the same read-only token as the rest. A separate file rather than staging-deploy-verify.yml parameterised by environment: the two deploy scripts and smoke suites still differ in substance, and the single on-box deploy script (1.6) is where they converge. test_deploy_workflows.py pins that each environment's chain is one called workflow, triggered only by that call, under one calling job holding its group; that nothing inside takes a group; that no workflow reaches a droplet's key outside its chain but tower-service-contract's read-only staging probe; that every job reaching production is gated to a push to main; and that the two environments' groups differ. test_smoke_sends_radar_key.py follows the production smoke suite into the new file, and now also checks that ci.yml hands each called workflow its own environment's key, since a called workflow's secret is whatever its caller passes under that name. ClickUp 123zgec4mxd. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
cd278f4 to
ebedabb
Compare
8ff713a to
834ad0b
Compare
This comment has been minimized.
This comment has been minimized.
|
Rebased with #590 onto What that changed here:
Verification on this head: the same tests with |
|
Claude finished @jehanazad's task in 9m 16s —— View job Review complete
This sync is a merge from Prior findings, re-checked
Playwright-image gating. Moot — there's no One new, narrow finding from this pass
ahead = sum(int(step.get("timeout-minutes", 0)) for step in steps[:index])
assert ahead + int(timeout[:-1]) <= _job(workflow, job)["timeout-minutes"]A step with no declared This is narrow (checkout actions rarely take 20+ minutes) and not something introduced by the diff's logic error so much as a gap in what the test proves versus what its comment claims ("fit in its job's cap after the capped steps ahead of it" — a step with no cap isn't actually capped). Worth a look, not blocking: fixing it would mean asserting every step ahead of a rollback declares its own Rest of the diffSpot-checked the e2e comment-only changes ( No other issues found. |
ClickUp: 1.1 Hold the production lock across production smoke, E2E and their rollbacks
Stacked on #590 (1.4). Review that first; this PR's own changes are the two commits above it.
Why
Only
deploy-productionand its deploy-failure rollback tookproduction-deploy. GitHub takes and releases a concurrency group per job. So a following run's production deploy could start while this run's smoke tests and E2E were still testing production, and a rollback from either would then act on that deploy.Seen on 2026-09-23: run 35841886234's "Deploy to production" began at 09:26:53, while run 35841881231's "Playwright E2E (production)" ran until 09:26:59.
What changes
1. Production's smoke and E2E rollbacks get 15 minutes.
rollback.shcan rebuild the image it restores. The deploy-failure rollbacks allow 15 minutes for that, but these two set nocommand_timeout, so the ssh action's 10-minute default could cut one off part way.rollback.sh. Each must allow at least 15 minutes and fit in its job's cap after the capped steps ahead of it. Every deploying workflow must also still show such a step, so one written in an unrecognised form can't drop out of the check unnoticed.2. One lock across production's whole chain.
.github/workflows/production-deploy-verify.yml. OneProductionjob inci.ymlcalls it, holdingproduction-deploywithcancel-in-progress: false, as staging's chain already works.staging-deploy, so a following run's staging chain still runs while this run verifies production. Only production-to-production is serialised.APP_DIRand the Playwright image arrive as inputs.RADAR_API_KEYstays optional. The called workflow doesn't require it, which matches how the jobs behaved inci.yml: if it's unset, the smoke suite warns that its keyed checks are unverified, and the chain still starts.staging-deploy-verify.ymlparameterised by environment, because the two deploy scripts and smoke suites still differ in substance. The single on-box deploy script (1.6) is where they converge.docs/architecture.mdand every comment that sent a reader toci.ymlfor the production jobs now name the new file.What the tests pin
test_deploy_workflows.py:tower-service-contract's read-only staging probe;main;test_smoke_sends_radar_key.py: it reads the moved production smoke suite. It also finds each chain's caller inci.ymland checks the caller passes that environment's own key, since a called workflow's secret is whatever its caller passes under that name.What to expect after merge
Productionjobs: during a burst of merges one is expected, as a cancelledStagingjob already is. One run holds the group, one waits, and a third cancels the waiting one. The run that replaces it deploys a superset.Assumption shared with staging
GitHub holds the calling job's group until every job in the called workflow has finished, including an
always()rollback that runs after a cancel. Staging has relied on this since #381, and nothing in a PR run can exercise it.Overlap
#625 also edits
e2e/playwright.config.tsande2e/specs/dashboard.spec.ts. This PR only changes comments in those files, and git merges the two cleanly in either order.Verification
ci.ymlat Run production's E2E rollback in its own job, away from npm #590's head, differs only in the wiring listed above.test_no_job_in_a_chain_takes_a_lock_of_its_own.main, so its first real run is the first merge after this lands.🤖 Generated with Claude Code