Architecture Decision Record
ADR-064: Deploy Preconditions as Proof-of-Recency Gates
Status
Accepted (2026-08-01). Revised 2026-08-07: the MMCA.Helpdesk workflow inventory below was corrected
(it also carries release-templates.yml, and its ci.yml runs two jobs, not one); the conclusion
that Helpdesk has no rollout for a recency gate to block is unchanged. Revised 2026-09-01: the broker
recency gate now requires TWO job conclusions, cross-service and servicebus-emulator-smoke, in
both repos (ADC promoted the emulator tier on 2026-08-31 as TD-17, Store immediately after), so the
bullet that described one required job and treated servicebus-emulator-smoke as the advisory job
that justifies reading jobs instead of the run conclusion is restated; that justification now rests
on apphost-smoke, which is still advisory. The same date records a second change, in MMCA.ADC
only: backend-test-gate (TD-20, added 2026-08-31) joined deploy.needs, so ADC's deploy condition
now tolerates skipped from two conditional test gates rather than one, while MMCA.Store still
tolerates it from e2e-gate alone. The three freshness gates and the gate mechanism are unchanged.
Revised 2026-09-03: MMCA.Store gained its own backend-test-gate, so the asymmetry the 2026-09-01
revision recorded is closed and both repos now run the same complementary pair of conditional test
gates; Store's deploy.yml citations are re-anchored for the lines the new job displaced.
Revised 2026-09-11: a fourth freshness gate, cross-browser-freshness, now runs in both repos
(MMCA.ADC/.github/workflows/deploy.yml:1039, MMCA.Store/.github/workflows/deploy.yml:953), so
every sentence that said "three" says four, and the same break-glass input governs all four. MMCA.ADC
also added a third conditional test gate, ai-eval-gate
(MMCA.ADC/.github/workflows/deploy.yml:500-502), which Store does not have, so the two
deploy.needs lists are no longer identical: 12 entries in ADC, 11 in Store. Every deploy.yml
citation below is re-anchored for the lines the new jobs displaced.
Revised 2026-09-19: the MMCA.Helpdesk inventory is corrected again. Its ci.yml now runs four
jobs, not the two the 2026-08-07 revision recorded: changes, build-and-test, template-smoke
and postgresql-canary, the last an advisory continue-on-error PostgreSQL consumer canary added
for ADR-113. The workflow-file count is still four and none of the four ci.yml jobs has an Azure
step, so the conclusion that Helpdesk has no rollout for a recency gate to block is unchanged.
Revised 2026-09-25: every deploy.yml citation in both repos is re-anchored onto its current line.
The gates, their windows, the break-glass contract and the ai-eval-gate asymmetry (12 deploy.needs
entries in ADC, 11 in Store) are unchanged.
Revised 2026-10-01 (the four gate bodies now run as one shared composite action owned by MMCA.Common,
so the per-repo copies and their message differences are gone; see Revision below).
Revised 2026-10-06: the job-reading gates now page an unfiltered run listing and filter completed runs client-side (MMCA.Common v1.220.0), and every citation is re-anchored.
Revised 2026-10-07: the four gates now run as the four steps of one freshness job that skips a docs-only push, backend-test-gate is retired in both repos, and the success-only lookup filters client-side (see Revision below).
Revised 2026-10-07: the Rationale's API-cost sentence now states the bounded read count and the
per-repo job timeout, and the Decision notes that both CONTRIBUTING files still name the gates as jobs.
Revised 2026-10-21: both CONTRIBUTING files now name the single freshness job and its four steps,
the broker-step comments are restated (neither mentions an advisory job; they differ in two places),
and every deploy.yml anchor is re-pointed.
Revised 2026-10-26: anchors refreshed after a one-line deploy.yml insertion in both apps and a
five-line e2e.yml insertion in ADC moved them.
Revised 2026-10-28: anchors refreshed after both CONTRIBUTING files moved their push-only sentence.
Context
A production rollout in both deployed apps waits on a list of jobs in deploy.needs
(MMCA.ADC/.github/workflows/deploy.yml:1161, MMCA.Store/.github/workflows/deploy.yml:1099). Most of
those jobs have the change itself as their subject: a supply-chain audit (ADR-038), a FinOps
cost check, a chromium end-to-end run against the booted stack. They answer "is this build good."
Four kinds of verification cannot answer that question inside a deploy, because they cannot run
inside one, or cost too much to run there. A point-in-time restore takes minutes and provisions a real
Azure database
(MMCA.ADC/.github/workflows/dr-drill.yml:3-5, MMCA.Store/.github/workflows/dr-drill.yml:3-5). A k6
load run drives sustained traffic at production read endpoints
(MMCA.ADC/.github/workflows/load-test.yml:3-6). The Testcontainers broker round-trip needs a Docker
daemon that the gating jobs deliberately do not have
(MMCA.ADC/.github/workflows/deploy.yml:881-882, MMCA.Store/.github/workflows/deploy.yml:825-827).
The firefox and webkit end-to-end legs cost roughly
twenty minutes per engine and were taken off the per-deploy gate in 2026-07 for exactly that reason,
leaving the deploy-path e2e-gate chromium-only
(MMCA.ADC/.github/workflows/deploy.yml:909-912,
MMCA.Store/.github/workflows/deploy.yml:851-853). Each therefore lives on its own schedule: weekly
Monday for the drill (MMCA.ADC/.github/workflows/dr-drill.yml:32,
MMCA.Store/.github/workflows/dr-drill.yml:33), monthly for the load test
(load-test.yml:23, Store :21), weeknights for the broker tier
(MMCA.ADC/.github/workflows/cross-service-tests.yml:32,
MMCA.Store/.github/workflows/cross-service-tests.yml:35), and alternating weekly crons for the two
non-chromium engines, Monday firefox and Thursday webkit
(MMCA.ADC/.github/workflows/e2e.yml:54-55, MMCA.Store/.github/workflows/e2e.yml:46-47).
ADR-009 requires that those objectives exist and that a restore be drilled: its DR doc carries a drill-result table "that cannot stay empty." It says nothing about age. A drill recorded in March and a drill recorded last Monday read identically in a document, and a scheduled workflow that has been silently failing for six weeks still leaves a green-looking history behind it. Nothing decided what a deploy should do when the newest proof is old. ADR-057, the other CI-gate record, is explicitly a pull-request merge gate over migration shape and covers none of this.
Decision
A production deploy is blocked not only on green tests but on proof of recency for out-of-band verification: four gates assert that a real drill, a real load run, a real broker round-trip and a real run on each non-chromium browser engine happened recently enough to still mean something.
Four recency gates run as the four steps of one
freshnessjob indeploy.needs, beside the result-based gates. The job is listed withchanges,supply-chain,cost-guard,e2e-gate,foundationandbuild-images, plusai-eval-gatein ADC only, which is why ADC's list carries 8 entries and Store's 7 (MMCA.ADC/.github/workflows/deploy.yml:1161,MMCA.Store/.github/workflows/deploy.yml:1099). The job runs onubuntu-latestholdingactions: readandcontents: readonly, with a 5-minute timeout in ADC and 10 in Store (MMCA.ADC/.github/workflows/deploy.yml:833-840,MMCA.Store/.github/workflows/deploy.yml:777-784). Each step keeps its gate's name as bothnameandid:dr-freshness,load-freshness,cross-service-freshnessandcross-browser-freshness(MMCA.ADC/.github/workflows/deploy.yml:846-847,865-866,886-887,925-926,MMCA.Store/.github/workflows/deploy.yml:791-792,809-810,828-829,859-860). The last three carryif: ${{ !cancelled() }}, so they still run and report after an earlier step fails (MMCA.ADC/.github/workflows/deploy.yml:867,888,927,MMCA.Store/.github/workflows/deploy.yml:811,830,861). Every step calls one shared composite action,ivanball/MMCA.Common/.github/actions/freshness-gate(MMCA.ADC/.github/workflows/deploy.yml:851,870,893,933,MMCA.Store/.github/workflows/deploy.yml:795,814,835,867), so the gate logic has one implementation (MMCA.Common/.github/actions/freshness-gate/action.yml).Each gate asks the Actions API for the newest proof of one workflow and fails on its age. The window is the action's
window-daysinput:8for the weekly DR drill (MMCA.ADC/.github/workflows/deploy.yml:855,MMCA.Store/.github/workflows/deploy.yml:799),35for the monthly k6 run (:874/:818),5for the weekday-nightly broker tier (ADC:899, Store:841, widened from 3 on 2026-07-18 to tolerate a weekend or holiday, a note carried at:897-898in ADC and:839-840in Store), and10for the two alternating weekly browser crons (ADC:938, Store:875, a seven-day per-engine cadence plus slack for a skipped night or a late re-run, reasoned inline at:915-918in ADC and:871-874in Store). The DR and load gates pass norequired-jobs, so the action pages an unfiltered run listing ofdr-drill.ymlandload-test.ymlrespectively, keeps the runs that completed with conclusionsuccess(filtered client-side) and reads the newest one'supdated_at(MMCA.Common/.github/actions/freshness-gate/action.yml:152-167,223-231), computes the age in whole days and fails when it exceeds the window (MMCA.Common/.github/actions/freshness-gate/action.yml:143-147,246).Absence fails; it does not pass. An empty result (no successful run at all) prints a
FAILline naming the workflow to dispatch and exits 1 (MMCA.Common/.github/actions/freshness-gate/action.yml:227-228); in the job-reading modes a missing proof sets thefailflag and the step exits 1 at the end (MMCA.Common/.github/actions/freshness-gate/action.yml:214-215,246). One action produces every message, so both repos print the same sentences apart from the scan bound in the cross-browser absence message. A failed Actions API read also fails closed with::error::, in every mode (MMCA.Common/.github/actions/freshness-gate/action.yml:212,224).The broker gate keys off the jobs, not the run conclusion. It passes the two broker job names as
required-jobswithrequired-jobs-mode: same-runandmax-runs: '25'(MMCA.ADC/.github/workflows/deploy.yml:901-905,MMCA.Store/.github/workflows/deploy.yml:843-847), so the action enumerates completed runs ofcross-service-tests.yml(any conclusion) and, for each, asks the jobs API which jobs concludedsuccess, stopping at the first run where all the named jobs did (MMCA.Common/.github/actions/freshness-gate/action.yml:186-216). The listing is requested without a server-sidestatusfilter and narrowed to completed runs client-side, paging (at most five pages ofmax-runseach) untilmax-runscompleted runs are collected or the listing ends, and the first listed run's id and creation time go to the step summary so a stale listing is visible (MMCA.Common/.github/actions/freshness-gate/action.yml:152-182). Two jobs are required in both repos as of 2026-08-31 (ADC, TD-17) and immediately after in Store:cross-service, the Testcontainers RabbitMQ outbox to broker to consumer round-trip, andservicebus-emulator-smoke, the Azure Service Bus emulator topology plus AMQP round-trip, so the production transport is a deploy precondition too (ADR-066). The reasoning for reading jobs rather than the run is recorded inline in both repos: a skip-if-unchanged guard can make a run concludesuccesswith the test jobs skipped and no round-trip executed (MMCA.ADC/.github/workflows/deploy.yml:891-892,MMCA.Store/.github/workflows/deploy.yml:833). Both comments also count a completed run of any conclusion, "so a cancelled-but-proven run still counts" (MMCA.ADC/.github/workflows/deploy.yml:889-890,MMCA.Store/.github/workflows/deploy.yml:831). Neither mentions an advisory job, and neither repo'scross-service-tests.ymlcarries an advisory job today: its jobs areshould-run,cross-serviceandservicebus-emulator-smoke(MMCA.ADC/.github/workflows/cross-service-tests.yml:51,79,142,MMCA.Store/.github/workflows/cross-service-tests.yml:54,85,141).The cross-browser gate resolves each engine separately, for the same reason. It passes
E2E (firefox)andE2E (webkit)asrequired-jobswithrequired-jobs-mode: per-job(MMCA.ADC/.github/workflows/deploy.yml:940-944,MMCA.Store/.github/workflows/deploy.yml:877-881), so the action walks completede2e.ymlruns newest first once per engine, takes the first run in which that engine's job concludedsuccess, and age-checks each engine's proof on its own; any engine that is stale or missing sets thefailflag, so both are reported before the step exits 1 (MMCA.Common/.github/actions/freshness-gate/action.yml:147,215,234-241,246). Two properties of the nightly force the per-job read: the engines run on alternating crons, so the newest firefox proof and the newest webkit proof normally come from different runs and there is no single run to point at, and the scheduled non-chromium legs arecontinue-on-error, so a run can concludesuccesswith an engine red (MMCA.ADC/.github/workflows/e2e.yml:152,MMCA.Store/.github/workflows/e2e.yml:149). ADC records both properties inline (MMCA.ADC/.github/workflows/deploy.yml:915-916,920-922,928-930); Store's comments record only the alternation (MMCA.Store/.github/workflows/deploy.yml:853,862-863).Break-glass exists as a dispatch input pair and refuses to fire without a written reason.
workflow_dispatchcarriesskip_freshness_gates(boolean, defaultfalse) andskip_justification(string, default empty) (MMCA.ADC/.github/workflows/deploy.yml:13-20,MMCA.Store/.github/workflows/deploy.yml:26-33). Every step passes both to the action asskipandskip-justification(MMCA.ADC/.github/workflows/deploy.yml:857-858,876-877,906-907,945-946,MMCA.Store/.github/workflows/deploy.yml:801-802,820-821,848-849,882-883), which maps them into its step environment (MMCA.Common/.github/actions/freshness-gate/action.yml:93-94) and, when the flag is true with an empty justification, emits::error::and exits 1 (MMCA.Common/.github/actions/freshness-gate/action.yml:100-103). With a justification it writes a headed break-glass block plus the reason to the step summary, raises a::warning::annotation carrying the same text, and exits 0 (MMCA.Common/.github/actions/freshness-gate/action.yml:104-113). One input governs all four gates, because every step reads the same two inputs.The skip path is unreachable on a push, and the gates never run on a pull request or a docs-only push.
inputsis empty outside a dispatch, so${FG_SKIP:-false}readsfalse(MMCA.Common/.github/actions/freshness-gate/action.yml:100), and thefreshnessjob carriesif: github.event_name != 'pull_request' && needs.changes.outputs.code == 'true'(MMCA.ADC/.github/workflows/deploy.yml:835,MMCA.Store/.github/workflows/deploy.yml:781): the gates run on a push or manual dispatch that changes code, and a docs-only push skips them along with the deploy, whose condition also requiresneeds.changes.outputs.code == 'true'(MMCA.ADC/.github/workflows/deploy.yml:1192,MMCA.Store/.github/workflows/deploy.yml:1126).A break-glass skip still reports success, which is what the deploy condition demands. The deploy runs under
always()with explicit per-need results and requiressuccessfrom thefreshnessjob by name; only a conditional gate may beskipped(MMCA.ADC/.github/workflows/deploy.yml:1188-1199,MMCA.Store/.github/workflows/deploy.yml:1122-1132). Store allows it fore2e-gatealone, which runs only when the diff touches the UI (MMCA.Store/.github/workflows/deploy.yml:764); ADC allows it fore2e-gate(MMCA.ADC/.github/workflows/deploy.yml:819) and forai-eval-gate, which runs only when the diff touches the scoring code (MMCA.ADC/.github/workflows/deploy.yml:526). A code deploy that skipse2e-gateis still tested: the test gate is the pull request'sbuild-and-test, because branch protection enforces admins and requires an up-to-date branch, so the merged tree is the PR-tested tree (MMCA.ADC/.github/workflows/deploy.yml:814-816,1162-1164,MMCA.Store/.github/workflows/deploy.yml:760-762,1100-1102). Exiting 0 inside the step is what keeps break-glass compatible with that contract, and both repos say so in the comment above the condition (MMCA.ADC/.github/workflows/deploy.yml:1182-1183,MMCA.Store/.github/workflows/deploy.yml:1116-1118).Deploy-time only, and deliberately not a required merge check. Both repos document the
freshnessjob (the four gates) as push-only, run after merge and never added to branch protection, since a job that never runs on a pull request would block every merge (MMCA.ADC/CONTRIBUTING.md:42-45,115-116,MMCA.Store/CONTRIBUTING.md:42-45,122-123). In both files the push-only half is the earlier passage and the "do not require these jobs" sentence is the later one. Both match the workflow: the earlier passage names the singlefreshnessjob and all four of its steps (dr-freshness,load-freshness,cross-service-freshnessandcross-browser-freshness), and the later sentence listsfreshnessamong the push-only jobs (MMCA.ADC/CONTRIBUTING.md:115,MMCA.Store/CONTRIBUTING.md:122), which is the one job that runs (MMCA.ADC/.github/workflows/deploy.yml:833,MMCA.Store/.github/workflows/deploy.yml:777).
Adoption is the two deployed apps, and all four gates are the same shape in both. The
deploy.needs lists are not identical, because ADC carries ai-eval-gate and Store does not:
8 entries in ADC against 7 in Store (MMCA.ADC/.github/workflows/deploy.yml:1161,
MMCA.Store/.github/workflows/deploy.yml:1099). The freshness job and e2e-gate are common to the
two lists. There is no per-repo gate body: both repos call
the same MMCA.Common action, so every message is identical apart from that scan bound, and the only per-repo difference in the
gate inputs is the cross-browser scan bound, 40 completed e2e.yml runs in ADC against 50 in Store
(MMCA.ADC/.github/workflows/deploy.yml:944, MMCA.Store/.github/workflows/deploy.yml:881). The job
timeout differs too, 5 minutes in ADC against 10 in Store
(MMCA.ADC/.github/workflows/deploy.yml:837, MMCA.Store/.github/workflows/deploy.yml:780). The
broker-step comments explaining the job read differ in two places: ADC's opens with a "Shared
composite action (ADR-038)." sentence and Store's closes with the clause "and a cancelled run can
still hold a genuine proof" (MMCA.ADC/.github/workflows/deploy.yml:889-892,
MMCA.Store/.github/workflows/deploy.yml:831-834);
ADC's backlog items TD-02 and TD-17 survive only in comments, in ADC
(MMCA.ADC/.github/workflows/deploy.yml:879,1167) and in the Store step header that mirrors them
(MMCA.Store/.github/workflows/deploy.yml:823); the header comments above each step still
differ in wording.
MMCA.Helpdesk has no
deploy pipeline at all: its .github/workflows/ holds four files, ci.yml, the two Claude
workflows, and release-templates.yml, and the fourth publishes the MMCA.Templates dotnet new pack
to nuget.org on a templates-v* tag rather than rolling anything out
(MMCA.Helpdesk/.github/workflows/release-templates.yml:3,17). ci.yml runs four jobs and none has
an Azure step: changes classifies the changed paths so the heavy advisory job below can skip a
docs-only pull request (MMCA.Helpdesk/.github/workflows/ci.yml:17), build-and-test builds and
tests against MMCA.Common source (MMCA.Helpdesk/.github/workflows/ci.yml:58), template-smoke
packs the template, generates an app and builds it through build/templates/smoke.ps1
(MMCA.Helpdesk/.github/workflows/ci.yml:120), and postgresql-canary generates a
PostgreSQL-shaped app against a real postgres:17 service container as the ADR-113 consumer canary,
advisory by continue-on-error and gated on needs.changes.outputs.code == 'true'
(MMCA.Helpdesk/.github/workflows/ci.yml:146,152,155). There is no rollout for a recency gate to
block.
MMCA.Common has no deploy workflow either: its .github/workflows/ holds three files, ci.yml,
release.yml and load-tests.yml (a scheduled and manually dispatched load run,
MMCA.Common/.github/workflows/load-tests.yml:16-17); it publishes
packages on a tag rather than deploying a service, and none of those files runs a freshness gate. It does own the gate implementation: the composite action
MMCA.Common/.github/actions/freshness-gate/action.yml, which both deployed apps consume.
Rationale
- A proof with no expiry date is documentation, not a control. ADR-009 already required the drill to be recorded, and recording it was the honest half of the problem; a record has no shelf life. The age comparison is what converts an artifact into something that can say no.
- The expensive verification stays off the deploy path. Running the restore, the k6 scenario or a
Testcontainers broker tier per deploy would add minutes and real Azure or Docker cost to every
rollout, and the broker tier could not run there at all. A bounded set of Actions API reads (at most
five listing pages per gate in the success and same-run modes, and per required job in the per-job
mode the cross-browser gate uses, plus one jobs read per scanned run per required job,
MMCA.Common/.github/actions/freshness-gate/action.yml:164,190,193,236-239) inside a job with a 5-minute timeout in ADC and 10 in Store (MMCA.ADC/.github/workflows/deploy.yml:837,MMCA.Store/.github/workflows/deploy.yml:780) buys the same guarantee at effectively zero marginal cost, which is exactly why the gate is affordable enough to keep enabled. - Fail on absence, because a vacuous gate is worse than none. A gate that passes when it finds nothing reads as evidence while proving nothing, so an empty query result exits 1 rather than 0.
- Job-level truth for the broker and cross-browser gates. The run conclusion is wrong in both directions here (a skipped-but-green run, a failed-but-proven run), so the only honest signal is whether the specific job that performs the round-trip, or that drives the engine, actually ran and passed. Keying off the cheaper signal would have produced both a false pass and a deploy-blocking false red.
- An unenforced proof rots quietly. Moving firefox and webkit off the per-deploy gate saved the
minutes but left nothing asserting those legs still passed, so cross-engine coverage could sit red
for weeks while deploys kept shipping green
(
MMCA.Store/.github/workflows/deploy.yml:854-855). A recency gate restores enforcement for two Actions API queries rather than by putting roughly forty minutes of browser time back on every UI deploy (MMCA.ADC/.github/workflows/deploy.yml:911-912). - A justified skip beats an undocumented one. The alternative to break-glass is not "no skip," it is someone commenting the gate out or force-merging around it. Requiring a non-empty reason, then printing it in the step summary and as a run annotation, ties the decision to the exact deploy that shipped without it.
- Windows are the cadence plus slack, not round numbers. 8 days over a weekly cron, 35 over a
monthly one, 5 over a weekday-only nightly, 10 over a weekly per-engine cron: each tolerates one
missed or delayed run without tolerating a dead schedule. Store's comment states the boundary
explicitly for the newest window: 7 plus 1 would red the deploy on any single-week hiccup, and a
wider one would let an engine rot for two full cycles (
MMCA.Store/.github/workflows/deploy.yml:871-874).
Trade-offs
- An unrelated stale proof blocks an unrelated deploy. A one-line hotfix does not ship when the
monthly k6 cron did not fire, and the failure surfaces after merge: the
freshnessjob goes red anddeployis left skipped by its explicit per-need condition (MMCA.ADC/.github/workflows/deploy.yml:1188-1199,MMCA.Store/.github/workflows/deploy.yml:1122-1132). The coupling is deliberate, and the cost is a blocked rollout at whatever moment the schedule happened to lapse. - Break-glass is auditable but human-judged. Only non-emptiness is checked: nothing rates the reason, there is no second approver, no expiry and no tracked follow-up beyond the step-summary sentence telling the operator to re-run the workflow. One input also skips all four gates at once, so a deploy forced past a broken broker nightly silently forgoes the DR, capacity and cross-engine proofs too.
- Recency is not relevance. The DR drill rotates across the live per-service databases by ISO week
(four in ADC, three in Store), so a "fresh" recovery proof may belong to a database this deploy does
not touch (
MMCA.ADC/.github/workflows/dr-drill.yml:7-11,MMCA.Store/.github/workflows/dr-drill.yml:7-13). The same applies to the other three: the newest k6 run, the newest round-trip and the newest firefox and webkit legs proved an earlier commit, not this one. The cross-browser window is the widest, so a UI change can ship against an engine proof up to ten days old. - Whole-day arithmetic widens every window. The age is integer division by 86400 compared with
-gt(MMCA.Common/.github/actions/freshness-gate/action.yml:143,145), so a proof up to a day older than the stated number still passes. - The job-reading gates scan a bounded history. The broker gate walks at most the 25 most recent
completed runs (
MMCA.ADC/.github/workflows/deploy.yml:905,MMCA.Store/.github/workflows/deploy.yml:847) and the cross-browser gate at most 40 in ADC and 50 in Store (MMCA.ADC/.github/workflows/deploy.yml:944,MMCA.Store/.github/workflows/deploy.yml:881); the action rejects amax-runsoutside 1 to 100 (MMCA.Common/.github/actions/freshness-gate/action.yml:119-120). Past the bound they fail closed with a message that names it, "no completed <workflow> run among the newest <N> in which <jobs> executed and passed" (MMCA.Common/.github/actions/freshness-gate/action.yml:214). Each scanned run also costs a second API call for its jobs, and the cross-browser gate pays that per engine; when in-progress runs crowd the listing, collecting the completed runs can take extra listing pages (at most five,MMCA.Common/.github/actions/freshness-gate/action.yml:164-180). - No pull-request signal. Because the gates are push-only, a contributor cannot learn from the PR that a proof is about to be stale; the discovery happens on the post-merge deploy run.
Revision (2026-10-01)
The four gate bodies no longer live in each repo's deploy.yml. MMCA.Common v1.216.0 (#463) added the
composite action MMCA.Common/.github/actions/freshness-gate/action.yml, and both deployed apps now
call it from a single step per gate (MMCA.ADC/.github/workflows/deploy.yml:861,886,915,967,
MMCA.Store/.github/workflows/deploy.yml:834,859,887,926). The decision is unchanged: the same four
gates, windows (8, 35, 5, 10, now the window-days input in place of the job-level
FRESHNESS_DAYS), job-reading logic, fail-on-absence rule and break-glass contract hold, and the
deploy condition and deploy.needs counts (12 in ADC, 11 in Store) are untouched. What changed is
where the logic lives and three consequences of sharing it. First, the per-repo differences in
messages are gone (apart from the cross-browser scan bound): ADC's broker failure messages no longer carry the (TD-02 / TD-17) suffix, and
the broker-gate step comments in the two repos are now identical
(MMCA.ADC/.github/workflows/deploy.yml:911-914, MMCA.Store/.github/workflows/deploy.yml:883-886),
so ADC's inline naming of apphost-smoke and of the 2026-07-21 to 07-24 break-glass incident, and
Store's comment about workflow_call legs not appearing in the listing, no longer exist in source.
Second, the job-reading failure message now names the scan bound
(MMCA.Common/.github/actions/freshness-gate/action.yml:176) and a failed Actions API read fails
closed with ::error:: (:173-174), so the "never ran rather than not recently" ambiguity the
Trade-offs recorded is gone. Third, the cross-browser gate age-checks each engine independently
(:197-200) rather than computing the older of the two proofs, which gives the same outcome. Every
gate-body citation in Decision, Rationale and Trade-offs is re-anchored onto the action or onto the
current deploy.yml lines, and the Adoption paragraph now records that MMCA.Common owns the gate
implementation. Not recorded as a decision here: both apps reference the action at the mutable ref
@main (MMCA.ADC/.github/workflows/deploy.yml:861, MMCA.Store/.github/workflows/deploy.yml:834)
while other actions in the same file are pinned to a commit SHA
(MMCA.ADC/.github/workflows/deploy.yml:79), so a merge to MMCA.Common main changes both apps'
deploy gates with no consumer-side change. Whether that is deliberate is not determinable from source.
Revision (2026-10-06)
- The job-reading listing changed in MMCA.Common v1.220.0 (
MMCA.Common/CHANGELOG.md:247): runs are listed without the server-sidestatus=completedfilter, narrowed to completed runs client-side, paged (at most five pages) untilmax-runscompleted runs are collected, and the first listed run is logged to the step summary (MMCA.Common/.github/actions/freshness-gate/action.yml:148-198). Decision and Trade-offs now record this, plus the 1 to 100 validation ofmax-runs(:116-118). The pass/fail decision on a correct listing is unchanged. - MMCA.Common's workflow inventory in the Adoption paragraph now lists five files, adding
load-tests.yml; none runs a freshness gate, so the conclusion stands. - Correction to the 2026-10-01 note on the
@mainref: third-party actions are SHA-pinned (MMCA.ADC/.github/workflows/deploy.yml:79,MMCA.Store/.github/workflows/deploy.yml:88), but every first-party MMCA.Common action in both files is referenced at@main, not onlyfreshness-gate(ADC:478,647,672, Store:510,617,642). - Current locations of jobs named in earlier Status revisions:
cross-browser-freshnessatMMCA.ADC/.github/workflows/deploy.yml:1000andMMCA.Store/.github/workflows/deploy.yml:954;ai-eval-gateatMMCA.ADC/.github/workflows/deploy.yml:558-560. - Every
path:lineanchor in Context, Decision, Rationale and Trade-offs was re-verified against current source and re-anchored; gates, windows,deploy.needscounts (12 in ADC, 11 in Store) and the break-glass contract are unchanged.
Revision (2026-10-07)
- The four freshness gates are no longer four jobs. Both apps run them as the four steps of one
freshnessjob, each step keeping its gate's name asnameandid, the last three underif: ${{ !cancelled() }}(MMCA.ADC/.github/workflows/deploy.yml:817-930,MMCA.Store/.github/workflows/deploy.yml:774-880), anddeploy.needsnamesfreshnessin their place (MMCA.ADC/.github/workflows/deploy.yml:1145,MMCA.Store/.github/workflows/deploy.yml:1083). Gate names, windows, job-reading modes, scan bounds and the break-glass contract are unchanged. - The
freshnessjob skips a docs-only push (needs.changes.outputs.code == 'true',MMCA.ADC/.github/workflows/deploy.yml:819,MMCA.Store/.github/workflows/deploy.yml:778), as docost-guardand, on a push,supply-chain(ADC:772,:566; Store:732,:529); the deploy itself already skipped that path (ADC:1176, Store:1110). backend-test-gateis retired in both repos, ending the post-mergeCI.slnfre-test that ADC added as TD-20 on 2026-08-31 and Store adopted on 2026-09-03. Branch protection on both repos now enforces admins with strict up-to-date required checks, so the merged tree is the tree the pull request'sbuild-and-testalready tested (MMCA.ADC/.github/workflows/deploy.yml:1146-1148,MMCA.Store/.github/workflows/deploy.yml:1084-1086; the protection settings themselves live in GitHub, not in any file). The deploy condition therefore toleratesskippedfrome2e-gatealone in Store and frome2e-gateandai-eval-gatein ADC, andai-eval-gatenow also requires a diff that touches the scoring code (MMCA.ADC/.github/workflows/deploy.yml:511). Thedeploy.needscounts are 8 in ADC and 7 in Store.- The success-only lookup used by the DR and load gates no longer lists runs with a server-side
status=successfilter: like the job-reading modes it pages an unfiltered listing and keeps the runs that completed with conclusionsuccessclient-side (MMCA.Common/.github/actions/freshness-gate/action.yml:12-15,152-167). The pass/fail decision on a correct listing is unchanged. cross-service-tests.ymlno longer carries an advisory job in either repo (theapphost-smokejob is removed; the jobs areshould-run,cross-serviceandservicebus-emulator-smoke), so the advisory-job half of the broker gate's job-read rationale has no live example; the skip-if-unchanged half still applies.- MMCA.Common's workflow inventory in the Adoption paragraph is three files; its two Claude workflows are deleted.
- Decision, Adoption, Rationale and Trade-offs are re-anchored onto the current
deploy.ymland action lines.
Re-verified against current source after the v1.233.0 release. The gates, windows, deploy.needs
counts (8 in ADC, 7 in Store, MMCA.ADC/.github/workflows/deploy.yml:1145,
MMCA.Store/.github/workflows/deploy.yml:1083) and the break-glass contract are unchanged; the
counts and anchors quoted in the earlier dated Status sentences and Revision sections are
historical and are left as written.
- Rationale no longer says "one or two Actions API reads inside a five-minute job": a gate reads at
most five listing pages, once per required job in the per-job mode the cross-browser gate uses
(
MMCA.ADC/.github/workflows/deploy.yml:927,MMCA.Store/.github/workflows/deploy.yml:877), and adds one jobs read per scanned run per required job (MMCA.Common/.github/actions/freshness-gate/action.yml:164,190,193,236-239), and thefreshnessjob timeout is 5 minutes in ADC and 10 in Store (MMCA.ADC/.github/workflows/deploy.yml:821,MMCA.Store/.github/workflows/deploy.yml:777). - Decision now notes that both
CONTRIBUTING.mdfiles still list the gates by step id as though each were a job (MMCA.ADC/CONTRIBUTING.md:42-44,MMCA.Store/CONTRIBUTING.md:42-43) and list "the freshness gates" among the push-only jobs (MMCA.ADC/CONTRIBUTING.md:111,MMCA.Store/CONTRIBUTING.md:118), while the job that runs isfreshness. - Anchors re-verified against current source: the weeknight broker cron is
MMCA.ADC/.github/workflows/cross-service-tests.yml:32(Store:35unchanged).
Revision (2026-10-21)
Re-verified against current source. The four gates, their windows (8, 35, 5, 10), scan
bounds (25 for the broker gate; 40 in ADC and 50 in Store for the cross-browser gate), job timeouts
(5 minutes in ADC, 10 in Store), the deploy.needs counts (8 in ADC, 7 in Store) and the
break-glass contract are unchanged. Two descriptions moved.
- Both
CONTRIBUTING.mdfiles now name the singlefreshnessjob and all four of its steps (MMCA.ADC/CONTRIBUTING.md:42-45,MMCA.Store/CONTRIBUTING.md:42-45), and the "do not require the push-only jobs" sentence listsfreshness(MMCA.ADC/CONTRIBUTING.md:112,MMCA.Store/CONTRIBUTING.md:120). The Decision no longer says the enumerations are incomplete or that the files treat the gates as jobs. - The broker-step comments are restated. Neither mentions an advisory job; both count a completed
run of any conclusion "so a cancelled-but-proven run still counts"
(
MMCA.ADC/.github/workflows/deploy.yml:888-889,MMCA.Store/.github/workflows/deploy.yml:830). They differ in two places: ADC's leading "Shared composite action (ADR-038)." sentence (MMCA.ADC/.github/workflows/deploy.yml:888) and Store's trailing clause "and a cancelled run can still hold a genuine proof" (MMCA.Store/.github/workflows/deploy.yml:833). - Anchors re-verified against current source:
deploy.needsADC:1160, Store:1098; deploy condition ADC1187-1198(code check:1191), Store1121-1131(:1125); PR-tested-tree comments ADC1161-1163, Store1099-1101; break-glass-exits-success comments ADC1181-1182, Store1115-1117;freshnessjob ADC832-839(if:834, timeout:836), Store776-783(timeout:779,if:780); step name/id,!cancelled(),uses,window-days,required-jobsblocks and skip inputs as cited in the Decision; e2e-gateifADC:818, Store:763, with the untested-skip comments at ADC813-815and Store759-761;ai-eval-gateifADC:525; Docker note ADC880-881, Store824-826; chromium-only note ADC908-911, Store850-852; TD-02/TD-17 comments ADC:878,1166, Store:822; cross-browser alternation ADC914-915,927-929and continue-on-error919-921, Store alternation852,861-862; Store 7+1 boundary870-873and "sit red for weeks"853-854; ADC "roughly 40 minutes"910-911(all in the respective.github/workflows/deploy.yml). First-party MMCA.Common actions remain referenced at@main(ADC:495,609,624plus the four freshness steps; Store:512,559,573plus the four freshness steps) while third-party actions stay SHA-pinned (ADC:79, Store:88).
Revision (2026-10-26)
Re-verified against current source. The four gates, their windows (8, 35, 5, 10), scan
bounds (25 for the broker gate; 40 in ADC and 50 in Store for the cross-browser gate), job timeouts
(5 minutes in ADC, 10 in Store), the deploy.needs counts (8 in ADC, 7 in Store), the deploy
condition, the broker-step comment wording and the break-glass contract are unchanged. Only anchors
moved: every deploy.yml line from ai-eval-gate down sits one line later in both apps, and ADC's
e2e.yml schedule and continue-on-error lines sit five lines later. The break-glass inputs (ADC
13-20, Store 26-33), the schedule files, the cross-service-tests.yml job lines, both
CONTRIBUTING.md files and the freshness-gate action anchors are unchanged.
- Anchors re-verified against current source:
deploy.needsADC:1161, Store:1099; deploy condition ADC1188-1199(code check:1192), Store1122-1132(:1126); PR-tested-tree comments ADC1162-1164, Store1100-1102; break-glass-exits-success comments ADC1182-1183, Store1116-1118;freshnessjob ADC833-840(if:835, timeout:837), Store777-784(timeout:780,if:781); step name/id ADC846-847,865-866,886-887,925-926, Store791-792,809-810,828-829,859-860;!cancelled()ADC867,888,927, Store811,830,861;usesADC851,870,893,933, Store795,814,835,867;window-daysADC855,874,899,938, Store799,818,841,875; brokerrequired-jobsblock ADC901-905, Store843-847; cross-browser block ADC940-944, Store877-881; skip inputs ADC857-858,876-877,906-907,945-946, Store801-802,820-821,848-849,882-883; e2e-gateifADC:819, Store:764, with the untested-skip comments at ADC814-816and Store760-762;ai-eval-gateifADC:526; Docker note ADC881-882, Store825-827; chromium-only note ADC909-912, Store851-853; TD-02/TD-17 comments ADC:879,1167, Store:823; broker-step comments ADC889-892, Store831-834; cross-browser alternation ADC915-916,928-930and continue-on-error920-922, Store alternation853,862-863; Store 7+1 boundary871-874and "sit red for weeks"854-855; ADC "roughly 40 minutes"911-912(all in the respective.github/workflows/deploy.yml). The ADC non-chromium crons areMMCA.ADC/.github/workflows/e2e.yml:54-55and theircontinue-on-errorisMMCA.ADC/.github/workflows/e2e.yml:152(Store:46-47and:149unchanged).
Revision (2026-10-28)
Re-verified against current source. The four gates, their windows (8, 35, 5, 10), scan
bounds (25 for the broker gate; 40 in ADC and 50 in Store for the cross-browser gate), job timeouts
(5 minutes in ADC, 10 in Store), the deploy.needs counts (8 in ADC, 7 in Store), the deploy
condition, every deploy.yml, e2e.yml, cross-service-tests.yml and freshness-gate action
anchor, and the break-glass contract are unchanged. Only the CONTRIBUTING.md anchors for the
"do not require the push-only jobs" sentence moved; its wording, which still lists freshness, is
unchanged.
- Anchors re-verified against current source: the push-only sentence is
MMCA.ADC/CONTRIBUTING.md:115-116(was112-113) andMMCA.Store/CONTRIBUTING.md:122-123(was120-121); the earlier passage naming thefreshnessjob and its four steps stays atMMCA.ADC/CONTRIBUTING.md:42-45andMMCA.Store/CONTRIBUTING.md:42-45.
Related
ADR-009 (states the recovery objectives and requires that a restore be drilled and recorded; this record decides that a deploy is blocked on how recently that drill, and the capacity and broker proofs beside it, actually happened), ADR-057 (the sibling CI-gate record, which constrains migration shape and is explicitly a pull-request merge gate rather than a deploy gate).