Skip to content

fix(cli): zip the publish archive to the same bytes every time - #3358

Merged
miguel-heygen merged 1 commit into
mainfrom
fix-deterministic-publish-archive
Aug 19, 2026
Merged

fix(cli): zip the publish archive to the same bytes every time#3358
miguel-heygen merged 1 commit into
mainfrom
fix-deterministic-publish-archive

Conversation

@miguel-heygen

Copy link
Copy Markdown
Collaborator

createPublishArchive produced different bytes for identical content depending on when it ran.

Why

zipPublishFileMap calls archive.addFile(path, content) with no attr, and adm-zip 0.6.0 stamps _time = fromDate2DOS(new Date()) at entry construction (headers/entryHeader.js:37), writing it into both the local and central headers. A ZIP timestamp resolves to two seconds, so two builds match only when they land in the same bucket.

That makes the archive's digest a function of the clock rather than of its contents — backwards for something cloud render uploads and addresses by content.

How it surfaced

publishProject.test.ts:321 asserts two archives built back-to-back are byte-identical. Both sides are the same expression, so the only way it can fail is non-determinism. It has been failing rarely since July and clearing on every re-run, which is exactly the signature of a narrow timing window.

The fix

Entry times are pinned to a fixed value after the entries are added.

Built from local components on purpose: fromDate2DOS reads getFullYear, getMonth, getHours and friends, so a fixed instant would still encode differently per timezone. Verified identical bytes under three zones:

UTC                  1b09f6c6063a73d4
America/Los_Angeles  1b09f6c6063a73d4
Asia/Kolkata         1b09f6c6063a73d4

1980-01-01 is the earliest a DOS timestamp can represent (fromDate2DOS writes zeroes below 1980).

Test

The new test moves the clock across a two-second boundary, because that is what reproduces it — back-to-back builds land in the same bucket almost always, which is how it hid for months. Mutation-checked: removing the pin reproduces the original failure exactly (expected false to be true), deterministically rather than one run in dozens.

Scope

Archive bytes change once, on merge, for every input. That is the point — the same project now zips to one digest instead of a new one every two seconds. No test or caller pinned the previous value; publish.ts:166 and cloud/render.ts:605 are the two consumers and both take the buffer as-is.

CLI suite: 2783 passing.

adm-zip stamps every entry with `new Date()` as it is constructed, and a ZIP
timestamp resolves to two seconds — so archiving identical content twice gave
different bytes whenever the two runs landed either side of a boundary. The
archive's digest was a function of the clock rather than of its contents, which
is backwards for something `cloud render` uploads and addresses by content.

It surfaced as a CI flake: publishProject.test.ts asserts two archives built
back to back are byte-identical, and both sides are the same expression, so the
only way it can fail is non-determinism. The window is narrow, which is why it
survived since July and why re-running always cleared it.

Entry times are now fixed. Built from local components deliberately:
`fromDate2DOS` reads getFullYear/getMonth/getHours, so a fixed instant would
still encode differently per timezone — verified identical bytes under UTC,
America/Los_Angeles and Asia/Kolkata.

The new test moves the clock across a boundary, which is what reproduces it;
back-to-back builds land in the same bucket almost always, which is exactly how
it hid.
@miguel-heygen
miguel-heygen merged commit d464f60 into main Aug 19, 2026
48 checks passed
@miguel-heygen
miguel-heygen deleted the fix-deterministic-publish-archive branch August 19, 2026 22:25
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