The three release sequences shipped with publishConfig.access: restricted, so nothing in the @deepseek-ai scope was installable from outside the organization. A restricted dependency is what actually blocks a public consumer: every harness package declares the vendored framework as a peerDependency, and dsh-sandbox-local declares the Landlock entry as a dependency. Those two sequences therefore go public first — the nine vendor/* packages and the three native/landlock-run packages — while the dsh family stays restricted until its own sequence is opened deliberately. No public package requires a restricted one in this arrangement. Access is now per sequence, so no publish path can pass --access: one flag cannot express two levels and would override the manifest that owns the fact. publish.ts stops passing it, matching the native workflow, and check-workspace-constraints holds each manifest to its own sequence's level, which is what stops the scope from drifting one package at a time. Harness consumers reference the Landlock entry as workspace:^ instead of workspace:*, so a published harness package accepts the entry's patch and minor releases. The entry keeps workspace:* for its platform packages, where the binary must match the entry version exactly. Two rationales that named a private registry no longer describe the vendored sequence; they now state the durable reason, which is that the verification must not depend on the registry already carrying matching versions.
103 lines
4.1 KiB
TypeScript
103 lines
4.1 KiB
TypeScript
/**
|
|
* 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'
|
|
|
|
/** 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 {
|
|
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 <dsh|vendor> --from <packed directory>')
|
|
}
|
|
|
|
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'] : []
|
|
// No --access: the sequences do not share one access level, so a
|
|
// command-line flag could not serve both and would override the manifest
|
|
// that does. Each packed manifest decides, and
|
|
// check-workspace-constraints holds every manifest to its sequence's level.
|
|
run('npm', ['publish', tarball, ...tagArgs])
|
|
published += 1
|
|
}
|
|
|
|
console.log(`release publish: family ${family.id}, ${String(published)} published, ${String(skipped)} already present`)
|
|
}
|
|
|
|
if (isEntry(import.meta.url)) main()
|