mirror of
https://github.com/THU-MAIC/OpenMAIC.git
synced 2026-10-19 05:49:27 +08:00
* chore(packages): publish storage and enforce version bumps * fix(packages): cover all publish inputs and tarballs * fix(packages): scope version guard to release inputs * fix(packages): fail closed on new package inputs * docs(packages): clarify version guard boundaries * fix(packages): harden release guard edge cases * fix(packages): align smoke peers and git inputs * docs(packages): make release smoke policy explicit * fix(packages): anchor version guard at repository root * fix(packages): report version guard setup errors * fix(packages): harden release validation * fix(ci): validate complete main pushes * test(storage): pin the exported PostgreSQL schemas DOCUMENT_PG_SCHEMA and RUNTIME_PG_SCHEMA are public API. A deployment that provisions these tables with its own migration tooling has to reproduce the DDL exactly for ensureDocumentSchema() / ensureSchema() to stay the intended no-op against an already-provisioned database. Nothing guarded that today: every statement is CREATE ... IF NOT EXISTS, so PostgreSQL silently accepts whatever table already exists under the name. A column type, a nullability, an index or a FK action can drift apart from a downstream migration without an error, and the first symptom is a store query failing in production or succeeding against the wrong types. Pin both constants verbatim, and assert every statement stays IF NOT EXISTS guarded so the ensure functions remain idempotent. The pin does not judge the DDL; it makes changing it impossible to do by accident. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): gate publishing on the version check The version check ran only in ci.yml. publish-packages.yml is a separate workflow, so it could reach `pnpm publish` on a main push, a matching tag, or a manual run with dry_run=false even when CI was red, skipped, or never ran. Run the validation inside the publish job, before publishing, in a new release mode that works for every trigger. A push range only exists for branch pushes, so release mode judges each package against the registry instead: a version that is not published yet is a release, and a version that is already published is accepted only when the package source has not moved since that release. `pnpm publish` skips an already-published version, so without that second half a drifted package releases as a silent no-op. The anchor for "since that release" is the @openmaic/<name>@<version> tag the job now writes after every successful publish, falling back to the push range for packages released before those tags existed. With neither anchor the release stops instead of guessing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): make the release gate prove only what it can Three independent reviews of the previous revision converged on one root cause: it used git tags and a push range as the record of which tree produced a published version. Neither is trustworthy. Tags are mutable and can be created or moved by hand, so "the tag resolves" was being read as "the tag is authentic"; pnpm records no gitHead; and a push range describes one push, not the origin of a release. Packages released before the scheme existed had no anchor at all, which made the tag and manual triggers fail closed from the first run with no way to self-heal, dry runs included. Drop the anchor machinery. Drift is prevented where it is provable, in diff mode at merge time, which sees every commit before it can be released. The pre-publish gate is now limited to claims a release can establish: every version it publishes is new and moves forward, an already-published version is reported and left alone, and any registry answer that is not a definitive 404 stops the release. Around it, close what the reviews found in the workflow: - real publishes only from a commit contained in main, so a tag or manual run cannot ship an unreviewed ref while holding NPM_TOKEN - one repository-wide concurrency group, so a main push and a tag push cannot each decide the same version is unpublished - publish and mark each package individually, so a partial failure stays retryable instead of leaving published packages unrecorded - fail if the build rewrites tracked package files, which would otherwise publish content that is not in the released commit - compare against every published version when ordering, and refuse to guess when the registry holds versions this check cannot order The schema pin also asserted the constants without asserting what the ensure functions execute, so those could diverge. Both are now run against a recording queryable and their exact statement sequence is asserted. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): refuse to release from a tree behind the registry Simplifying the gate introduced a hole that a mutation check caught: a package whose local version is already published took the "already published, skip" path before any ordering check, so a tree rolled back to an older published version passed silently. That is not a harmless no-op, because pnpm publish rewrites each workspace dependency to the version in the tree, and a sibling released alongside it would be published declaring a dependency on the older package. Order every package against the registry first, then decide: at the highest published version it is a legitimate skip, below it the release stops. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): couple publishing to a green CI run for the same commit Second review round, on the reworked design. The headline defect was a hard blocker: `git fetch --depth=0` is not valid git (that is an actions/checkout convention), so under `set -e` every real publish aborted before the gate ever ran. Remove the depth argument; the checkout already has full history. The rest closes the gap both reviewers reached independently, that being on main is not the same as having passed the gate: - wait for this exact commit's CI conclusion and require success. ci.yml runs concurrently with this workflow on a push to main and blocks nothing, so an unpublished version whose source drifted could be released while its own version check was still running or already red. - require the commit on main's FIRST-PARENT history. Plain reachability also accepts every intermediate commit of every branch merged with a merge commit. A pull request that adds a bad tree and reverts it in the next commit leaves that tree an ancestor of main forever, and a tag pointing at it published it. - do not cancel CI runs on main. Each run validates only its own push range, so cancelling one drops the range that contained the change, and its replacement compares against the cancelled tip and sees nothing. - run the version check on every main push. A branch creation or force push reports an unusable `before`, which used to skip the gate entirely; fall back to the first parent instead. - publish with --ignore-scripts. Every package's prepublishOnly reruns the build, deleting and regenerating the dist that was just verified and smoke-tested, so npm could receive bytes nothing had checked. - run storage's PostgreSQL contract suites before publishing it. They skip themselves without a database, so the one backend that needs a real PostgreSQL was shipping unexercised. - order registry versions with real semver, prereleases included. Rejecting every non-x.y.z version meant one historical prerelease anywhere in one package's history would refuse every future release of every package. - warn when a release marker exists but points at another commit. Note what is NOT enforceable here: a workflow_dispatch or tag run executes the workflow definition from the selected ref, so a branch that edits this file can delete these checks. The boundary has to be a GitHub Environment holding NPM_TOKEN behind a main-only deployment branch rule. The job now declares that environment and the header states the required setup; the in-file checks are defence in depth. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): stop the first-parent check failing on SIGPIPE Third review round. The blocker this time: `git rev-list --first-parent | grep -qx` under `set -o pipefail`. grep exits at the first match, rev-list then dies of SIGPIPE, and the pipeline returns 141, so a commit that IS on main is read as "not on main" and every real publish is refused. It does not reproduce in a shallow clone, where main's history fits in the pipe buffer; with the full history this job checks out it always will. Count matches instead, which reads the whole stream. Also from this round: - identify the CI run by workflow file and triggering event rather than by a check-run display name. Names are not unique, so any other workflow or app publishing a check called "Lint, Typecheck & Unit Tests" could stand in for a red version gate. - fail the main-push version check when the push range is unusable instead of substituting HEAD^. A force push can replace many commits at once, so the previous commit is not the range that needs checking, and a green run here is what publishing depends on. - correct the stated limitation. It claimed the package-directory input model was exact for dsl and storage; it is not. Their dist is whatever the lockfile's TypeScript emits, and dsl's shipped JSON schema comes from the lockfile's ts-json-schema-generator, so a toolchain bump can change any of the four tarballs with no diff under the package directory. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * refactor(packages): make a merged version bump the only release input The tag trigger was incoherent. Its own documentation admitted that any @openmaic/* tag republishes the whole family and that which packages actually go out is decided by the manifests, so the tag name never participated in the decision. It was a second entry point to one decision function, it bypassed review entirely because a tag is not a diff and can be created by anyone with write access on any commit, and it cannot be guarded: environment deployment rules match GITHUB_REF, so refs/tags/* and a main-only rule are mutually exclusive. Drop it. A version bump that landed on main is now the only release input, and `@openmaic/<name>@<version>` tags are an output written after a package reaches the registry. Nothing is lost: a manual dispatch from main still republishes without a new commit, and that path is inside the protected environment. Split the workflow so the token has a boundary. `validate` holds everything that does not need NPM_TOKEN and runs from any ref, which keeps dry runs useful to contributors. `publish` is the only job declaring the `release` environment, so the token is never attached to a run that is merely validating. It rebuilds rather than sharing state, which is the right trade for a release. Also add the `actions: read` permission the CI-conclusion query needs; it was switched from the checks API to the Actions API without updating the scope, which would have failed with 403 on every real publish. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * docs(contributing): explain when a package version must be bumped CONTRIBUTING said nothing about versioning or releases, while this branch adds a CI check that fails a contributor's PR with "publishable package inputs changed but version did not increase". A gate that rejects work without telling anyone the rule is the kind that ends up disabled. State the rule, the exact failure message, which files are exempt, and how to choose the number, with a specific warning for @openmaic/dsl: it is the contract the other packages validate against, so narrowing what an existing document may contain is breaking even when the diff is small. Also state what contributors do NOT do: publishing is automatic once a bump lands on main, and the release tag is a marker written afterwards rather than something to push. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): run renderer's tests before publishing renderer The pre-publish gate ran renderer's typecheck but never its test suite, so the one package whose behaviour is hardest to typecheck was the one shipping unexercised. It has 25 test files and 242 tests; they just were not in the filter list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(packages): keep the git credential away from package code Review finding: the publish job held `contents: write` while actions/checkout had persisted that credential, and it then ran `pnpm install` and the package builds. Any dependency or build script executed with a credentialed git remote in reach and a write scope to use it, which is a wider blast radius than publishing needs. Separate the two capabilities so no job holds both: - `validate` and `publish` run package code. Both now check out with `persist-credentials: false`, `validate` is pinned to `contents: read`, and `publish` drops write scope entirely, keeping only the `actions: read` it needs for the CI conclusion and `id-token: write` for provenance. - a new `mark` job is the only holder of `contents: write` and a git credential, and it installs nothing and builds nothing. `mark` also reconciles instead of only recording this run. A marker whose push failed previously could never be repaired before, because the release plan excludes versions already on the registry and the marker logic sat behind that plan. It now marks any registry version whose tag is missing, and leaves an existing tag alone rather than moving it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
423 lines
15 KiB
JavaScript
423 lines
15 KiB
JavaScript
import { execFileSync } from 'node:child_process';
|
|
import { readFileSync, writeFileSync } from 'node:fs';
|
|
import { join } from 'node:path';
|
|
|
|
const REGISTRY = 'https://registry.npmjs.org';
|
|
|
|
const commonIgnoredInputs = {
|
|
files: ['.gitignore', 'vitest.config.ts'],
|
|
directories: ['docs/', 'test/'],
|
|
};
|
|
|
|
// Keep this package set in lockstep with publish-packages.yml. The release
|
|
// workflow intentionally publishes only these four owned packages.
|
|
//
|
|
// KNOWN LIMITATION: this treats "publishable input" as "file under the package
|
|
// directory", which is an under-approximation for all four packages.
|
|
//
|
|
// Renderer and importer inline their dependency graph through Rollup, so a
|
|
// lockfile-only resolution change rewrites their published bundles. Even dsl
|
|
// and storage are not exempt: their dist is whatever the lockfile's TypeScript
|
|
// emits, and dsl's shipped JSON schema is generated by the lockfile's
|
|
// ts-json-schema-generator. A toolchain bump can therefore change any of the
|
|
// four tarballs with no diff under the package directory.
|
|
//
|
|
// Closing this would mean treating the lockfile and toolchain configuration as
|
|
// an input of every package, which makes every dependency bump demand four
|
|
// version bumps, or externalising the bundled dependencies. Both are decisions
|
|
// about how the packages are built rather than about this check. Diff mode is a
|
|
// merge-time guard against the common case, not a proof of byte equality.
|
|
const ignoredPackageInputs = {
|
|
dsl: commonIgnoredInputs,
|
|
storage: commonIgnoredInputs,
|
|
renderer: commonIgnoredInputs,
|
|
importer: {
|
|
// These are local tooling, demo assets, or the legacy reference
|
|
// implementation. The importer build and package files exclude all of them.
|
|
files: [
|
|
...commonIgnoredInputs.files,
|
|
'.babelrc.cjs',
|
|
'.eslintignore',
|
|
'.eslintrc.cjs',
|
|
'DESIGN.md',
|
|
'SKILL.md',
|
|
'favicon.ico',
|
|
'index.html',
|
|
],
|
|
directories: [...commonIgnoredInputs.directories, 'scripts/', 'src1/'],
|
|
},
|
|
};
|
|
|
|
const usage = [
|
|
'Usage:',
|
|
' check-package-version-bumps.mjs <base-ref> (diff mode, merge-time gate)',
|
|
' check-package-version-bumps.mjs --release (release mode, pre-publish gate)',
|
|
].join('\n');
|
|
|
|
let repositoryRoot;
|
|
try {
|
|
repositoryRoot = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
encoding: 'utf8',
|
|
}).trim();
|
|
} catch {
|
|
console.error('Package version checks must run inside a Git worktree.');
|
|
process.exit(2);
|
|
}
|
|
|
|
function git(args, { quiet = false } = {}) {
|
|
return execFileSync('git', args, {
|
|
cwd: repositoryRoot,
|
|
encoding: 'utf8',
|
|
// execFileSync forwards the child's stderr to ours by default, which turns
|
|
// an expected lookup miss into a scary "fatal:" line in the job log.
|
|
stdio: quiet ? ['ignore', 'pipe', 'pipe'] : undefined,
|
|
}).trim();
|
|
}
|
|
|
|
function gitFileAt(ref, file) {
|
|
const path = git(['ls-tree', '--full-tree', '--name-only', ref, '--', file]);
|
|
if (path === '') return undefined;
|
|
return git(['show', `${ref}:${file}`]);
|
|
}
|
|
|
|
function resolveCommit(ref) {
|
|
if (!ref) return undefined;
|
|
try {
|
|
return git(['rev-parse', '--verify', `${ref}^{commit}`], { quiet: true });
|
|
} catch {
|
|
return undefined;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a semver version. Prerelease identifiers are kept because the registry
|
|
* holds whatever was ever published: refusing to order them would let one
|
|
* historical `x.y.z-beta.1` block every future release of every package.
|
|
*/
|
|
function parseVersion(raw) {
|
|
const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?(?:\+[0-9A-Za-z.-]+)?$/.exec(raw);
|
|
if (!match) return undefined;
|
|
return {
|
|
raw,
|
|
parts: match.slice(1, 4).map(Number),
|
|
prerelease: match[4] === undefined ? undefined : match[4].split('.'),
|
|
};
|
|
}
|
|
|
|
function isStable(version) {
|
|
return version.prerelease === undefined;
|
|
}
|
|
|
|
/** Semver precedence for prerelease identifiers. */
|
|
function comparePrerelease(left, right) {
|
|
if (left === undefined && right === undefined) return 0;
|
|
// A version without a prerelease outranks one with it.
|
|
if (left === undefined) return 1;
|
|
if (right === undefined) return -1;
|
|
for (let i = 0; i < Math.max(left.length, right.length); i += 1) {
|
|
const a = left[i];
|
|
const b = right[i];
|
|
if (a === undefined) return -1;
|
|
if (b === undefined) return 1;
|
|
const aNumeric = /^\d+$/.test(a);
|
|
const bNumeric = /^\d+$/.test(b);
|
|
if (aNumeric && bNumeric) {
|
|
if (Number(a) !== Number(b)) return Number(a) - Number(b);
|
|
} else if (aNumeric !== bNumeric) {
|
|
// Numeric identifiers always have lower precedence than alphanumeric.
|
|
return aNumeric ? -1 : 1;
|
|
} else if (a !== b) {
|
|
return a < b ? -1 : 1;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
function readVersion(contents, source) {
|
|
const raw = JSON.parse(contents).version;
|
|
const version = parseVersion(raw);
|
|
if (!version || !isStable(version)) {
|
|
throw new Error(`${source} must use a stable x.y.z version, got ${JSON.stringify(raw)}`);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
function compareVersions(left, right) {
|
|
for (let i = 0; i < left.parts.length; i += 1) {
|
|
if (left.parts[i] !== right.parts[i]) return left.parts[i] - right.parts[i];
|
|
}
|
|
return comparePrerelease(left.prerelease, right.prerelease);
|
|
}
|
|
|
|
function packageDirectory(name) {
|
|
return `packages/@openmaic/${name}`;
|
|
}
|
|
|
|
/** Whether any publishable input of `name` differs between `base` and HEAD. */
|
|
function publishableInputsChanged(name, base) {
|
|
const directory = packageDirectory(name);
|
|
const ignored = ignoredPackageInputs[name];
|
|
const changed = git([
|
|
'diff',
|
|
'--name-only',
|
|
'--no-renames',
|
|
'--diff-filter=ACDMRT',
|
|
`${base}...HEAD`,
|
|
'--',
|
|
directory,
|
|
])
|
|
.split('\n')
|
|
.filter(Boolean);
|
|
|
|
return changed.some((file) => {
|
|
const relative = file.slice(directory.length + 1);
|
|
const isIgnored =
|
|
ignored.files.includes(relative) ||
|
|
ignored.directories.some((prefix) => relative.startsWith(prefix));
|
|
return !isIgnored;
|
|
});
|
|
}
|
|
|
|
function failIfAny(failures, headline) {
|
|
if (failures.length === 0) return;
|
|
console.error([headline, ...failures.map((failure) => `- ${failure}`)].join('\n'));
|
|
process.exit(1);
|
|
}
|
|
|
|
/**
|
|
* Diff mode: every publishable change between `base` and HEAD must carry a
|
|
* version increase.
|
|
*
|
|
* This is where drift is actually prevented. It runs on pull requests and on
|
|
* pushes to main, where a real push range always exists, so it sees every
|
|
* commit that can change a package before that change is reachable by a
|
|
* release.
|
|
*/
|
|
function runDiffMode(base) {
|
|
if (!resolveCommit(base)) {
|
|
console.error(`Base ref ${JSON.stringify(base)} is not an available commit.`);
|
|
process.exit(2);
|
|
}
|
|
|
|
const failures = [];
|
|
for (const name of Object.keys(ignoredPackageInputs)) {
|
|
if (!publishableInputsChanged(name, base)) continue;
|
|
|
|
const manifest = `${packageDirectory(name)}/package.json`;
|
|
const beforeContents = gitFileAt(base, manifest);
|
|
const afterContents = gitFileAt('HEAD', manifest);
|
|
if (afterContents === undefined) {
|
|
failures.push(`${name}: ${manifest} was removed`);
|
|
continue;
|
|
}
|
|
if (beforeContents === undefined) {
|
|
console.log(`${name}: new package at ${base}, nothing to compare.`);
|
|
continue;
|
|
}
|
|
|
|
let before;
|
|
let after;
|
|
try {
|
|
before = readVersion(beforeContents, `${manifest} at ${base}`);
|
|
after = readVersion(afterContents, `${manifest} at HEAD`);
|
|
} catch (error) {
|
|
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
continue;
|
|
}
|
|
|
|
if (compareVersions(after, before) <= 0) {
|
|
failures.push(
|
|
`${name}: publishable package inputs changed but version did not increase ` +
|
|
`(${before.raw} -> ${after.raw})`,
|
|
);
|
|
} else {
|
|
console.log(`${name}: ${before.raw} -> ${after.raw}`);
|
|
}
|
|
}
|
|
|
|
failIfAny(
|
|
failures,
|
|
'Every @openmaic publishable package change must ship with a new package version:',
|
|
);
|
|
console.log('Package version check passed.');
|
|
}
|
|
|
|
/**
|
|
* Versions of `name` already on the registry.
|
|
*
|
|
* Returns `undefined` only for a definitive "this package does not exist"
|
|
* answer. Every other outcome — a transient error, an auth or proxy failure, an
|
|
* unparseable body, a registry that is not the one we publish to — exits
|
|
* non-zero, because reading "unknown" as "never published" would skip the
|
|
* checks below entirely.
|
|
*/
|
|
function registryVersions(name) {
|
|
let stdout = '';
|
|
let stderr = '';
|
|
let failed = false;
|
|
try {
|
|
stdout = execFileSync('npm', ['view', name, 'versions', '--json', '--registry', REGISTRY], {
|
|
encoding: 'utf8',
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
});
|
|
} catch (error) {
|
|
failed = true;
|
|
stdout = String(error.stdout ?? '');
|
|
stderr = String(error.stderr ?? '');
|
|
}
|
|
|
|
const body = stdout.trim();
|
|
if (body !== '') {
|
|
let parsed;
|
|
try {
|
|
parsed = JSON.parse(body);
|
|
} catch {
|
|
console.error(`Registry response for ${name} was not JSON: ${body.slice(0, 200)}`);
|
|
process.exit(2);
|
|
}
|
|
// npm --json reports errors as an object with an `error` member.
|
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed) && parsed.error) {
|
|
if (parsed.error.code === 'E404') return undefined;
|
|
console.error(
|
|
`Registry error for ${name}: ${parsed.error.code} ${parsed.error.summary ?? ''}`,
|
|
);
|
|
process.exit(2);
|
|
}
|
|
if (Array.isArray(parsed) && parsed.every((entry) => typeof entry === 'string')) return parsed;
|
|
if (typeof parsed === 'string') return [parsed];
|
|
console.error(`Unexpected registry payload for ${name}: ${body.slice(0, 200)}`);
|
|
process.exit(2);
|
|
}
|
|
|
|
if (failed && /E404|404 Not Found/.test(stderr)) return undefined;
|
|
console.error(
|
|
`Unable to determine the published versions of ${name}: ${stderr.trim() || 'empty response'}`,
|
|
);
|
|
process.exit(2);
|
|
}
|
|
|
|
/**
|
|
* Release mode: runs inside the publish job, before publishing, for every
|
|
* trigger.
|
|
*
|
|
* Scope note. An earlier revision of this gate also tried to prove, at publish
|
|
* time, that a package whose version is already on the registry still matches
|
|
* the source that produced it. There is no trustworthy record to prove that
|
|
* against: git tags are mutable and can be created or moved by hand, pnpm does
|
|
* not record `gitHead`, and a push range describes one push rather than the
|
|
* origin of a release. Every anchor available here is either forgeable or
|
|
* missing for packages released before the scheme existed, and treating a
|
|
* forgeable anchor as authoritative is worse than not checking, because it
|
|
* turns "unproven" into "proven".
|
|
*
|
|
* So drift is prevented where it is provable — diff mode, at merge time, on
|
|
* every push to main — and this gate is limited to the claims a release can
|
|
* actually establish:
|
|
*
|
|
* 1. every version about to be published is new and moves forward, so a
|
|
* release can never quietly reuse or downgrade a published version;
|
|
* 2. a package whose version is already published is reported and left
|
|
* alone, so `pnpm publish` skipping it is a stated outcome rather than a
|
|
* silent one;
|
|
* 3. anything the registry cannot answer definitively stops the release.
|
|
*
|
|
* The workflow adds the two guarantees that do not belong in a script: real
|
|
* publishes only happen from a commit contained in `main`, and each package is
|
|
* published and tagged individually so a partial failure stays retryable.
|
|
*/
|
|
function runReleaseMode() {
|
|
const failures = [];
|
|
const releases = [];
|
|
|
|
for (const name of Object.keys(ignoredPackageInputs)) {
|
|
const packageName = `@openmaic/${name}`;
|
|
const manifest = join(repositoryRoot, packageDirectory(name), 'package.json');
|
|
|
|
let local;
|
|
try {
|
|
local = readVersion(readFileSync(manifest, 'utf8'), `${packageName} package.json`);
|
|
} catch (error) {
|
|
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
continue;
|
|
}
|
|
|
|
const published = registryVersions(packageName);
|
|
if (published === undefined) {
|
|
console.log(`${packageName}: not on the registry yet, ${local.raw} is the first release.`);
|
|
releases.push({ package: packageName, version: local.raw });
|
|
continue;
|
|
}
|
|
|
|
// Order against every published version, prereleases included: each one
|
|
// occupies its number on the registry, so discarding them could let a
|
|
// release move backwards.
|
|
const unparsable = published.filter((version) => parseVersion(version) === undefined);
|
|
if (unparsable.length > 0) {
|
|
failures.push(
|
|
`${packageName}: the registry holds versions that are not semver ` +
|
|
`(${unparsable.slice(0, 5).join(', ')}); refusing to order ${local.raw} against them.`,
|
|
);
|
|
continue;
|
|
}
|
|
const parsed = published.map(parseVersion).filter((version) => version !== undefined);
|
|
if (parsed.length === 0) {
|
|
failures.push(`${packageName}: the registry reports no usable versions; refusing to guess.`);
|
|
continue;
|
|
}
|
|
const highest = [...parsed].sort(compareVersions).pop();
|
|
|
|
if (published.includes(local.raw)) {
|
|
// Being behind the registry is not a harmless no-op. `pnpm publish`
|
|
// rewrites each `workspace:*` dependency to the version in this tree, so
|
|
// a sibling released alongside a rolled-back package would be published
|
|
// declaring a dependency on the older one.
|
|
if (compareVersions(local, highest) < 0) {
|
|
failures.push(
|
|
`${packageName}: the tree is at ${local.raw} while ${highest.raw} is published. ` +
|
|
'Refusing to release from a tree that is behind the registry: any sibling ' +
|
|
`published from it would declare a dependency on ${local.raw}.`,
|
|
);
|
|
continue;
|
|
}
|
|
console.log(
|
|
`${packageName}: ${local.raw} is already published; this run will not republish it.`,
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if (compareVersions(local, highest) <= 0) {
|
|
failures.push(
|
|
`${packageName}: ${local.raw} is not greater than the published ${highest.raw}; ` +
|
|
'refusing to release from a stale tree',
|
|
);
|
|
continue;
|
|
}
|
|
|
|
console.log(`${packageName}: releasing ${local.raw} (published: ${highest.raw}).`);
|
|
releases.push({ package: packageName, version: local.raw });
|
|
}
|
|
|
|
failIfAny(failures, 'Refusing to publish @openmaic packages:');
|
|
|
|
const planPath = process.env.RELEASE_PLAN_PATH;
|
|
if (planPath) {
|
|
writeFileSync(planPath, `${JSON.stringify(releases, null, 2)}\n`);
|
|
console.log(`Wrote the release plan for ${releases.length} package(s) to ${planPath}.`);
|
|
}
|
|
|
|
if (releases.length === 0) {
|
|
console.log('Nothing to release: every package version is already on the registry.');
|
|
}
|
|
console.log('Release version check passed.');
|
|
}
|
|
|
|
const args = process.argv.slice(2);
|
|
if (args[0] === '--release') {
|
|
runReleaseMode();
|
|
} else if (args.length > 0 && !args[0].startsWith('--')) {
|
|
runDiffMode(args[0]);
|
|
} else {
|
|
console.error(usage);
|
|
process.exit(2);
|
|
}
|