Repository navigation
A container leg that pushed nothing reported success - #1031
Merged
Merged
Conversation
CD run 31322066110 failed the nodetype-bake job with
error : Cannot create image index because provided images are invalid.
Items must have 'Config', 'Manifest', 'ManifestMediaType' and
'ManifestDigest' metadata.
which reads as a defect in the image index or in Memex.NodeType.Bake. It is
neither. The linux-arm64 leg sat 30.2 s (16:01:03.9 -> 16:01:34.1) between its
publish directory and a bare warning "The operation was canceled.", and that
warning is the whole story:
* Microsoft.NET.Build.Containers builds its registry client as
`new SocketsHttpHandler { ConnectTimeout = TimeSpan.FromSeconds(30) }`, so a
stalled TCP/TLS connect to the base registry (mcr.microsoft.com, fetched
before "Building image" is ever logged) surfaces as TaskCanceledException.
* CreateNewImage.Execute catches TaskCanceledException /
OperationCanceledException, calls Log.LogWarningFromException — which drops
the inner TimeoutException naming ConnectTimeout — and returns
`!Log.HasLoggedErrors`, i.e. SUCCESS, having produced no image.
* _PublishSingleContainer then emits its GeneratedContainer item
unconditionally, with every metadata value empty, and the failure lands
~100 ms later in CreateImageIndex blaming the index.
So a network stall is reported as an image-index defect, with neither host nor
timeout anywhere in the log. Nothing in the bake project or in the job split
(#979) is involved: the same tree published memex-bake fine in the run before
and the run after, 26 of 27 image-building CD runs are green, and the identical
publish in the migration job on that same commit succeeded.
Refuse the success that produced no artifact. A guard in the root
Directory.Build.props fails the leg where it actually failed (MW1026), naming
the RID and both registries, so the next occurrence is one line to attribute
instead of an hour of log archaeology. It matters most on the SINGLE-RID path,
where there is no CreateImageIndex downstream at all and a swallowed
cancellation means a publish that reports success while pushing nothing.
Verified against the SDK targets: the guard runs once per RID leg after
_PublishSingleContainer, sees the four Generated* properties populated on a
healthy leg (inert), and when it does fire it fails the publish with exit 1
before CreateImageIndex runs.
The stall itself is upstream infrastructure and is deliberately NOT papered
over here: no retry around the publish, no dropped architecture, no
continue-on-error. The workflow comment records the other consequence, which is
a policy question rather than a bug — each image job pushes independently, so
4f0c35c shipped memex-portal-ai:3.0.0-rc1.ci.2470 with no matching
memex-bake:ci.2470, a portal increment the NodeType bake gate never certified.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…dowed targets file Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR hardens the CI/CD container publishing pipeline by detecting when a per-RID container publish “succeeds” while producing no image artifacts (due to an SDK-swallowed cancellation), and by documenting the correct triage interpretation in the CD workflow.
Changes:
- Add an MSBuild guard after
_PublishSingleContainerto fail fast (MW1026) when the generated container manifest/config/digest metadata is missing. - Document in
main-cd.ymlthat the downstream “invalid images”/image-index error is typically a symptom of an earlier cancelled/stalled registry connect, not an index defect.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| Directory.Build.props | Adds an MSBuild target that fails the build immediately when a container publish leg produced no image metadata, preventing misleading downstream failures. |
| .github/workflows/main-cd.yml | Adds workflow comments capturing the triage narrative so future incidents are diagnosed from the correct log signal (the 30s gap + cancellation warning). |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Contributor
Issue #1026 was raised as "no deployable image was published", and the alerting did fire. What nothing checked was WHICH image. Each image job pushes independently, so one dead leg leaves main with a PARTIAL release, and the registry proves it: 4f0c35c has memex-portal-ai 3.0.0-rc1.ci.2470, 4f0c35c <- shipped memex-bake (neither tag) <- missing i.e. a portal increment the NodeType bake gate never certified, while the self-updater picks the newest version tag off the registry regardless. A red job says "CD failed"; it does not say that memex-bake is the hole, and nothing was asserting that a main commit ends up with the full set at all. So make the run's last word a registry check. verify-images runs `if: always()` after every leg — pass, fail or cancelled — and asserts that all five images carry this commit's short-SHA tag, and that the four .NET ones are linux amd64+arm64 image INDEXES rather than a surviving single arch. Every repository is reported, so one miss cannot hide the next, and it is wired into alert-on-failure's needs so a missing image opens the ci-failure issue even in the case a per-job red cannot see: a leg that reports success while pushing nothing. Verified against the real registry in both directions: the check names memex-bake:4f0c35c as missing and exits 1 on the failed commit, and passes clean on 86578a4, purely from ACR state. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Review: the guard reports the failing leg by $(ContainerRuntimeIdentifier), which is not guaranteed to be set on the single-RID path — the very path where this guard is the ONLY thing that catches a swallowed cancellation, and where a message reading "RuntimeIdentifier ''" would be worth nothing. In practice ComputeContainerBaseImage defaults it from $(RuntimeIdentifier) before _PublishSingleContainer runs, and multi-arch legs are handed it explicitly — verified on all three invocation shapes (multi-arch legs, an implicit multi-arch publish, and a true single-RID publish with -p:RuntimeIdentifier=linux-x64: all three name the leg). But the message is the entire point of the target, so fall back to $(RuntimeIdentifier) and then to a visible <unspecified> rather than depend on an SDK default holding. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This was referenced Aug 9, 2026
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.
Fixes #1026
CD has since gone green on its own (
31324251944, all five images for86578a4), so this is anintermittent failure — which is the worse kind, because when it fires it silently skips image
publication. This PR does not declare it transient and walk away: it establishes the exact mechanism,
rules out the repo-side hypotheses with evidence, and makes both the build failure and the missing
image impossible to mistake for anything else.
The exact mechanism
Timeline from the bake job log (
linux-x64had already pushed at 16:01:00.9):Memex.NodeType.Bake -> …/linux-arm64/publish/Building image …-linux-arm64is never loggedwarning : The operation was canceled.error : Cannot create image index …Every step of the chain is in the SDK:
CreateNewImagefetches the base image manifest (mcr.microsoft.com/dotnet/aspnet:10.0)before it logs
Building image …. That message is absent, so the 30 s was spent in that fetch,not in the index.
Microsoft.NET.Build.Containersbuilds its registry client asnew SocketsHttpHandler { UseCookies = false, ConnectTimeout = TimeSpan.FromSeconds(30) }(
DefaultRegistryAPI). A stalled connect surfaces asTaskCanceledException("The operation was canceled.")with an innerTimeoutException("A connection could not be established within the configured ConnectTimeout.")— dotnet/runtime #47484,
#78070. 30.2 s is that timeout, to the tenth.
CreateNewImage.Execute()catches it, callsLog.LogWarningFromException— which logs onlyex.Message, dropping the innerTimeoutExceptionthat names the host and the timeout — andreturns
!Log.HasLoggedErrors, i.e. success, having produced no image._PublishSingleContaineremits itsGeneratedContaineritem unconditionally, every metadatavalue empty.
CreateImageIndex.ParseImagesrejects the empty metadata → the misleading error.So the index never sees a race: it sees an item a leg was allowed to publish while failing. The
answer to "why intermittently" is simply "only when a registry connect stalls ≥ 30 s".
Hypotheses checked, with evidence
A multi-arch race (the
ProduceReferenceAssemblyprior art, PR #552 / CS0009). Ruled out, two ways:Memex.NodeType.Bake— it lives in the rootDirectory.Build.propsunder
'$(ContainerRuntimeIdentifiers)' != '', not per-csproj, so it reaches the whole closure.Queried directly for a library in that closure:
compiled and wrote their publish directories —
…/linux-arm64/publish/is right there in the log.The failure is strictly after the build, in a registry call.
The #979 job split changing concurrency. Ruled out — it moved in the safe direction. GitHub gives
every job its own fresh VM, so after the split
nodetype-bakeshares no runner, no NuGet cache and nooutput path with any other image job; before the split it was a second step in the migration job,
i.e. strictly more sharing. Within the job the two RID legs run in parallel either way (the SDK's
BuildInParalleldefault) — #979 did not touch that. And the job had been green in every run since itwas split at 00:08 that day, including 11 minutes before the failure.
Frequency. 1 failure in 27 image-building CD runs (last 100 completed runs: 26 success, 73 skipped,
1 failure). The runs immediately before (
cb25f07) and after (d2db7dc, a superset tree containing4f0c35cfc) both publishedmemex-bake. On the same commit, themigrationjob did the identicalmulti-arch publish against the identical base image and pushed both legs 12 s apart.
Conclusion: an upstream stall, amplified by an SDK defect that reports it as success. There is no
repo-side change that prevents a connect from stalling — and per the ground rules this PR adds no retry,
drops no architecture, disables no index and sets no
continue-on-error, because none of those wouldhave produced an image.
What this PR changes — two detection layers
1. A leg that produced no image no longer reports success (root
Directory.Build.props,error MW1026). It fails where the failure actually happened, naming the RID and both registries andpointing at the warning above it, instead of letting an empty item travel two tasks downstream to be
blamed on the image index.
It matters most on the single-RID path, where there is no
CreateImageIndexat all: today aswallowed cancellation there means
dotnet publishexits 0 having pushed nothing.2.
verify-images— assert the image, never the tick (main-cd.yml). Runsif: always()afterevery leg and asserts all five images carry this commit's short-SHA tag, and that the four .NET ones are
linux
amd64+arm64indexes rather than a surviving single arch. Every repository is reported, so onemiss cannot hide the next, and it is in
alert-on-failure'sneeds, so a missing image opens theci-failureissue even when every publish leg reported success.This closes the gap the issue actually left open. The alerting did fire for #1026 — but "CD failed"
never said which image was missing, and nothing was checking that a main commit ends up with the
full set:
ci.2470is a portal increment the NodeType bake gate never certified, and the self-updater picks thenewest version tag regardless.
Verification
verify-imageslogic run against the real registry, both directions: on4f0c35cit namesmemex-bake:4f0c35c is MISSING from ACRand exits 1; on86578a4it passes clean — reproducingCD failed on main: run 31322066110 #1026 from ACR state alone.
_PublishSingleContainer, sees all fourGenerated*properties populated, stays silent; forced tofire →
error MW1026, exit 1,CreateImageIndexnever runs.Memex.NodeType.Bakepublish with CD's flags (-c Release --no-self-contained -t:PublishContainer -p:ContainerRuntimeIdentifiers="linux-x64;linux-arm64" -p:CIRun=true) completesgreen with the guard in place — both legs valid, index built, no MW1026.
Not decided here
Whether CD should refuse to ship a partial set (e.g. hold the portal tag until every leg lands) is a
delivery-policy call for the maintainer.
verify-imagesmakes the partial release loud and named; itdoes not change what gets published.
No What's New entry
Build/CI-only change with no user-visible behaviour.