infra(api): carry the frontend deploy's three edges back to the API - #11212
Merged
Conversation
api/cloudbuild.yaml invented the candidate rollout; app/cloudbuild.yaml (#11207) then improved it and the improvements never came back. - `:latest` waits on `promote`, not on `build-image`. The tag used to reach the registry before the candidate was deployed, let alone smoked, so a failed smoke still left `:latest` on the image that failed it. - The smoke re-asserts the `candidate` tag after its probes as well as before. The tag is shared across builds; a concurrent one moving it mid-smoke would otherwise let this build promote a revision it only believed it probed. - The probes stop piping into `grep -q`, which exits at the first match and SIGPIPEs curl — the form only ever passed because `-ceu` carries no `pipefail`. They go through the same `expect` helper app/cloudbuild.yaml uses; `/health` keeps a bare variant, because reaching it without the origin header is what its gate exemption has to prove. The expanded script (Cloud Build `$$` resolved) passes `bash -n`, and the helpers were exercised against a local server for the happy path, a missing needle and a 404. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Deployment guarantees are overstated, and the changelog contains contradictory rollout descriptions.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR hardens the API’s candidate-based Cloud Run deployment.
Changes:
- Publishes
:latestonly after promotion. - Adds clearer smoke-probe helpers.
- Rechecks the candidate tag after probing.
File summaries
| File | Description |
|---|---|
api/cloudbuild.yaml |
Strengthens deployment sequencing and smoke checks. |
CHANGELOG.md |
Documents the rollout changes. |
Review details
- Files reviewed: 2/2 changed files
- Comments generated: 3
- Review effort level: Balanced
💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Copilot, twice and correctly: status.traffic is control-plane state. Both reads can name this revision while tag-URL propagation still sends a probe to the previous candidate — the residual app/cloudbuild.yaml already documents at its own smoke step. The check stays; the claim is now "the tag was never reassigned while the probes ran", with the residual and its mild worst case spelled out. Also: the frontend entry's parenthetical said the API pushes :latest alongside the deploy, which this PR is what changes. Put in the past tense and pointed at the entry above it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
# Conflicts: # CHANGELOG.md
The fragment convention landed on main in #11215 while this PR was open: a PR writes changelog.d/<slug>.md and leaves CHANGELOG.md alone. The one CHANGELOG.md edit that stays is not this PR's own entry but the correction of a PREVIOUS one — #11207's frontend bullet still says the API pushes :latest alongside the deploy, which is exactly what this PR changes. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
Measured, not assumed: with the correction in place, `tools.changelog check --base origin/main` answers "CHANGELOG.md [Unreleased] gained a bullet". The fragment gate compares bullet SETS, so editing an existing bullet is indistinguishable from adding one. So #11207's frontend entry keeps its now-stale parenthetical ("the API still pushes :latest alongside the deploy") for the moment. It needs either a skip-changelog-labelled touch or a pass at the release cut, where [Unreleased] is edited by hand anyway. Reported rather than forced. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3
MarkusNeusinger
added a commit
that referenced
this pull request
Sep 3, 2026
One line of `CHANGELOG.md`, so the `[Unreleased]` section describes one rollout state instead of two. #11207's frontend entry said: > …then `update-traffic` to exactly that revision (the chains are not identical — this one pushes `:latest` only after the promotion, **where the API still pushes it alongside the deploy**). #11212 is what changed that, so the sentence became false the moment it merged. Now: > …to exactly that revision (the chains **were** not identical **at the time** — this one **pushed** `:latest` only after the promotion, where the API still **pushed** it alongside the deploy; **the API caught up in #11212**). Past tense, and the closing PR named rather than a position: the release cut folds fragments in **above** the existing bullets, so "the entry above" would be wrong today and right after the cut, while `#11212` is true in both. ## Why `skip-changelog` rather than a fragment This corrects an existing entry; it is not a change worth a release-notes line of its own. And it *needs* the label, because the fragment gate compares bullet **sets** — an edited bullet is indistinguishable from an added one. Measured rather than assumed, when this same edit first rode along in #11212: ``` error: CHANGELOG.md [Unreleased] gained a bullet — it belongs in a fragment: - **The frontend deploys through a candidate revision instead of straigh… ``` That is the reason it was taken back out of #11212 and moved here. The `skip-changelog` label did not exist in this repository — #11215 documented it as the escape hatch without creating it — so it was created for this PR (grey, "No changelog fragment needed: this PR changes nothing worth a release-notes line"). The gate's own `if:` condition now has something to match. **Noted for later, not built here:** the gate could tell an *added* bullet from a *changed* one by treating the bold title as the bullet's identity, which would let a correction like this pass without a label. That is a sibling-pair change for both repos, deliberately out of scope tonight. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3 Co-authored-by: Claude Fable 5.1 <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.
api/cloudbuild.yamlinvented the candidate rollout.app/cloudbuild.yamlthen improved it in #11207, and the improvements never came back to the file they came from. Three of them do here — nothing else in the deploy changes.1.
:latestwas published before the smokepush-latestwaited onbuild-image. The tag therefore reached Artifact Registry before the candidate was even deployed, let alone probed: a build whose smoke failed still left:latestpointing at the image that failed it — and:latestis what a human reaches for when they want "the image that is running".It now waits on
promote, exactly as the frontend's does. Still not a guarantee across concurrent builds (two overlapping deploys race for the tag), which is why the deploy step pulls:$BUILD_IDand never:latest; the comment says so at the step.2. The
candidatetag was checked once, not twicecandidateis a shared tag. The smoke resolved it, asserted it named this build's revision, then probed — and a concurrent build moving the tag between that assertion and the last probe would leave this build promoting a revision it only believed it had smoked.The tag is now re-asserted after the probes. A competing build only ever tags its own revision and never ours back, so seeing our revision at both ends means every probe in between hit it. Same reasoning and same wording as
app/cloudbuild.yaml.The API is the service where this matters most: its probes are the ones that touch Cloud SQL and the admin gate.
3.
curl … | grep -qis a trap that had not sprung yetgrep -qexits at the first match and SIGPIPEs curl. The pipeline only ever passed because-ceucarries nopipefail— the day someone addsset -o pipefailas a hardening pass, every deploy goes red for reasons that have nothing to do with the deploy.The probes now go through the same
expecthelper the frontend build uses: fetch to a file,grep -qF, and on failure say which probe failed and what was missing./healthkeeps a bare variant (expect_bare) because reaching it without the origin header is precisely what its gate exemption has to prove.Verification
No Cloud Build dry-run exists for this, so the script was checked out of band:
$$→$, substitutions resolved) —bash -nclean, no leftover$$.OK: /health,OK: /libraries), a needle that is not in the body (needle missing must fail (/libraries is missing: NOT_THERE), exit 1), and a 404 (candidate did not serve /nope, exit 1).HDR=()expanded as"${HDR[@]}"underset -uwas confirmed safe on bash 5.2 — the cloud-sdk builder's bash.The real proof is the next
deploy-apibuild, which runs the whole chain.🤖 Generated with Claude Code
https://claude.ai/code/session_01UEScQMZFvxxNNyNJYryfa3