2026-08-10 23:35:23 +08:00
|
|
|
/**
|
|
|
|
|
* 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/proposed/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'
|
2026-08-11 00:51:02 +08:00
|
|
|
import { attempt, run } from './process.ts'
|
|
|
|
|
import { packedIdentity, readPublishOrder } from './tarball.ts'
|
2026-08-10 23:35:23 +08:00
|
|
|
|
|
|
|
|
/** 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-<base64>` 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 {
|
2026-08-11 00:51:02 +08:00
|
|
|
const result = attempt('npm', ['view', `${name}@${version}`, 'dist.integrity', '--json'])
|
2026-08-10 23:35:23 +08:00
|
|
|
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 <dsh|vendor> --from <packed directory>')
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const family = releaseFamily(values.family)
|
|
|
|
|
const directory = resolve(process.cwd(), values.from)
|
|
|
|
|
|
|
|
|
|
let published = 0
|
|
|
|
|
let skipped = 0
|
2026-08-11 00:51:02 +08:00
|
|
|
for (const filename of readPublishOrder(directory)) {
|
2026-08-10 23:35:23 +08:00
|
|
|
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
|
|
|
|
|
}
|
2026-08-11 00:51:02 +08:00
|
|
|
// A prerelease version never takes the latest dist-tag.
|
|
|
|
|
const tagArgs = version.includes('-') ? ['--tag', 'next'] : []
|
|
|
|
|
run('npm', ['publish', tarball, '--access', ACCESS, ...tagArgs])
|
2026-08-10 23:35:23 +08:00
|
|
|
published += 1
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
console.log(`release publish: family ${family.id}, ${String(published)} published, ${String(skipped)} already present`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main()
|