Skip to content

fix(cdn): publish build artifacts in an order a reader can trust - #178

Merged
ABB65 merged 1 commit into
mainfrom
fix/cdn-artifact-publish-order
Jul 29, 2026
Merged

fix(cdn): publish build artifacts in an order a reader can trust#178
ABB65 merged 1 commit into
mainfrom
fix/cdn-artifact-publish-order

Conversation

@ABB65

@ABB65 ABB65 commented Jul 29, 2026

Copy link
Copy Markdown
Member

Follow-up to #177 — the two ordering issues found while tracing that incident. Both left windows where the CDN advertised something it had not published.

1. _manifest.json was published first

It is the content version pointer: consumers key freshness off its commitSha. Publishing it at step 4 meant that for the entire rest of the build it named a commit whose content, bundles and media manifest were still uploading — objects go up one at a time.

Measured on staging (R2 LastModified, full rebuild):

_manifest.json     11:01:19.718   <-- version pointer flips here
content/…          11:01:30 → 11:03:03
_bundle/en.json    11:03:04.167
_bundle/tr.json    11:03:04.562   <-- content it points at lands here

105 seconds on a full rebuild, ~24 s on a selective one. A consumer reading in that window pins the new commitSha to pre-build bodies — and if it caches per commit, never re-reads them.

This is the invariant #153 protected ("the manifest must not outrun the bundle"), but that fix covered a build that skipped the bundle. It did not cover every build that simply had not written it yet.

The manifest now goes last, after every artifact it describes. Side benefit: a build that dies midway leaves the old manifest pointing at the old, complete content instead of a half-written snapshot.

2. _media_manifest.json was deleted and re-uploaded on every full rebuild

It was written at step 8, after the step-7 stale-object sweep — so it was never in uploadedPaths when the sweep ran. Every full rebuild deleted it, then re-uploaded it ~1 s later.

That window is small but not harmless: the delivery SDK throws on any non-2xx, and MediaAccessor caches the media manifest for the lifetime of the instance. A consumer booting inside the window stays broken until it is replaced.

It now runs before the sweep. The inverse still holds — a project with no media assets uploads nothing, the path stays out of uploadedPaths, and the sweep still collects a stale manifest.

New order

4. model index + defs
5. content (content/, meta/, documents/)
6. locale bundles
7. media manifest      ← moved before the sweep
8. _manifest.json      ← moved last
9. stale-object sweep
10. purge

Tests

Three new cases, verified to fail against the previous ordering:

  • publishes _manifest.json after every artifact it describes → old: expected '_bundle/en.json' to be '_manifest.json'
  • never deletes _media_manifest.json during a full rebuild that has media → old: expected [...] to not include '_media_manifest.json'
  • sweeps a stale _media_manifest.json when the project has no media assets (guards the inverse — passes both ways by design)

pnpm test 1098/1098 · pnpm lint 0 errors · nuxt typecheck clean.

Note

Neither of these caused the incident in #177 — that was the missing cdn.build_complete on push-triggered builds, now fixed and verified on staging (webhook build → delivery, HTTP 200). These are the latent races found alongside it.

Two ordering bugs in executeCDNBuild left windows where the CDN advertised
something it had not published.

`_manifest.json` went up FIRST. It is the content version pointer — consumers
key freshness off its commitSha — so for the entire rest of the build it named a
commit whose content, bundles and media manifest were still uploading. Objects
go up one at a time: measured on staging, a full rebuild wrote the manifest at
11:01:19 and the last bundle at 11:03:04, a 105-second window (~24s on a
selective build). A consumer reading in there pinned the new commitSha to
pre-build bodies and, if it caches per commit, never re-read them. This is the
invariant #153 protected — the manifest must not outrun the bundle — but that
fix covered a build that SKIPPED the bundle, not every build that simply had
not written it yet. The manifest now goes last, after every artifact it
describes, so a build that dies midway leaves the old manifest pointing at the
old, complete content instead of a half-written snapshot.

`_media_manifest.json` was written AFTER the stale-object sweep, so it was never
in uploadedPaths when the sweep ran: every full rebuild deleted it and
re-uploaded it a moment later. The delivery SDK throws on any non-2xx and caches
the media manifest for the lifetime of the instance, so a consumer booting
inside that window stays broken until it is replaced. It now runs before the
sweep, which also keeps the inverse honest — a project with no media assets
uploads nothing, the path stays out of uploadedPaths, and the sweep still
collects a stale manifest.

Both are covered by tests that fail against the previous ordering.
@ABB65
ABB65 merged commit 60c3f11 into main Jul 29, 2026
2 checks passed
@ABB65
ABB65 deleted the fix/cdn-artifact-publish-order branch July 29, 2026 19:54
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.

1 participant