Skip to content

fix(cd): a broken pipe mid-push no longer throws away the whole release (#2810) - #2828

Merged
rbuergi merged 1 commit into
mainfrom
fix/cd-container-push-bounded-retry
Aug 30, 2026
Merged

rbuergi merged 1 commit into
mainfrom
fix/cd-container-push-bounded-retry

Conversation

@rbuergi

@rbuergi rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Delivery has been blocked for hours. Across the last 20 CD runs there is not one non-skipped Promote job, so every self-updating install is still on the previous image. There are two independent causes; this PR fixes the one nobody owns.

The measurement

Run 33328213655 (target 84bf70b17):

18:48:05  linux-arm64 pushed cleanly
18:52:58  [Registry] Encountered a HttpRequestException Unknown with message "Broken pipe".
          Pausing before retry.
18:53:07  CONTAINER1013 → CONTAINER1001: Failed to upload blob using
          PATCH https://meshweaver.azurecr.io/v2/memex-portal-ai/blobs/uploads/…

Same pair on run 33322756606 (target 8cbf69ac9) at 16:49Z. The SDK's own retry did not recover either.

And ci.6846 promoted successfully at 18:14Z — between the two failures. So this is intermittent: not credentials, not quota, not a purge policy. The registry is Premium SKU and healthy throughout.

Publication is all-or-nothing by design — promote applies the real tags only once every leg succeeded, so an install can never pull a half-shipped set. The cost of that guarantee is that one broken pipe on one architecture throws away the entire release.

The change

Up to three attempts on the portal-ai push, with an az acr login between them (a token can expire during a multi-arch push whose legs take minutes each).

🚨 Not a skip-trapdoor, and the distinction is the whole point:

  • a compile error, a missing project or a rejected credential fails identically on all three attempts and the step ends RED, naming the last attempt's output;
  • only a connection dying mid-upload gets a second chance;
  • three consecutive failures emit an ::error:: saying it is no longer a transient and wants investigating rather than another retry, pointing at CD failed on main: run 33322756606 #2810.

It mirrors the idiom already in node-repo-{compile-check,gate,publish-bake}.yml, which carry the same "Bounded retry, and deliberately NOT a skip-trapdoor" comment for their registry logins — added after three ACR logins failed on 2026-08-28 with the registry healthy throughout.

Verified

YAML parses yaml.safe_load
the failure path exits 1 proved, not assumed — the loop shape simulated with an always-failing command ends ::error:: + exit 1
no guard's parse broken MeshWeaver.Documentation.Test 123/123, which is every guard that reads this workflow: ReleaseDeliveryChainGuard, MainRunsAreNeverCancelledGuard, PlatformBakeLaneGuard, PlatformReleaseNotifyGuard, WorkflowJobIfContextGuard

The other blocker, so they are not conflated

A CS0419 ambiguous cref in MeshWeaver.Plugins (MeshStoreAccess.cs:72 and two more) — core CD builds the portal hosts from plugins-repo, and #2772's new ObserveCompletion overload made three bare <see cref>s ambiguous there. Fixed by MeshWeaver.Plugins#993.

Different cause, different commits: both ACR failures are on commits that predate the overload. If Promote is still skipped after both land, read which error the portal-ai job carries — CS0419 means the cref fix did not take; CONTAINER1001 means the connection died three times running and this is no longer transient.

🤖 Generated with Claude Code

…se (#2810)

Delivery was blocked for hours by an ACR blob upload that lost its
connection, and because publication is all-or-nothing one leg took the whole
release with it.

Measured, run 33328213655 (target 84bf70b):

  18:48:05  linux-arm64 pushed cleanly
  18:52:58  [Registry] Encountered a HttpRequestException Unknown with
            message "Broken pipe". Pausing before retry.
  18:53:07  CONTAINER1013 → CONTAINER1001: Failed to upload blob using
            PATCH https://meshweaver.azurecr.io/v2/memex-portal-ai/blobs/uploads/…

Same pair on run 33322756606 (target 8cbf69a) at 16:49Z. The SDK's own
retry did not recover either. Between them, ci.6846 promoted successfully at
18:14Z — so the fault is INTERMITTENT: not credentials, not quota, not a
purge policy, and the registry is Premium SKU and healthy throughout.

The portal-ai push now makes up to 3 attempts, with an `az acr login`
between them (a token can expire during a multi-arch push whose legs take
minutes each).

🚨 NOT a skip-trapdoor, and the distinction is the point:
  - a compile error, a missing project or a rejected credential fails
    identically on all three attempts and the step ends RED, naming the last
    attempt's output;
  - only a connection dying mid-upload gets a second chance;
  - three consecutive failures emit an ::error:: saying it is no longer a
    transient and wants investigating rather than another retry, and points
    at #2810.
It mirrors the idiom already in node-repo-{compile-check,gate,publish-bake}.yml,
which carry the same "Bounded retry, and deliberately NOT a skip-trapdoor"
comment for their registry logins.

Verified:
  - the YAML parses (yaml.safe_load);
  - the failure path was PROVED to exit 1 rather than assumed — the loop
    shape was simulated with an always-failing command and ends
    `::error::` + exit 1;
  - MeshWeaver.Documentation.Test 123/123 green, which is every guard that
    reads this workflow (ReleaseDeliveryChainGuard, MainRunsAreNeverCancelled-
    Guard, PlatformBakeLaneGuard, PlatformReleaseNotifyGuard,
    WorkflowJobIfContextGuard), so the step's rewrite does not break a parse.

This is one of TWO blockers on delivery. The other is a CS0419 ambiguous
cref in MeshWeaver.Plugins (core CD builds the portal hosts from
plugins-repo), fixed by MeshWeaver.Plugins#993 — separate cause, separate
commits: both ACR failures are on commits that PREDATE #2772's overload.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings August 30, 2026 19:12

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

This PR improves resilience of the main continuous delivery pipeline so that intermittent Azure Container Registry connection failures (e.g., “Broken pipe” mid-blob upload) don’t discard an otherwise good multi-arch publish, while preserving the all-or-nothing promotion guarantee.

Changes:

  • Adds a bounded (3-attempt) retry loop around the memex-portal-ai multi-arch container publish in main-cd.yml.
  • Adds a corresponding “What’s New” entry documenting the operational impact and rationale for the bounded retry.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/MeshWeaver.Documentation/Data/WhatsNew/2026-08-30-one-broken-pipe-no-longer-costs-a-whole-release.md New release note describing the intermittent ACR failure mode and the bounded-retry mitigation.
.github/workflows/main-cd.yml Wraps the portal-ai publish in a 3-attempt retry with re-login between attempts, keeping the job red on persistent failures.

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

Comment on lines +897 to +900
echo "attempt $attempt failed — re-authenticating and retrying in 30s"
az acr login --name meshweaver || true
sleep 30
fi
@rbuergi

rbuergi commented Aug 30, 2026

Copy link
Copy Markdown
Contributor Author

🚨 Correcting my own justification: CONTAINER1013 is a WRAPPER, and the two failures are not the same

I grouped these as "the same CONTAINER1013/1001 pair, twice". That grouping is wrong, and I verified it against the logs rather than accepting the correction:

run commit image inner cause
33322756606 8cbf69a 16:35 migration Connection refused (meshweaver.azurecr.io) — no CONTAINER1001 at all
33328213655 84bf70b 18:53 portal-ai CONTAINER1001 … PATCH … 'RequestedRangeNotSatisfiable' (HTTP 416)

CONTAINER1013 is the SDK's outer "failed to push" wrapper; it wraps different inner causes. I compared wrappers. That is the same same-symptom-⇒-same-cause shortcut this repo has paid for three times today — and the countermeasure took thirty seconds: open the error string per run.

What that means for this PR — it is narrower than I claimed

Still exactly right for the connection-refused class. Nothing was uploaded; the connection never opened. A bounded retry with a re-login is the correct answer, and it is the same fault that put the retry idiom into node-repo-*.yml in the first place.

Unproven for the 416, and I will not claim otherwise. The upload session's _state token decodes to {"Offset":0,"StartedAt":"…18:47:50Z"} and the PATCH was rejected at 18:53:07 — 5m17s in, still at offset 0. A 416 on blobs/uploads/ means the registry's offset no longer agrees with the client's. So:

  • this retry does give it a fresh upload session each attempt, which the SDK's in-place resume does not — that is a real improvement over dying on an invalidated session;
  • but if the mechanism is a single long-lived upload of a very large layer outliving the registry's session window, then attempts 2 and 3 take the same five minutes and hit the same wall, and this costs 3× the CI time to fail anyway.

The step's final message already says the right thing for that case — three consecutive failures emit "no longer a transient … wants investigating rather than another retry" — so the PR degrades honestly. But the body oversold one cause, and I have corrected it.

Keeping it, with the scope stated

I am not withdrawing this: one of the two blockers is squarely in scope, the other gets a materially better shot than the SDK's resume, and the failure mode is loud rather than silent. What it is not is a fix for the 416 mechanism — that wants its own investigation (layer size, chunked upload, or ACR's actual session window, none of which is measured), and belongs on #2827 rather than being quietly absorbed here.

(The wrapper-vs-inner-cause finding and the _state decode are the reviewing session's; the verification and this correction are mine.)

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 0)

790 tests  ±0   786 ✅ ±0   4m 21s ⏱️ -2s
  7 suites ±0     4 💤 ±0 
  7 files   ±0     0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 1)

947 tests  ±0   947 ✅ ±0   4m 57s ⏱️ ±0s
  7 suites ±0     0 💤 ±0 
  7 files   ±0     0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 4)

1 085 tests  ±0   1 085 ✅ ±0   5m 30s ⏱️ ±0s
    7 suites ±0       0 💤 ±0 
    7 files   ±0       0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 3)

1 176 tests  ±0   1 176 ✅ ±0   8m 17s ⏱️ +25s
    7 suites ±0       0 💤 ±0 
    7 files   ±0       0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 5)

    8 files  ±0      8 suites  ±0   5m 57s ⏱️ -3s
1 531 tests ±0  1 530 ✅ ±0  1 💤 ±0  0 ❌ ±0 
2 221 runs  +1  2 220 ✅ +1  1 💤 ±0  0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results (shard 2)

3 016 tests  ±0   2 824 ✅ ±0   6m 23s ⏱️ +12s
    8 suites ±0     192 💤 ±0 
    8 files   ±0       0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

   44 files  ±0     44 suites  ±0   35m 26s ⏱️ +31s
8 545 tests ±0  8 348 ✅ ±0  197 💤 ±0  0 ❌ ±0 
9 235 runs  +1  9 038 ✅ +1  197 💤 ±0  0 ❌ ±0 

Results for commit 378bcc3. ± Comparison against base commit c3ad2da.

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