Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
// How publish-packages.yml releases a package. From this one process, in order: the release tag must name
// the package's declared prefix and its manifest version, the release-checks suite must pass, the semver
2026-11-12 21:21:41 +00:00
// policy check must pass for that version, an npm release's declared dependency ranges must hold (the
// workspace is built, then every tagged release a sibling's range admits is checked against what the
// sibling imports), the package's own tests must pass, and only then the release action runs. No workflow
// condition can then release what any of those checks did not pass.
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
// docs/semver-policy.md, "The suite gates every release".
2026-10-03 13:43:47 +02:00
//
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
// node scripts/release-after-suite.mjs npm-publish <package dir> <git ref> npm publish --access public in <dir>
// node scripts/release-after-suite.mjs python-build <package dir> <git ref> python -m build <dir>, for the PyPI step
//
// <git ref> is the run's GITHUB_REF. A tag ref is a release and gets the tag and semver checks; npm-publish
// refuses any other ref. python-build on a branch ref builds without them, for the manual runs that publish
// nothing.
2026-10-03 13:43:47 +02:00
import { spawnSync } from "node:child_process" ;
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
import { readFileSync } from "node:fs" ;
import { join } from "node:path" ;
2026-10-03 13:43:47 +02:00
import { fileURLToPath , pathToFileURL } from "node:url" ;
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
import releasePackages from "./release-packages.mjs" ;
2026-10-03 13:43:47 +02:00
export const suite = fileURLToPath ( new URL ( "./check-changelog-semver.test.mjs" , import . meta . url ) ) ;
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
export const checker = fileURLToPath ( new URL ( "./check-changelog-semver.mjs" , import . meta . url ) ) ;
2026-11-12 21:21:41 +00:00
export const rangeCheck = fileURLToPath ( new URL ( "./test-declared-dependency-ranges.mjs" , import . meta . url ) ) ;
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
const repoRoot = fileURLToPath ( new URL ( ".." , import . meta . url ) ) ;
// The checks run without the variables that can make `node --test` run nothing and exit 0
// (NODE_OPTIONS test filters, NODE_TEST_CONTEXT, any NODE_TEST_*), and without the publish token,
// which only the release action receives.
const UNCHECKED = /^(NODE_OPTIONS|NODE_TEST_.*|NODE_AUTH_TOKEN)$/i ;
export const checkEnv = ( env ) => Object . fromEntries ( Object . entries ( env ) . filter ( ( [ key ] ) => ! UNCHECKED . test ( key ) ) ) ;
const pyprojectField = ( text , field ) =>
text . split ( /^\[/m ) . find ( ( section ) => section . startsWith ( "project]" ) ) ? . match ( new RegExp ( ` ^ ${ field } = "([^"]+)" $ ` , "m" ) ) ? . [ 1 ] ;
2026-10-03 13:43:47 +02:00
// Scoped npm packages default to restricted, and CultLib is MIT, so the publish says public.
export const actions = {
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
"npm-publish" : {
manifest : ( dir ) => JSON . parse ( readFileSync ( join ( dir , "package.json" ) , "utf8" ) ) ,
test : ( dir ) => [ "npm" , [ "test" ] , dir ] ,
2026-11-12 21:21:41 +00:00
// The range check builds each tagged release against the workspace's built dists, so it follows a workspace build.
ranges : [ [ "npm" , [ "run" , "build:ts" ] , repoRoot ] , [ process . execPath , [ rangeCheck ] , repoRoot ] ] ,
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
release : ( dir ) => [ "npm" , [ "publish" , "--access" , "public" ] , dir ] ,
tagOnly : true ,
} ,
"python-build" : {
manifest : ( dir ) => {
const text = readFileSync ( join ( dir , "pyproject.toml" ) , "utf8" ) ;
return { name : pyprojectField ( text , "name" ) , version : pyprojectField ( text , "version" ) } ;
} ,
test : ( dir ) => [ "python" , [ "-m" , "unittest" , "discover" , "-s" , join ( dir , "tests" ) ] , undefined ] ,
release : ( dir ) => [ "python" , [ "-m" , "build" , dir ] , undefined ] ,
tagOnly : false ,
} ,
2026-10-03 13:43:47 +02:00
} ;
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
const spawn = ( command , args , cwd , env ) => spawnSync ( command , args , { cwd , env , stdio : "inherit" } ) . status ? ? 1 ;
const refuse = ( message ) => {
console . error ( ` ${ message } ; not releasing. ` ) ;
return 1 ;
} ;
2026-10-03 13:43:47 +02:00
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
// Returns the exit code. `run(command, args, cwd, env)` returns a command's exit code; tests pass their own.
export function releaseAfterSuite ( argv , run = spawn , env = process . env ) {
const [ name , dir , ref , ... rest ] = argv ;
if ( ! Object . hasOwn ( actions , name ? ? "" ) || ! dir || ! ref || rest . length > 0 ) {
console . error ( ` usage: node scripts/release-after-suite.mjs < ${ Object . keys ( actions ) . join ( "|" ) } > <package dir> <git ref> ` ) ;
2026-10-03 13:43:47 +02:00
return 2 ;
}
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
const action = actions [ name ] ;
let manifest ;
try {
manifest = action . manifest ( dir ) ;
} catch {
return refuse ( ` ${ dir } has no readable manifest ` ) ;
}
const entry = Object . hasOwn ( releasePackages , manifest . name ? ? "" ) ? releasePackages [ manifest . name ] : null ;
if ( entry === null ) return refuse ( ` ${ dir } is ${ manifest . name } , which scripts/release-packages.mjs does not declare ` ) ;
const isTag = ref . startsWith ( "refs/tags/" ) ;
const tag = ` refs/tags/ ${ entry . tagPrefix } -v ${ manifest . version } ` ;
if ( isTag && ref !== tag ) return refuse ( ` ${ ref } is not ${ tag } , the tag of ${ manifest . name } 's manifest version ` ) ;
if ( ! isTag && action . tagOnly ) return refuse ( ` ${ name } releases only from a release tag, and ${ ref } is not one ` ) ;
const clean = checkEnv ( env ) ;
if ( run ( process . execPath , [ "--test" , suite ] , undefined , clean ) !== 0 ) {
return refuse ( "The release check suite failed (scripts/check-changelog-semver.test.mjs)" ) ;
}
if ( isTag && run ( process . execPath , [ checker , "--package" , manifest . name , "--version" , manifest . version , "--cwd" , repoRoot ] , undefined , clean ) !== 0 ) {
return refuse ( ` ${ manifest . name } ${ manifest . version } fails the semver policy (docs/semver-policy.md) ` ) ;
2026-10-03 13:43:47 +02:00
}
2026-11-12 21:21:41 +00:00
for ( const [ command , args , cwd ] of isTag ? action . ranges ? ? [ ] : [ ] ) {
if ( run ( command , args , cwd , clean ) !== 0 ) return refuse ( ` ${ manifest . name } 's declared dependency ranges do not hold (scripts/test-declared-dependency-ranges.mjs) ` ) ;
}
Release guard: the checks run in a clean env, and every per-tag check runs in the guard
release-after-suite.mjs now takes the run's git ref and, from one process, checks
the tag against the declared prefix and manifest version, runs the release-checks
suite, the semver policy check and the package's own tests, then releases. The
checks run without NODE_OPTIONS, NODE_TEST_*, or NODE_AUTH_TOKEN; only the release
action gets the token. The workflow's separate tag, semver and test steps are gone,
and so is the pin's continue-on-error/shell/defaults ban, which no release check
needs any more.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-03 14:05:28 +02:00
if ( run ( ... action . test ( dir ) , clean ) !== 0 ) return refuse ( ` ${ manifest . name } 's own tests failed ` ) ;
return run ( ... action . release ( dir ) , env ) ;
2026-10-03 13:43:47 +02:00
}
if ( process . argv [ 1 ] && import . meta . url === pathToFileURL ( process . argv [ 1 ] ) . href ) {
process . exitCode = releaseAfterSuite ( process . argv . slice ( 2 ) ) ;
}