/** * Publish one packed release family from the tarballs the pack step produced. * * Publication is decided per package against the registry, never from a list of * "what this release includes": 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 * ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)). * * Skipping on identical integrity is what makes re-running the publish step over * the same artifact safe. */ import { createHash } from 'node:crypto' import { readFileSync } from 'node:fs' import { join, resolve } from 'node:path' import { parseArgs } from 'node:util' import { releaseFamily } from './families.ts' import { attempt, isEntry, run } from './process.ts' import { packedIdentity, readPublishOrder } from './tarball.ts' /** npm access level for every package this repository publishes. */ const ACCESS = 'restricted' /** What the registry knows about one version. */ type RegistryState = | { readonly kind: 'absent' } | { readonly kind: 'present'; readonly integrity: string } /** * The subresource integrity string npm records for a tarball. * @param tarball - absolute tarball path. * @returns A `sha512-` string. */ function integrityOf(tarball: string): string { return `sha512-${createHash('sha512').update(readFileSync(tarball)).digest('base64')}` } /** * Ask the registry whether a version exists, and with what integrity. * @param name - package name. * @param version - package version. * @returns The registry state for that version. */ function registryState(name: string, version: string): RegistryState { const result = attempt('npm', ['view', `${name}@${version}`, 'dist.integrity', '--json']) 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: unknown = 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 the family named by `--family` from the directory named by `--from`. */ function main(): void { const { values } = parseArgs({ options: { family: { type: 'string' }, from: { type: 'string' } }, allowPositionals: false, }) if (values.family === undefined || values.from === undefined) { throw new Error('usage: publish.ts --family --from ') } const family = releaseFamily(values.family) const directory = resolve(process.cwd(), values.from) let published = 0 let skipped = 0 for (const filename of readPublishOrder(directory)) { const tarball = join(directory, 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(`release publish: ${name}@${version} already published, skipping`) skipped += 1 continue } // A prerelease version never takes the latest dist-tag. const tagArgs = version.includes('-') ? ['--tag', 'next'] : [] run('npm', ['publish', tarball, '--access', ACCESS, ...tagArgs]) published += 1 } console.log(`release publish: family ${family.id}, ${String(published)} published, ${String(skipped)} already present`) } if (isEntry(import.meta.url)) main()