Status
Proposed. Amends ADR 0020 (Amendment 5’s Google Cloud Run decision): thecloud_run.traffic_tag
recipe is removed.
Context
The last uncovered row of docs/coverage.yaml,cloud-run-tag, asks for what
gcloud run deploy --no-traffic --tag pr-42 --update-env-vars … does: a revision of a service per pull request, with
that pull request’s image and variables, reachable at its own URL and serving no traffic. ADR 0020 shipped the tag
alone as a recipe (cloud_run.traffic_tag, read-modify-write of the service’s traffic) and left the row uncovered,
because the revision and its variables were made by a deploy Sponson did not perform. vercel.deploy
has since shown that starting a deploy is a side effect a plan may own.
A revision is not something a request template can make. Cloud Run’s revisions are immutable and made by changing
the service’s whole template (assumption CR4 in packages/sim/src/routes/cloud_run.ts), so a new one must start
from the template the service has; the patch answers a long-running operation that must be polled (CR9); and a
template change alone moves the share of traffic on LATEST to the new revision (CR5), so the patch must also pin it.
Decision
- A first-class adapter,
cloud_run, with one op,revision. Given a service (providers.cloud_run.projectand.region, paramservice), animageandenv(literals,{ from },{ secret }), it makes a revision from the service’s current template with that image and those variables, named<service>-<tag>-<hash of the contents>, and puts the tag (default: the scope,pr-42) on it with no traffic share. Two patches, each with anupdateMaskand the service’setagand each waited for: the template (with every LATEST target bound to the revision it serves, as gcloud’s--no-trafficdoes, when the line allows it), thentrafficwith the tag moved onto the revision. Its outputs are the revision’s name and the tag’s URL exactly astrafficStatuses[].urianswers it; the URL is never built (CR6). - Pinning LATEST is opt-in. Binding a share on LATEST to a named revision changes how the service’s later
production deploys get traffic: they no longer get it by themselves. A preview tool must not do that silently. So
when a share follows LATEST (or
trafficis empty),planandapplyrefuse the line before any write withPARAM_INVALIDonpin_latest, the message saying what pinning does, how to undo it (update-traffic --to-latest) and the two ways forward:pin_latest: trueon the line, or pinning the traffic yourself first. Traffic already on named revisions needs no opt-in.pin_latestis a permission, not state: it is not hashed, and a line with nothing to write is not refused when traffic is put back on LATEST later. - No traffic is moved, so a line writes no environment (
writesEnvironmentnull) unless its provider block saysproduction: true, the way anhttpAPI block does: then every run with the line needs approval. - Destroy removes the tag only. The revision is left: Cloud Run deletes non-addressable revisions past its per-service limit by itself (CR12), and a revision cannot be deleted while it may receive traffic.
- Drift is the tagged revision’s image and the values of the variables the line declares. Revisions being
immutable, a console edit is a redeploy under the tag (
changed); a removed tag ismissing. Reconciling finds the plan’s revision again by its name and moves the tag back; nothing is made twice. - One way for one job. The
cloud_run.traffic_tagrecipe is removed: the op makes the revision and the tag, and a recipe tagging a revision someone else deployed would be a second, weaker way to cover the same row. - The credential is gcloud’s token, through the shared machinery. The adapter’s requests go through the generic
adapter’s client with a fixed
commandcredential (gcloud auth print-access-token, CR1), so the token is fetched once per run, re-fetched after a 401 and masked like any secret.AdapterAboutgains an optionalcredentialCommandso the generated docs andsponson initsay which command, rather than naming an environment variable that does not exist.
Consequences
- The
cloud-run-tagrow is covered, and with it every row of the coverage matrix that is not manual. - A service whose traffic follows LATEST needs a decision before its first preview revision:
pin_latest: true(then its traffic is pinned to the revision that served it, as after anygcloud run deploy --no-traffic, and later production deploys do not receive traffic until it is sent to them withupdate-traffic --to-latest), or pinning it by hand. Until then the line is refused, with nothing written. docs/plan-format.md says so. - The service’s template is left as the preview revision’s, as after any
gcloud run deploy --tag: a production deploy that sets only some variables (--update-env-vars) inherits the preview’s others. Production deploys should set every variable (--set-env-vars, orgcloud run services replace). - Breaking for lines that used
recipe: cloud_run.traffic_tag: destroy their scopes (or remove the tags by hand) before upgrading, then usecloud_run.revision. - The etag precondition (CR8) and the operation ending when the revision is ready (CR9) are not documented; the
adapter retries a refused patch after reading the service again, and waits for the revision’s
Readycondition as well.scenarios/contract.test.tspins both for a live run.