mirror of
https://github.com/deepseek-ai/deepseek-harness
synced 2026-08-15 21:04:50 +00:00
A landlock publication failed with `E409 Failed to save packument` on the second of three packages. The registry answers a write it could not commit that way, and publishing several packages back to back is what provokes it. Neither publish path could recover. The native sequence published from a shell loop of bare `npm publish` calls: no retry, and no way to resume, because the registry rejects a repeat of an existing version permanently — so a failure partway through left the release stuck. publish.ts skipped versions already present, which made a re-run safe, but had no retry either. Both paths now attempt a tarball up to four times, space writes at least two seconds apart, and back off 2s/4s/8s between attempts. Every retry re-reads the registry first, because a reported failure can answer a write that landed anyway: a version that now exists with this tarball's integrity counts as published rather than as one to place again. That same re-read is what turns a mid-run `E403 cannot publish over the previously published versions` into a skip when the bytes match, and leaves it a hard failure when they do not. The native sequence gets the registry comparison publish.ts already had, through its own script rather than shared code — the two sequences keep separate publication paths. Its publish job now checks out the repository, which the shell loop did not need. Verified against a scripted registry: a clean publish, one E409 then success, an E409 whose write landed anyway, E409 on every attempt (fails after four), and a version already present with matching integrity (publishes nothing).
166 lines
6.9 KiB
JavaScript
166 lines
6.9 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Publish the packed launcher family from the tarballs `pack-release.mjs`
|
|
* produced, in `publish-order.txt` order.
|
|
*
|
|
* What goes out is decided per package against the registry, never from the
|
|
* order file alone: a version the registry lacks is published, a version whose
|
|
* published tarball has the same integrity is skipped, and a version whose
|
|
* published tarball differs fails the run — that last case means the content
|
|
* changed without a version bump. Skipping on identical integrity is what makes
|
|
* re-running the publish step over the same artifact safe, which matters here
|
|
* because a partial publication used to leave no way forward: republishing an
|
|
* existing version fails permanently.
|
|
*
|
|
* Usage: `node scripts/publish-release.mjs [packed dir]`.
|
|
*/
|
|
|
|
import fs from 'node:fs';
|
|
import path from 'node:path';
|
|
import crypto from 'node:crypto';
|
|
import { spawnSync } from 'node:child_process';
|
|
import { setTimeout as sleep } from 'node:timers/promises';
|
|
import { root } from './repo.mjs';
|
|
|
|
/**
|
|
* Registry codes that answer a write which did not settle, rather than a
|
|
* rejection of what was sent. `E409 Failed to save packument` is the one this
|
|
* sequence actually hits: publishing the platform packages and the entry back
|
|
* to back can outrun the registry's own processing. A rejected payload (`E403`
|
|
* over an existing version, a malformed manifest) never clears on a retry.
|
|
*/
|
|
const TRANSIENT_PUBLISH_CODES = ['E409', 'E429', 'E500', 'E502', 'E503', 'E504', 'ETIMEDOUT', 'ECONNRESET', 'EAI_AGAIN'];
|
|
|
|
/** How many times one tarball's publish is attempted before the run fails. */
|
|
const PUBLISH_ATTEMPTS = 4;
|
|
|
|
/**
|
|
* Shortest gap between two publishes, and the first retry backoff. The registry
|
|
* needs a moment to commit a packument before the next write; back to back
|
|
* publishes are what produce `E409`.
|
|
*/
|
|
const PUBLISH_SPACING_MS = 2_000;
|
|
|
|
const destination = path.resolve(process.argv.slice(2).find((arg) => !arg.startsWith('--')) || path.join(root, 'dist', 'npm'));
|
|
|
|
/**
|
|
* @param {string} output Combined npm output.
|
|
* @returns {boolean} True when the registry reported a write it did not commit.
|
|
*/
|
|
function isTransientFailure(output) {
|
|
return TRANSIENT_PUBLISH_CODES.some((code) => output.includes(`code ${code}`));
|
|
}
|
|
|
|
/**
|
|
* @param {string} tarball Absolute tarball path.
|
|
* @returns {string} The `sha512-<base64>` integrity npm records for it.
|
|
*/
|
|
function integrityOf(tarball) {
|
|
return `sha512-${crypto.createHash('sha512').update(fs.readFileSync(tarball)).digest('base64')}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} tarball Absolute tarball path.
|
|
* @returns {{name: string, version: string}} What the packed manifest declares.
|
|
*/
|
|
function packedIdentity(tarball) {
|
|
const result = spawnSync('tar', ['-xOzf', tarball, 'package/package.json'], { encoding: 'utf8' });
|
|
if (result.status !== 0) throw new Error(`cannot read the manifest inside ${tarball}:\n${result.stderr}`);
|
|
const manifest = JSON.parse(result.stdout);
|
|
if (typeof manifest.name !== 'string' || typeof manifest.version !== 'string') {
|
|
throw new Error(`${tarball} manifest lacks name/version`);
|
|
}
|
|
return { name: manifest.name, version: manifest.version };
|
|
}
|
|
|
|
/**
|
|
* Ask the registry whether a version exists, and with what integrity.
|
|
* @param {string} name Package name.
|
|
* @param {string} version Package version.
|
|
* @returns {{kind: 'absent'} | {kind: 'present', integrity: string}} Registry state.
|
|
*/
|
|
function registryState(name, version) {
|
|
const result = spawnSync('npm', ['view', `${name}@${version}`, 'dist.integrity', '--json'], { encoding: 'utf8' });
|
|
if (result.status !== 0) {
|
|
const output = `${result.stdout}${result.stderr}`;
|
|
if (output.includes('E404') || output.includes('404 Not Found')) return { kind: 'absent' };
|
|
throw new Error(`npm view ${name}@${version} failed:\n${output}`);
|
|
}
|
|
const parsed = JSON.parse(result.stdout);
|
|
if (typeof parsed !== 'string' || parsed === '') {
|
|
throw new Error(`registry reported no dist.integrity for ${name}@${version}`);
|
|
}
|
|
return { kind: 'present', integrity: parsed };
|
|
}
|
|
|
|
/**
|
|
* Publish one tarball, retrying a registry write that did not settle.
|
|
*
|
|
* Every retry re-reads the registry first, because `E409` can answer a write
|
|
* that landed anyway: republishing a version that now exists fails permanently,
|
|
* so the same integrity appearing under the failed attempt counts as success.
|
|
* @param {string} tarball Absolute tarball path.
|
|
* @param {string} name Package name the tarball declares.
|
|
* @param {string} version Package version the tarball declares.
|
|
*/
|
|
async function publishTarball(tarball, name, version) {
|
|
// A prerelease version never takes the latest dist-tag.
|
|
const tagArgs = version.includes('-') ? ['--tag', 'next'] : [];
|
|
for (let tries = 1; tries <= PUBLISH_ATTEMPTS; tries += 1) {
|
|
// No --access: publishConfig.access in each manifest decides, and a
|
|
// command-line flag would override it.
|
|
const result = spawnSync('npm', ['publish', tarball, ...tagArgs], { encoding: 'utf8' });
|
|
const output = `${result.stdout}${result.stderr}`;
|
|
if (result.status === 0) return;
|
|
|
|
const settled = registryState(name, version);
|
|
if (settled.kind === 'present' && settled.integrity === integrityOf(tarball)) {
|
|
console.log(`landlock publish: ${name}@${version} landed despite a reported failure, continuing`);
|
|
return;
|
|
}
|
|
if (tries === PUBLISH_ATTEMPTS || !isTransientFailure(output)) {
|
|
throw new Error(`npm publish ${name}@${version} failed:\n${output}`);
|
|
}
|
|
const backoff = PUBLISH_SPACING_MS * 2 ** (tries - 1);
|
|
console.log(
|
|
`landlock publish: ${name}@${version} hit a transient registry failure`
|
|
+ ` (attempt ${tries} of ${PUBLISH_ATTEMPTS}), retrying in ${backoff}ms`,
|
|
);
|
|
await sleep(backoff);
|
|
}
|
|
}
|
|
|
|
const order = fs
|
|
.readFileSync(path.join(destination, 'publish-order.txt'), 'utf8')
|
|
.split('\n')
|
|
.filter((line) => line !== '');
|
|
|
|
let published = 0;
|
|
let skipped = 0;
|
|
for (const filename of order) {
|
|
const tarball = path.join(destination, filename);
|
|
const { name, version } = packedIdentity(tarball);
|
|
const state = registryState(name, version);
|
|
if (state.kind === 'present') {
|
|
const local = integrityOf(tarball);
|
|
if (state.integrity !== local) {
|
|
throw new Error(
|
|
`${name}@${version} is already published with different content`
|
|
+ `\n registry: ${state.integrity}\n packed: ${local}`
|
|
+ '\nBump the version, or investigate why the build is not reproducible.',
|
|
);
|
|
}
|
|
console.log(`landlock publish: ${name}@${version} already published, skipping`);
|
|
skipped += 1;
|
|
continue;
|
|
}
|
|
// Space out the writes: the gap belongs between publishes, so a run that only
|
|
// skips does not wait at all.
|
|
if (published > 0) await sleep(PUBLISH_SPACING_MS);
|
|
await publishTarball(tarball, name, version);
|
|
console.log(`landlock publish: ${name}@${version} published`);
|
|
published += 1;
|
|
}
|
|
|
|
console.log(`landlock publish: ${published} published, ${skipped} already present`);
|