Files
imccyu d9dcf5a484 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

83 lines
3.2 KiB
TypeScript

/**
* Process helpers shared by the release scripts: the release steps drive `git`,
* `pnpm`, `npm`, and `tar`, and each needs one of three failure behaviours.
*/
import { spawnSync } from 'node:child_process'
import { realpathSync } from 'node:fs'
import { fileURLToPath } from 'node:url'
/** Where and with what environment a release step runs a command. */
export interface RunOptions {
/** Working directory; defaults to the current one. */
readonly cwd?: string
/** Child environment; defaults to this process's. */
readonly env?: NodeJS.ProcessEnv
}
/** What a command produced, for a caller that decides what a failure means. */
export interface CommandResult {
/** Exit status, or null when a signal ended the process. */
readonly status: number | null
/** Captured standard output. */
readonly stdout: string
/** Captured standard error. */
readonly stderr: string
}
/**
* Run a command and capture its output without judging the exit status.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns The exit status and captured streams.
*/
export function attempt(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, encoding: 'utf8' })
if (result.error !== undefined) throw result.error
return { status: result.status, stdout: result.stdout, stderr: result.stderr }
}
/**
* Run a command, capture its standard output, and fail on a non-zero exit.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
* @returns The trimmed standard output.
*/
export function capture(command: string, args: readonly string[], options: RunOptions = {}): string {
const result = attempt(command, args, options)
if (result.status !== 0) {
throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}:\n${result.stdout}\n${result.stderr}`)
}
return result.stdout.trim()
}
/**
* Run a command with inherited streams, so its progress reaches the log, and
* fail on a non-zero exit.
* @param command - executable name.
* @param args - command arguments.
* @param options - working directory and environment.
*/
export function run(command: string, args: readonly string[], options: RunOptions = {}): void {
const result = spawnSync(command, [...args], { cwd: options.cwd, env: options.env, stdio: 'inherit' })
if (result.error !== undefined) throw result.error
if (result.status !== 0) throw new Error(`${command} ${args.join(' ')} exited with ${String(result.status)}`)
}
/**
* Whether this module is the process entry point.
*
* The release scripts are both commands and modules: a test imports their pure
* logic, and importing a module runs its body, so an unguarded `main()` would
* run the wrong command with the wrong arguments.
* @param moduleUrl - the caller's `import.meta.url`.
* @returns True when Node started this module.
*/
export function isEntry(moduleUrl: string): boolean {
const invoked = process.argv[1]
if (invoked === undefined) return false
return realpathSync(invoked) === realpathSync(fileURLToPath(moduleUrl))
}