Skip to content

fix(cd): a bake-only reconcile no longer blames promote for its own broken query - #2642

Merged
rbuergi merged 2 commits into
mainfrom
fix/cd-bake-version-recovery
Aug 29, 2026
Merged

rbuergi merged 2 commits into
mainfrom
fix/cd-bake-version-recovery

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 29, 2026 •

Copy link
Copy Markdown
Contributor

Core main CD was red on nine consecutive scheduled reconciles (2026-08-28 22:12Z → 08-29 05:26Z). The delivery itself was fine the whole time — what was broken is the reconcile's release-version read, and its error message sent readers at the wrong component.

What was actually failing

Every red run failed on one step, Resolve the release version this bake belongs to:

memex-portal-ai:aaf95af carries no version tag (tags on that digest:  ) — promote's
Phase C never armed a release for this sha … Fix promote; do not publish a marker
for an invented version.

Two things are wrong with that sentence.

1. The error text accuses a healthy component

Promote had armed the release. Run 33210930983 job 98987531545 pushed all three tags onto one digest:

memex-portal-ai:aaf95af   memex-portal-ai:main   memex-portal-ai:3.0.0-rc8.ci.6360
      → sha256:6c3abd508033db59865e8bedf68076bdb75b4c044e3fa1159c01eff98b2a1089

and the bake in that same run published, sealed and wrote _releases/3.0.0-rc8.ci.6360 → identity s35f21e69e34d028fa49c70d79455a93d. aaf95af was fully delivered at 21:23Z. Every later reconcile then told the reader that the one component doing its job was the one to fix.

An error that names a healthy component as the culprit is worse than a silent failure — it is a silent failure with directions attached. It cost nine hours in the wrong direction.

2. The swallow is the root cause; the query crash is only the trigger

tags=$(az acr manifest list-metadata --registry meshweaver --name memex-portal-ai \
         --query "[?contains(tags, '${SHORT_SHA}')].tags[]" -o tsv 2>/dev/null || true)

tags is null on an untagged manifest — the orphaned per-platform children an index push leaves behind. memex-portal-ai holds 2101 manifests, 16 of them untagged (dated 08-16 → 08-22). jmespath's contains() throws on null rather than returning false, and the throw aborts the whole query:

ERROR: Invalid jmespath query supplied for `--query`: In function contains(),
invalid type for value: None, expected one of: ['array', 'string'], received: "null"

So one orphan makes the read find nothing. Then 2>/dev/null || true converted "the query crashed with an invalid-jmespath error" into "there is no version tag" — and only because of that could the step reach the message blaming promote. Removing the swallow is the more important half of this fix; the tags && guard just stops the crash.

The fix (commit 62f4734)

  • [?tags && contains(tags, '${SHORT_SHA}')] — && short-circuits, so untagged rows never reach contains().
  • 2>/dev/null || true removed — an az failure now fails the step with az's own message, so the next reader debugs the query rather than promote.

Verified against live ACR, all three paths:

case result
aaf95af, bake-only release version 3.0.0-rc8.ci.6360 — what the nine red runs should have printed
a sha with no version tag exit 1, with the (now accurate) message
az itself fails exit 1, carrying az's own error text — no longer masquerading as a missing tag

Second, separate defect (commit 9cae035)

az acr repository show-tags returns an array of plain strings unless --detail is passed, so the gate's --query "[].name" matched nothing and newest_short was always empty. Every gate log has read Newest published set: <none> for as long as that step has existed.

This is latent, not the outage: it fails open (relevant stays true), so it never turned CD red — it only meant the image-irrelevant skip could never fire. The batching probe 60 lines above already passes --detail; this one did not. Kept as its own commit for that reason.

Behaviour this restores, worth knowing: a push whose entire diff since the newest published set is under clients/(react-native|voice-gateway)/ will now correctly skip the image build, which it never did before.

Not caused by aaf95afc3

aaf95afc3 (#2630, workflows-only) did not cause this. It is implicated only as the sha that happened to be main's tip when ACR returned HTTP 500 on two blob uploads (jobs 98980554453, 98980554462 in run 33209932873) and then sat still. That 500 was transient and the 21:04Z reconcile healed it.

The real precondition is main going quiet — the first reconcile that finds a complete image set and nothing to build is the first one to take the bake-only path at all. Tracked separately in #2643, because the next merge to main masks it — and it already did: origin/main moved to 5f067884e (#2634) mid-investigation.

🤖 Generated with Claude Code

rbuergi and others added 2 commits August 29, 2026 08:19
…roken query

The read a bake-only reconcile uses to recover the release version off the
promoted image has never once succeeded. It crashed, the crash was swallowed,
and the resulting error accused promote — which was working fine.

`az acr manifest list-metadata --query "[?contains(tags, '<sha>')]"` throws
`In function contains(), invalid type for value: None` on any manifest whose
`tags` is null — the orphaned per-platform children an index push leaves
behind (16 of memex-portal-ai's 2101 manifests). jmespath aborts the WHOLE
query on that throw, so one orphan makes the read unable to find any tag.

`2>/dev/null || true` then turned that crash into an empty tag list, and the
step reported "memex-portal-ai:<sha> carries no version tag ... promote's
Phase C never armed a release for this sha. Fix promote." Promote had armed
it: memex-portal-ai:aaf95af, :main and :3.0.0-rc8.ci.6360 all sit on digest
sha256:6c3abd508033db59865e8bedf68076bdb75b4c044e3fa1159c01eff98b2a1089.
Nine scheduled reconciles (2026-08-28 22:12Z through 08-29 05:26Z) died
pointing at the healthy half of CD.

The swallow is the root cause; the guard only stops the crash:

- `tags && contains(tags, ...)` short-circuits the untagged rows out.
- the stderr swallow is gone, so an az failure now fails the step with az's
  own message instead of masquerading as a missing tag.

Verified against live ACR: aaf95af resolves 3.0.0-rc8.ci.6360; a sha with no
version tag still exits 1 with the (now accurate) message; a failing az exits
1 carrying az's error text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing --detail)

`az acr repository show-tags` returns an array of plain STRINGS unless
`--detail` is passed, so `--query "[].name"` matched nothing and newest_short
was always empty. Every gate log has read `Newest published set: <none>` for
as long as this step has existed.

Latent, not the outage: the probe fails open (relevant stays true), so it
never turned CD red. It only meant the image-irrelevant skip below could never
fire, and CD rebuilt the full image set for clients-only changes it was
written to ignore. The batching probe 60 lines up already passes --detail;
this one did not.

Note the behaviour this restores: with the probe working, a push whose whole
diff since the newest published set is under clients/(react-native|voice-gateway)/
will now correctly skip the image build, which it never did before.

With --detail the probe returns aaf95af, as intended.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Fixes the main-cd.yml reconcile path so bake-only runs correctly recover the promoted release version from ACR tags (and fail with actionable az errors instead of silently misdiagnosing missing tags), and restores the “image-irrelevant changes” skip logic by making the show-tags query actually return tag objects.

Changes:

  • Add --detail to az acr repository show-tags so --query "[].name" works and newest_short is no longer always empty.
  • Harden the bake-only “resolve release version” ACR query against null tag sets (tags && contains(...)) and stop swallowing az stderr so real query failures fail loudly.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

490 tests   486 ✅  4m 20s ⏱️
  6 suites    4 💤
  6 files      0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

898 tests   898 ✅  5m 36s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

957 tests   957 ✅  5m 28s ⏱️
  6 suites    0 💤
  6 files      0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

1 020 tests   1 020 ✅  6m 25s ⏱️
    6 suites      0 💤
    6 files        0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

1 585 tests   1 584 ✅  7m 14s ⏱️
    8 suites      1 💤
    8 files        0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

   10 files     10 suites   6m 0s ⏱️
3 773 tests 3 581 ✅ 192 💤 0 ❌
4 405 runs  4 213 ✅ 192 💤 0 ❌

Results for commit 9cae035.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   42 files     42 suites   35m 5s ⏱️
8 723 tests 8 526 ✅ 197 💤 0 ❌
9 355 runs  9 158 ✅ 197 💤 0 ❌

Results for commit 9cae035.

@rbuergi
rbuergi merged commit ecef6db into main Aug 29, 2026
24 checks passed
@rbuergi
rbuergi deleted the fix/cd-bake-version-recovery branch August 29, 2026 06:35
rbuergi added a commit that referenced this pull request Aug 30, 2026
…ness writing to GitHub

## The gap

main-cd's `decide` step chooses between building an image set and re-asserting the
content bake (`bake_only`). The bake-only branch is reached only on a reconcile
that finds a COMPLETE image set — i.e. only when nobody is pushing. On a busy trunk
it never runs: its first execution was in production at 22:12 on a Friday, and it
failed nine times out of nine over 33 h, because every fix-verifying push took the
other branch and repaired the APPEARANCE of the problem.

#2642 fixed that specific defect and test-cd-steps.py executes THAT step. What
neither did is remove the shape, which is what this issue is about.

`decide` is a pure-shell step with `id:`, no `${{ }}`, and every input arriving via
`env:` — exactly the shape the harness already requires — so it is extracted, not
copied, and driven with the inputs the real step reads.

Four cases: the bake-only branch is taken on a complete set; it does not also
publish; it says WHY in the decision log; and — the one that keeps the other three
honest — an INCOMPLETE set on the same event still builds. If those two ever
collapse into one, delivery either stops or doubles.

Vacuity guard in the harness's own idiom: if the step stops writing `bake_only=true`
it dies rather than passing, the same way the release cases die if the step stops
calling `az acr manifest list-metadata`.

Mutation-checked: making the branch unreachable (`COMPLETE = "MUTATED"`) fails three
of the four; restored, all pass.

## 🚨 A safety device the harness was missing

Writing these surfaced something worse than the gap. `decide` posts a heal COMMENT
to the CD-failure issue on a reconcile attempt, and `run_step` inherits the caller's
environment — so on a machine with a live `gh` login, RUNNING THE TESTS POSTS REAL
COMMENTS TO REAL ISSUES. It did, three times, to #2810; I deleted them by hand.

`gh` is now stubbed on PATH beside `az` (records the call, answers nothing, so the
step's control flow is unchanged and its side effect is not), and GH_TOKEN /
GITHUB_TOKEN / GH_ENTERPRISE_TOKEN are blanked with GH_CONFIG_DIR pointed into the
temp dir. Either alone would do; both are there because one will still be present
after someone edits the other. Re-running the mutation created ZERO comments.

The harness also now supplies GITHUB_STEP_SUMMARY and GITHUB_REPOSITORY, which the
runner provides to every step and without which a `set -u` step dies for a reason
unrelated to the logic under test — which is how a harness gets disabled.

    python3 .github/scripts/test-cd-steps.py    all cases passed, 2 steps

Fixes #2643

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014FhP7Jg65q8cAATnFfeaTK
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants