Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 38 additions & 3 deletions docs/site/scripts/build-archives.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,12 @@ import { tmpdir } from 'node:os';
import { dirname, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import {
constants as zlibConstants,
crc32,
Comment thread
jeremi marked this conversation as resolved.
deflateRawSync,
gunzipSync,
} from 'node:zlib';
import { applyArchiveSeo } from './apply-archive-seo.mjs';
import {
archiveOutputDirectory,
Expand Down Expand Up @@ -170,6 +176,28 @@ async function replaceFile(path, contents) {
}
}

const deterministicGzipHeader = Buffer.from([
0x1f, 0x8b, 0x08, 0x00,
0x00, 0x00, 0x00, 0x00,
0x02, 0xff,
]);

function deterministicGzip(contents) {
// zlib records the host OS in its gzip header, so gzipSync emits different
// bytes on macOS and Linux. Build the framing explicitly with no optional
// fields, zero mtime, maximum-compression XFL, and the unknown-OS marker.
const compressed = deflateRawSync(contents, {
level: 9,
memLevel: 8,
strategy: zlibConstants.Z_DEFAULT_STRATEGY,
windowBits: 15,
});
const trailer = Buffer.alloc(8);
trailer.writeUInt32LE(crc32(contents), 0);
trailer.writeUInt32LE(contents.length >>> 0, 4);
return Buffer.concat([deterministicGzipHeader, compressed, trailer]);
}

export async function normalizePagefindGzipMetadata(outputRoot) {
const pagefindRoot = resolve(outputRoot, 'pagefind');
let pagefindInfo;
Expand Down Expand Up @@ -204,9 +232,16 @@ export async function normalizePagefindGzipMetadata(outputRoot) {
throw new Error(`generated Pagefind WebAssembly must use gzip framing: ${path}`);
}
files += 1;
if (contents.subarray(4, 8).some((byte) => byte !== 0)) {
const updated = Buffer.from(contents);
updated.fill(0, 4, 8);
let uncompressed;
try {
uncompressed = gunzipSync(contents);
} catch (error) {
throw new Error(`generated Pagefind WebAssembly must be valid gzip: ${path}`, {
cause: error,
});
}
const updated = deterministicGzip(uncompressed);
if (!contents.equals(updated)) {
await replaceFile(path, updated);
normalized += 1;
}
Expand Down
26 changes: 20 additions & 6 deletions docs/site/scripts/build-archives.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import { dirname, resolve } from 'node:path';
import test from 'node:test';
import { fileURLToPath } from 'node:url';
import { promisify } from 'node:util';
import { gzipSync } from 'node:zlib';
import { gunzipSync, gzipSync } from 'node:zlib';

import {
buildDocsetArchive,
Expand Down Expand Up @@ -65,23 +65,29 @@ test('archive snapshot reads one no-follow regular-file descriptor', async (t) =
assert.equal(await readOptionalRegularFile(resolve(root, 'missing.json')), null);
});

test('Pagefind gzip metadata normalizes to a stable archive tree', async (t) => {
test('Pagefind gzip streams normalize across platforms without changing content', async (t) => {
const root = await mkdtemp(resolve(tmpdir(), 'registry-docs-pagefind-gzip-'));
t.after(() => rm(root, { recursive: true, force: true }));
const left = resolve(root, 'left');
const right = resolve(root, 'right');
await mkdir(resolve(left, 'pagefind'), { recursive: true });
await mkdir(resolve(right, 'pagefind'), { recursive: true });

const compressed = gzipSync('architecture-independent WebAssembly');
const wasm = Buffer.from(
`\0asm\x01\0\0\0${'architecture-independent WebAssembly'.repeat(256)}`,
);
for (const name of ['wasm.en.pagefind', 'wasm.unknown.pagefind']) {
const leftContents = Buffer.from(compressed);
const rightContents = Buffer.from(compressed);
const leftContents = gzipSync(wasm, { level: 1 });
const rightContents = gzipSync(wasm, { level: 9 });
leftContents.writeUInt32LE(1_700_000_000, 4);
rightContents.writeUInt32LE(1_800_000_000, 4);
leftContents[9] = 0x03;
rightContents[9] = 0x13;
await writeFile(resolve(left, 'pagefind', name), leftContents);
await writeFile(resolve(right, 'pagefind', name), rightContents);
assert.notDeepEqual(leftContents, rightContents);
assert.deepEqual(gunzipSync(leftContents), wasm);
assert.deepEqual(gunzipSync(rightContents), wasm);
}

assert.deepEqual(
Expand All @@ -95,10 +101,18 @@ test('Pagefind gzip metadata normalizes to a stable archive tree', async (t) =>
for (const name of ['wasm.en.pagefind', 'wasm.unknown.pagefind']) {
const normalizedLeft = await readFile(resolve(left, 'pagefind', name));
const normalizedRight = await readFile(resolve(right, 'pagefind', name));
assert.deepEqual(normalizedLeft.subarray(4, 8), Buffer.alloc(4));
assert.deepEqual(
normalizedLeft.subarray(0, 10),
Buffer.from([0x1f, 0x8b, 0x08, 0x00, 0, 0, 0, 0, 0x02, 0xff]),
);
assert.deepEqual(normalizedLeft, normalizedRight);
assert.deepEqual(gunzipSync(normalizedLeft), wasm);
}
assert.equal(await treeDigest(left), await treeDigest(right));
assert.deepEqual(
await normalizePagefindGzipMetadata(left),
{ files: 2, normalized: 0 },
);
});

test('Pagefind metadata normalization rejects an unexpected WASM format', async (t) => {
Expand Down
23 changes: 23 additions & 0 deletions release/OPERATIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,12 @@ The command prints the exact candidate run ID and URL immediately after the
dispatch is correlated. `--wait-for-ci` waits only for protected-main `ci.yml`
at the exact source SHA, then refreshes protected `main` again immediately
before dispatch. `--wait` follows only that uniquely identified candidate run.
Both waits print only workflow state changes by default. If a protected
environment is waiting for approval, the command names the environment, links
the exact run, and prints a read-only command for inspecting the pending
deployment. An authorized reviewer must approve it through **Review
deployments** in that run. Add `--verbose-wait` to retain the raw `gh run watch`
display when detailed live job output is useful.
Omit either flag when another operator or monitor owns the corresponding wait.

The request is accepted only when `source_sha` is the exact protected-main
Expand Down Expand Up @@ -443,6 +449,23 @@ SHA-512 integrity and PyPI SHA-256 digest of every client package before
docs promotion. The public verifier is read-only and can be rerun
independently.

For an interrupted publication after the annotated tag exists, classify the
Comment thread
jeremi marked this conversation as resolved.
exact recovery state before retrying:

```sh
release/scripts/registry-release verify-recovery --tag v<version>
```

This command is read-only. For an absent release or a bound draft, it verifies
that the local annotated tag exactly matches `origin`, revalidates the original
candidate and its lifetime, and checks the draft's candidate binding. It then
prints the exact protected-main `release.yml` retry command. The workflow owns
the fail-closed reconciliation of draft assets, OCI digests, npm packages, and
PyPI wheels immediately before each write. If the release is already
published, the command runs `verify-public`, reports the release complete, and
does not recommend a retry. It never approves environments or dispatches a
workflow, and it adds no release gate.

## Failure handling

| Failure state | Response |
Expand Down
Loading