feat(release): add release family metadata, pack, verify, and publish
A release family owns its member discovery, version baseline, tag naming, and
packed-payload rule; the dsh family shares one version across packages/ and
apps/, while every vendor/ package keeps its own version line. Publish order is
topological over runtime dependencies so no package reaches the registry before
one it depends on.
pack packs the whole family into one directory and records the upload order;
publish decides per package against the registry, skipping a version whose
published tarball has the same integrity and failing when it differs, which is
what makes re-running publish over one artifact safe.
The vendored packages keep upstream's payload: their manifests export ./src/*,
so the harness rule that rejects sources and declaration maps would publish an
export map pointing at absent files.
2026-08-10 23:35:23 +08:00
|
|
|
/**
|
|
|
|
|
* Pack one release family's whole publish set into a single directory, in
|
|
|
|
|
* publish order, and record that order for the publish step.
|
|
|
|
|
*
|
|
|
|
|
* The pack step is the release boundary: it runs without credentials, produces
|
|
|
|
|
* every tarball from one commit, and hands the publish step exactly those bytes
|
|
|
|
|
* ([rationale](../../.agents/notes/proposed/process/2026-08-10-npm-release-sequences.md)).
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
|
|
|
|
|
import { join, resolve } from 'node:path'
|
|
|
|
|
import { parseArgs } from 'node:util'
|
|
|
|
|
import { releaseFamily, tarballName, type ReleaseFamily, type ReleaseMember } from './families.ts'
|
2026-08-11 00:51:02 +08:00
|
|
|
import { run } from './process.ts'
|
|
|
|
|
import { PUBLISH_ORDER_FILE, tarballFiles } from './tarball.ts'
|
feat(release): add release family metadata, pack, verify, and publish
A release family owns its member discovery, version baseline, tag naming, and
packed-payload rule; the dsh family shares one version across packages/ and
apps/, while every vendor/ package keeps its own version line. Publish order is
topological over runtime dependencies so no package reaches the registry before
one it depends on.
pack packs the whole family into one directory and records the upload order;
publish decides per package against the registry, skipping a version whose
published tarball has the same integrity and failing when it differs, which is
what makes re-running publish over one artifact safe.
The vendored packages keep upstream's payload: their manifests export ./src/*,
so the harness rule that rejects sources and declaration maps would publish an
export map pointing at absent files.
2026-08-10 23:35:23 +08:00
|
|
|
|
|
|
|
|
/** Where pack output lands when `--out` is omitted. */
|
|
|
|
|
const DEFAULT_OUTPUT = 'dist/npm'
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Pack one member and check what its tarball carries.
|
|
|
|
|
* @param family - the release family being packed.
|
|
|
|
|
* @param member - the member to pack.
|
|
|
|
|
* @param destination - absolute output directory.
|
|
|
|
|
* @returns The tarball filename.
|
|
|
|
|
*/
|
|
|
|
|
function packMember(family: ReleaseFamily, member: ReleaseMember, destination: string): string {
|
|
|
|
|
run('pnpm', ['--dir', member.directory, 'pack', '--pack-destination', destination])
|
|
|
|
|
|
|
|
|
|
const filename = tarballName(member)
|
|
|
|
|
const tarball = join(destination, filename)
|
|
|
|
|
if (!existsSync(tarball)) throw new Error(`${member.name} produced no tarball at ${tarball}`)
|
|
|
|
|
family.validatePayload(member, tarballFiles(tarball))
|
|
|
|
|
return filename
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Pack the family named by `--family` into `--out`. */
|
|
|
|
|
function main(): void {
|
|
|
|
|
const { values } = parseArgs({
|
|
|
|
|
options: { family: { type: 'string' }, out: { type: 'string' } },
|
|
|
|
|
allowPositionals: false,
|
|
|
|
|
})
|
|
|
|
|
if (values.family === undefined) throw new Error('usage: pack.ts --family <dsh|vendor> [--out dist/npm]')
|
|
|
|
|
|
|
|
|
|
const family = releaseFamily(values.family)
|
|
|
|
|
const root = process.cwd()
|
|
|
|
|
const destination = resolve(root, values.out ?? DEFAULT_OUTPUT)
|
|
|
|
|
const members = family.publishOrder(family.members(root))
|
|
|
|
|
family.verifyVersions(members)
|
|
|
|
|
|
|
|
|
|
rmSync(destination, { recursive: true, force: true })
|
|
|
|
|
mkdirSync(destination, { recursive: true })
|
|
|
|
|
|
|
|
|
|
const order: string[] = []
|
|
|
|
|
for (const member of members) order.push(packMember(family, member, destination))
|
|
|
|
|
writeFileSync(join(destination, PUBLISH_ORDER_FILE), `${order.join('\n')}\n`)
|
|
|
|
|
|
|
|
|
|
console.log(`release pack: family ${family.id}, ${String(order.length)} tarball(s) in ${values.out ?? DEFAULT_OUTPUT}`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
main()
|