2026-08-11 00:26:26 +08:00
|
|
|
/**
|
2026-08-11 00:51:02 +08:00
|
|
|
* Install packed tarballs into a throwaway consumer outside the repository and
|
|
|
|
|
* drive the installed executable with plain Node.
|
2026-08-11 00:26:26 +08:00
|
|
|
*
|
2026-08-11 00:51:02 +08:00
|
|
|
* Every tarball the installed tree needs comes from `--from`, so the only
|
|
|
|
|
* registry traffic is for external dependencies. That matters beyond hermetic
|
|
|
|
|
* verification: the harness packages declare the vendored framework as a peer,
|
2026-08-13 13:55:05 +08:00
|
|
|
* those packages live in another release sequence, and this job must not depend
|
|
|
|
|
* on the registry already carrying versions that match — one pull request may
|
|
|
|
|
* bump both families before either publishes — so a dsh verification passes the
|
2026-08-11 00:51:02 +08:00
|
|
|
* vendored family's pack output too, while publishing only its own
|
fix(release): close the review findings on the release sequences
The root manifest carries the dsh family version. bump writes it with the
members, because the workspace constraint requires them to match, and that
constraint now accepts a prerelease segment: without both, release:dsh 0.0.2
left the root behind and 0.0.1-rc.1 could satisfy neither check.
The Landlock workflow no longer passes --access public, which overrode the
restricted publishConfig this repository just adopted for those packages.
Vendored change detection reads build inputs when a package publishes build
output, and vendor/cordis publishes the src its export map already pointed at:
its lib/ is untracked, so a real source edit read as 'nothing changed' and the
next publish would fail on a version whose bytes moved. The next version also
takes the last published version as its baseline, so a re-sync that restores a
lower upstream version cannot recompute a version already on the registry, and
bump confirms the registry carries what the newest tag names.
Tag prefixes are constructed rather than recovered from a full tag, which a
hyphenated version defeated. Pack runs group per ref so concurrent pull requests
stop displacing each other, the publish job carries the global group, and the
unused id-token permission is gone.
Every release script sits behind an entry guard, which is what lets the pure
judgements carry tests: tag naming, publish order and cycle reporting, version
arithmetic, payload policy, and the change judgement.
The Agent Note moves to implemented and states what shipped: one probe command,
the registry confirmation that now exists, and byte reproducibility recorded as
assumed rather than measured.
2026-08-11 01:26:36 +08:00
|
|
|
* ([rationale](../../.agents/notes/implemented/process/2026-08-10-npm-release-sequences.md)).
|
2026-08-11 00:51:02 +08:00
|
|
|
*
|
|
|
|
|
* What this proves is that `files` selected a complete payload and that the
|
|
|
|
|
* published dependency ranges resolve. A workspace link or a stale `lib/` in the
|
|
|
|
|
* checkout cannot stand in for a missing file here.
|
2026-08-11 00:26:26 +08:00
|
|
|
*/
|
|
|
|
|
|
2026-08-11 01:48:56 +08:00
|
|
|
import { mkdtempSync, readdirSync, rmSync, writeFileSync } from 'node:fs'
|
2026-08-11 00:26:26 +08:00
|
|
|
import { tmpdir } from 'node:os'
|
|
|
|
|
import { join, resolve } from 'node:path'
|
|
|
|
|
import { pathToFileURL } from 'node:url'
|
|
|
|
|
import { parseArgs } from 'node:util'
|
2026-08-11 00:51:02 +08:00
|
|
|
import { releaseFamily } from './families.ts'
|
fix(release): close the review findings on the release sequences
The root manifest carries the dsh family version. bump writes it with the
members, because the workspace constraint requires them to match, and that
constraint now accepts a prerelease segment: without both, release:dsh 0.0.2
left the root behind and 0.0.1-rc.1 could satisfy neither check.
The Landlock workflow no longer passes --access public, which overrode the
restricted publishConfig this repository just adopted for those packages.
Vendored change detection reads build inputs when a package publishes build
output, and vendor/cordis publishes the src its export map already pointed at:
its lib/ is untracked, so a real source edit read as 'nothing changed' and the
next publish would fail on a version whose bytes moved. The next version also
takes the last published version as its baseline, so a re-sync that restores a
lower upstream version cannot recompute a version already on the registry, and
bump confirms the registry carries what the newest tag names.
Tag prefixes are constructed rather than recovered from a full tag, which a
hyphenated version defeated. Pack runs group per ref so concurrent pull requests
stop displacing each other, the publish job carries the global group, and the
unused id-token permission is gone.
Every release script sits behind an entry guard, which is what lets the pure
judgements carry tests: tag naming, publish order and cycle reporting, version
arithmetic, payload policy, and the change judgement.
The Agent Note moves to implemented and states what shipped: one probe command,
the registry confirmation that now exists, and byte reproducibility recorded as
assumed rather than measured.
2026-08-11 01:26:36 +08:00
|
|
|
import { capture, isEntry } from './process.ts'
|
2026-08-11 01:48:56 +08:00
|
|
|
import { packedIdentity } from './tarball.ts'
|
2026-08-11 00:26:26 +08:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Environment for the installed artifact: no host Node hooks, no host DeepSeek
|
|
|
|
|
* Harness home, and no ambient npm user agent that would confuse npm.
|
|
|
|
|
* @param consumerRoot - the throwaway consumer directory.
|
|
|
|
|
* @returns The child environment.
|
|
|
|
|
*/
|
|
|
|
|
function consumerEnvironment(consumerRoot: string): NodeJS.ProcessEnv {
|
|
|
|
|
const environment = { ...process.env }
|
|
|
|
|
delete environment.npm_config_user_agent
|
|
|
|
|
delete environment.NPM_CONFIG_USER_AGENT
|
|
|
|
|
delete environment.NODE_OPTIONS
|
|
|
|
|
delete environment.NODE_PATH
|
|
|
|
|
environment.DSH_HOME = resolve(consumerRoot, '.dsh')
|
|
|
|
|
environment.DSH_AGENTS_HOME = resolve(consumerRoot, '.agents')
|
|
|
|
|
environment.DSH_TELEMETRY_DISABLED = '1'
|
|
|
|
|
return environment
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2026-08-11 00:51:02 +08:00
|
|
|
* Every packed tarball in the given directories, as `file:` dependency entries.
|
2026-08-11 01:48:56 +08:00
|
|
|
*
|
|
|
|
|
* The directories are read by their contents rather than a pack order file: a
|
|
|
|
|
* directory here can hold tarballs packed only to satisfy a cross-sequence
|
|
|
|
|
* dependency, which no release order describes.
|
|
|
|
|
* @param directories - absolute directories holding packed tarballs.
|
2026-08-11 00:51:02 +08:00
|
|
|
* @returns Package name to tarball file URL, and the version each carries.
|
2026-08-11 00:26:26 +08:00
|
|
|
*/
|
2026-08-11 00:51:02 +08:00
|
|
|
function packedDependencies(directories: readonly string[]): Map<string, { url: string; version: string }> {
|
|
|
|
|
const dependencies = new Map<string, { url: string; version: string }>()
|
|
|
|
|
for (const directory of directories) {
|
2026-08-11 01:48:56 +08:00
|
|
|
const tarballs = readdirSync(directory).filter(name => name.endsWith('.tgz')).sort()
|
|
|
|
|
if (tarballs.length === 0) throw new Error(`${directory} holds no packed tarball`)
|
|
|
|
|
for (const filename of tarballs) {
|
2026-08-11 00:51:02 +08:00
|
|
|
const tarball = join(directory, filename)
|
|
|
|
|
const { name, version } = packedIdentity(tarball)
|
|
|
|
|
dependencies.set(name, { url: pathToFileURL(tarball).href, version })
|
|
|
|
|
}
|
2026-08-11 00:26:26 +08:00
|
|
|
}
|
2026-08-11 00:51:02 +08:00
|
|
|
return dependencies
|
2026-08-11 00:26:26 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-11 00:51:02 +08:00
|
|
|
/** Install every tarball under `--from` and drive the `--family` entry. */
|
2026-08-11 00:26:26 +08:00
|
|
|
function main(): void {
|
|
|
|
|
const { values } = parseArgs({
|
2026-08-11 00:51:02 +08:00
|
|
|
options: { family: { type: 'string' }, from: { type: 'string', multiple: true } },
|
2026-08-11 00:26:26 +08:00
|
|
|
allowPositionals: false,
|
|
|
|
|
})
|
2026-08-11 00:51:02 +08:00
|
|
|
if (values.family === undefined || values.from === undefined || values.from.length === 0) {
|
|
|
|
|
throw new Error('usage: verify-packed-install.ts --family <dsh|vendor> --from <packed directory> [--from ...]')
|
2026-08-11 00:26:26 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const family = releaseFamily(values.family)
|
|
|
|
|
const entry = family.installedEntry
|
|
|
|
|
if (entry === undefined) {
|
|
|
|
|
console.log(`release verify-packed-install: family ${family.id} publishes no executable, nothing to drive`)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 00:51:02 +08:00
|
|
|
const root = process.cwd()
|
|
|
|
|
const packed = packedDependencies(values.from.map(directory => resolve(root, directory)))
|
|
|
|
|
const expected = packed.get(entry.packageName)
|
|
|
|
|
if (expected === undefined) throw new Error(`${entry.packageName} is not among the packed tarballs`)
|
|
|
|
|
|
2026-08-11 00:26:26 +08:00
|
|
|
const consumerRoot = mkdtempSync(join(tmpdir(), `dsh-packed-${family.id}-`))
|
|
|
|
|
try {
|
|
|
|
|
writeFileSync(join(consumerRoot, 'package.json'), `${JSON.stringify({
|
|
|
|
|
name: `dsh-packed-install-${family.id}`,
|
|
|
|
|
version: '0.0.0',
|
|
|
|
|
private: true,
|
2026-08-11 00:51:02 +08:00
|
|
|
dependencies: Object.fromEntries([...packed].map(([name, entryPacked]) => [name, entryPacked.url])),
|
2026-08-11 00:26:26 +08:00
|
|
|
}, null, 2)}\n`)
|
|
|
|
|
|
|
|
|
|
const environment = consumerEnvironment(consumerRoot)
|
2026-08-11 00:51:02 +08:00
|
|
|
console.log(`release verify-packed-install: installing ${String(packed.size)} tarball(s) into ${consumerRoot}`)
|
2026-08-11 01:48:56 +08:00
|
|
|
// Optional dependencies are omitted: the Landlock platform packages behind
|
|
|
|
|
// them need a musl toolchain and one build per architecture, and a consumer
|
|
|
|
|
// that cannot install them must still start — which is what optional means
|
|
|
|
|
// here. Their entry package is a plain dependency of dsh-sandbox-local, so
|
|
|
|
|
// its tarball is supplied through --from.
|
2026-08-11 01:39:01 +08:00
|
|
|
capture('npm', ['install', '--no-audit', '--no-fund', '--package-lock=false', '--omit=optional'],
|
|
|
|
|
{ cwd: consumerRoot, env: environment })
|
2026-08-11 00:26:26 +08:00
|
|
|
|
|
|
|
|
const bin = join(consumerRoot, 'node_modules', ...entry.packageName.split('/'), entry.binPath)
|
2026-08-11 00:51:02 +08:00
|
|
|
const version = capture(process.execPath, [bin, '--version'], { cwd: consumerRoot, env: environment })
|
|
|
|
|
if (version !== expected.version) {
|
|
|
|
|
throw new Error(`installed ${entry.packageName} --version reported ${JSON.stringify(version)}, expected ${expected.version}`)
|
2026-08-11 00:26:26 +08:00
|
|
|
}
|
|
|
|
|
console.log(`release verify-packed-install: installed ${entry.packageName} reports ${version}`)
|
|
|
|
|
} finally {
|
|
|
|
|
rmSync(consumerRoot, { recursive: true, force: true })
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
fix(release): close the review findings on the release sequences
The root manifest carries the dsh family version. bump writes it with the
members, because the workspace constraint requires them to match, and that
constraint now accepts a prerelease segment: without both, release:dsh 0.0.2
left the root behind and 0.0.1-rc.1 could satisfy neither check.
The Landlock workflow no longer passes --access public, which overrode the
restricted publishConfig this repository just adopted for those packages.
Vendored change detection reads build inputs when a package publishes build
output, and vendor/cordis publishes the src its export map already pointed at:
its lib/ is untracked, so a real source edit read as 'nothing changed' and the
next publish would fail on a version whose bytes moved. The next version also
takes the last published version as its baseline, so a re-sync that restores a
lower upstream version cannot recompute a version already on the registry, and
bump confirms the registry carries what the newest tag names.
Tag prefixes are constructed rather than recovered from a full tag, which a
hyphenated version defeated. Pack runs group per ref so concurrent pull requests
stop displacing each other, the publish job carries the global group, and the
unused id-token permission is gone.
Every release script sits behind an entry guard, which is what lets the pure
judgements carry tests: tag naming, publish order and cycle reporting, version
arithmetic, payload policy, and the change judgement.
The Agent Note moves to implemented and states what shipped: one probe command,
the registry confirmation that now exists, and byte reproducibility recorded as
assumed rather than measured.
2026-08-11 01:26:36 +08:00
|
|
|
if (isEntry(import.meta.url)) main()
|