mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-09-05 17:30:50 +00:00
fix(cli): zip the publish archive to the same bytes every time (#3358)
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.
This commit is contained in:
@@ -304,6 +304,33 @@ describe("createPublishArchive (U6 cloud-render regression guard)", () => {
|
||||
// `createPublishArchive` is exactly the thin composition of
|
||||
// `buildPublishFileMap` + `zipPublishFileMap` with no baking hook, and that
|
||||
// a local video asset's original bytes/HTML pass through unmodified.
|
||||
it("zips the same files to the same bytes across a two-second boundary", () => {
|
||||
// The flake this pins: adm-zip stamps each entry with `new Date()` as it is
|
||||
// constructed, and a ZIP timestamp resolves to two seconds — so two runs
|
||||
// either side of a boundary produced different bytes for identical content.
|
||||
// The byte-identity assertion below could only ever fail this way, and did,
|
||||
// rarely enough to survive since July.
|
||||
//
|
||||
// Moving the clock is what reproduces it: back-to-back builds land in the
|
||||
// same bucket almost always, which is exactly why it hid.
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
writeFileSync(join(dir, "index.html"), "<html></html>", "utf-8");
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:00.000Z"));
|
||||
const first = createPublishArchive(dir);
|
||||
vi.setSystemTime(new Date("2026-01-01T00:00:03.000Z"));
|
||||
const second = createPublishArchive(dir);
|
||||
expect(first.buffer.equals(second.buffer)).toBe(true);
|
||||
} finally {
|
||||
vi.useRealTimers();
|
||||
}
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
it("keeps a source video byte-identical and excludes proxies from the cloud-render archive", () => {
|
||||
const dir = makeProjectDir();
|
||||
try {
|
||||
|
||||
@@ -504,11 +504,32 @@ export function buildPublishFileMap(projectDir: string): Map<string, Buffer> {
|
||||
|
||||
/** Zip an in-memory archive file map (from `buildPublishFileMap`, optionally
|
||||
* transformed in between, e.g. by proxy baking) into the final archive buffer. */
|
||||
/**
|
||||
* Fixed entry timestamp, so the same files always zip to the same bytes.
|
||||
*
|
||||
* adm-zip stamps every entry with `new Date()` as it is constructed, and a ZIP
|
||||
* timestamp has two-second granularity — so archiving identical content twice
|
||||
* produced different bytes whenever the two runs landed either side of a
|
||||
* two-second boundary. That makes the archive's digest a function of the clock
|
||||
* rather than of its contents, which is the opposite of what a
|
||||
* content-addressed artifact needs.
|
||||
*
|
||||
* Built from local components on purpose: `fromDate2DOS` reads `getFullYear`,
|
||||
* `getMonth`, `getHours` and friends, so a fixed *instant* would still encode
|
||||
* differently in different timezones. Fixing the wall-clock reading is what
|
||||
* makes the bytes match across machines. 1980-01-01 is the earliest a DOS
|
||||
* timestamp can represent.
|
||||
*/
|
||||
const ARCHIVE_ENTRY_TIME = new Date(1980, 0, 1, 0, 0, 0, 0);
|
||||
|
||||
export function zipPublishFileMap(fileContents: Map<string, Buffer>): PublishArchiveResult {
|
||||
const archive = new AdmZip();
|
||||
for (const [filePath, content] of fileContents) {
|
||||
archive.addFile(filePath, content);
|
||||
}
|
||||
for (const entry of archive.getEntries()) {
|
||||
entry.header.time = ARCHIVE_ENTRY_TIME;
|
||||
}
|
||||
|
||||
return {
|
||||
buffer: archive.toBuffer(),
|
||||
|
||||
Reference in New Issue
Block a user