2026-08-11 00:51:02 +08:00
|
|
|
/**
|
|
|
|
|
* 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'
|
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 { realpathSync } from 'node:fs'
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
2026-08-11 00:51:02 +08:00
|
|
|
|
|
|
|
|
/** Where and with what environment a release step runs a command. */
|
|
|
|
|
export interface RunOptions {
|
|
|
|
|
readonly cwd?: string
|
|
|
|
|
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
|
|
|
|
|
readonly stdout: string
|
|
|
|
|
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 }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-14 11:27:14 +08:00
|
|
|
/**
|
2026-08-19 17:55:06 +08:00
|
|
|
* Run a command, then echo and return its captured output. Output is buffered
|
|
|
|
|
* until exit and stdout precedes stderr.
|
2026-08-14 11:27:14 +08:00
|
|
|
* @param command - executable name.
|
|
|
|
|
* @param args - command arguments.
|
|
|
|
|
* @param options - working directory and environment.
|
|
|
|
|
* @returns The exit status and captured streams.
|
|
|
|
|
*/
|
fix(release): state what the echo helper does, and drop two dead claims
Three corrections from review, none of which change behaviour.
attemptStreaming promised output "as the command produces it", which
spawnSync cannot do: it returns only after the child exits, and the two
streams are echoed one after the other, so their interleaving is lost.
For an npm publish that is visible — notices go to stderr while the
`+ name@version` confirmation goes to stdout, so the confirmation prints
first. The helper is now attemptEchoed and its contract says buffered,
echoed after exit, stdout before stderr; live progress would need an
asynchronous spawn with data listeners.
The traversal comment claimed a node on the stack is a cycle only peer
edges can form, and that skipping it drops just that edge. The cycle does
carry a peer edge, because the install edges were proved acyclic a moment
earlier, but the back edge that reaches the stacked node need not be the
peer one — which is what the post-condition exists to catch, so the
comment now points at it instead of asserting an invariant the traversal
does not have.
The `if (placed.has(member.name)) return` after leaving the stack was
unreachable: a re-entrant visit returns at the top guard while the member
is on the stack, so it can never be placed by the time the recursion
unwinds.
2026-08-14 15:14:16 +08:00
|
|
|
export function attemptEchoed(command: string, args: readonly string[], options: RunOptions = {}): CommandResult {
|
2026-08-14 11:27:14 +08:00
|
|
|
const result = spawnSync(command, [...args], {
|
|
|
|
|
cwd: options.cwd,
|
|
|
|
|
env: options.env,
|
|
|
|
|
encoding: 'utf8',
|
|
|
|
|
stdio: ['inherit', 'pipe', 'pipe'],
|
|
|
|
|
})
|
|
|
|
|
if (result.error !== undefined) throw result.error
|
|
|
|
|
if (result.stdout !== '') process.stdout.write(result.stdout)
|
|
|
|
|
if (result.stderr !== '') process.stderr.write(result.stderr)
|
|
|
|
|
return { status: result.status, stdout: result.stdout, stderr: result.stderr }
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-11 00:51:02 +08:00
|
|
|
/**
|
|
|
|
|
* 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)}`)
|
|
|
|
|
}
|
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
|
|
|
|
|
|
|
|
/**
|
2026-08-19 17:55:06 +08:00
|
|
|
* Return whether Node started the given module as the process entry point.
|
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
|
|
|
* @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))
|
|
|
|
|
}
|