Files
gstack/scripts/compare-pr-version.ts
T
Garry Tanandcapy-ai[bot] 9dea818e34 v1.91.122.0 fix(deps): clear the 2026-11-30 OSV suppressions, replace #3228, exempt dependabot from Version Gate rule 1 (#3239)
* fix(deps): clear the OSV suppressions that expire on 2026-11-30

Delete the hono, @anthropic-ai/sdk (#2754) and sprintf-js ignores, which the
lockfiles no longer match. Override image-size to 2.0.4 (#2753) and chokidar
to 4.0.3 in lib/diagram-render (drops braces), and delete those ignores.
Re-date katex to 2027-01-31: its fix is outside mermaid 11's range
(mermaid-js/mermaid#8388).

html-to-docx 1.8.0 bundles its own image-size 1.x, which the override cannot
reach and whose ICNS parser loops forever. make-pdf --to docx now hands it
only PNG, JPEG, GIF, WebP or SVG bytes. A golden docx test proves the Word
output is unchanged.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* build(deps-dev): @types/bun 1.4.3, prettier 3.9.10; hold the Anthropic SDKs to patches

Replaces dependabot #3228 without its agent-sdk 0.3 and sdk 0.133 bumps,
which break typecheck:test and overlay-sdk-cancel-eof. Their 0.x breaking
releases are semver-minor to dependabot, so the ignore covers minor and major.
@types/bun 1.4.3 retires 18 test type diagnostics; the baseline locks that in.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(ci): exempt dependabot dependency-manifest PRs from Version Gate rule 1

Dependabot never edits VERSION, so hard rule 1 failed every package.json PR
it opened. The workflow now passes the PR author and changed-file list; the
gate skips rule 1 only for dependabot[bot] when every file is a package.json,
bun.lock(b) or .github/dependabot.yml. A failed file lookup keeps the rule.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* fix(make-pdf): guard every html-to-docx sizing path, keep non-ICNS formats

Review found three spellings that got past the regex guard and still hung
--to docx: a data-src decoy, a percent-encoded separator, and an unquoted
encoded colon. html-to-docx sizes images on three paths, and buildImage
URI-decodes the whole src first. The guard now mirrors all three, drops ICNS
(the only input its bundled image-size routes to the looping parser) instead
of allowlisting formats, so BMP and TIFF embed as before, and re-parses the
result with html-to-docx's own html-to-vdom to fail closed on anything the
regex misreads. The golden test tolerates CRLF checkouts on Windows.

Version Gate treats a failed file lookup as an empty list, so a partial
paginated result cannot decide the exemption.

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): stamp v1.91.121.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

* chore(release): restamp v1.91.122.0

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>

---------

Co-authored-by: capy-ai[bot] <230910855+capy-ai[bot]@users.noreply.github.com>
2026-10-17 16:47:50 +00:00

121 lines
5.2 KiB
TypeScript

#!/usr/bin/env bun
// compare-pr-version — CI gate helper. Validates the PR's branch VERSION
// against the queue of other open PRs' claimed versions. Exits 0 (pass)
// or 1 (confirmed collision).
//
// Input:
// argv[2] — path to next.json (the util's JSON output)
// argv[3] — optional PR number for log lines
//
// Design note: fail-open on util error. A gstack bug must never freeze the
// merge queue. The gate enforces ONE rule: this PR must not claim the same
// version as another open PR. Lower-than-the-util's-suggestion is fine if
// the slot is unclaimed — that preserves monotonic version ordering on main
// when this PR lands ahead of higher-numbered queued PRs. The util's output
// is informational (the *recommended* slot for fresh /ship runs); the gate
// only blocks actual collisions.
import { readFileSync } from "node:fs";
const [, , jsonPath, prNumber] = process.argv;
if (!jsonPath) {
console.error("Usage: compare-pr-version <next.json> [pr-number]");
process.exit(2);
}
let parsed: any;
try {
parsed = JSON.parse(readFileSync(jsonPath, "utf8"));
} catch (e) {
console.log("::warning::could not parse util output; failing open");
process.exit(0);
}
if (parsed.offline === true) {
console.log("::warning::workspace-aware-ship util offline; failing open (no collision check performed)");
console.log(`::notice::If you merge this PR and a queued PR landed ahead, CHANGELOG may need manual reconciliation.`);
process.exit(0);
}
// PR_VERSION is supplied via env (set by the workflow from `cat VERSION`).
const prVersion = (process.env.PR_VERSION ?? "").trim();
const nextSlot = parsed.version;
if (!prVersion) {
console.log("::warning::PR_VERSION not set; failing open");
process.exit(0);
}
// Parse versions for comparison.
function parseV(s: string): number[] | null {
const m = s.match(/^(\d+)\.(\d+)\.(\d+)\.(\d+)$/);
return m ? [Number(m[1]), Number(m[2]), Number(m[3]), Number(m[4])] : null;
}
function cmp(a: number[], b: number[]): number {
for (let i = 0; i < 4; i++) if (a[i] !== b[i]) return a[i] - b[i];
return 0;
}
const pPR = parseV(prVersion);
const pNext = parseV(nextSlot);
if (!pPR || !pNext) {
console.log(`::warning::malformed version string (PR=${prVersion}, next=${nextSlot}); failing open`);
process.exit(0);
}
const tag = prNumber ? `PR #${prNumber}` : "this PR";
const claimed = (parsed.claimed ?? []) as Array<{ pr: number; branch: string; version: string; url?: string }>;
// Emit a GitHub step summary (always helpful, even on pass).
const claimedList = claimed
.map((c) => ` #${c.pr} ${c.branch} → v${c.version}`)
.join("\n");
console.log(`::group::Version gate (${tag})`);
console.log(` PR VERSION: v${prVersion}`);
console.log(` Suggested: v${nextSlot} (util's next-slot recommendation)`);
console.log(` Queue (${claimed.length} open PRs claiming versions):`);
if (claimedList) console.log(claimedList);
console.log("::endgroup::");
// Dependabot never edits VERSION, so hard rule 1 would fail every dependency
// PR it opens. Exempt it only when the author is dependabot and every changed
// file is a dependency manifest, a lockfile or the dependabot config. The
// workflow passes the author and the file list; an empty list (the lookup
// failed) keeps the rule.
const DEPENDENCY_FILE = /(?:^|\/)(?:package\.json|bun\.lockb?)$|^\.github\/dependabot\.yml$/;
const changedFiles = (process.env.PR_CHANGED_FILES ?? "").split("\n").map((f) => f.trim()).filter(Boolean);
const dependabotManifestOnly =
process.env.PR_AUTHOR === "dependabot[bot]" && changedFiles.length > 0 && changedFiles.every((f) => DEPENDENCY_FILE.test(f));
// Hard rule 1: this PR's VERSION must be strictly greater than the base
// version, otherwise we're not actually bumping.
const pBase = parseV((parsed.base_version ?? "").trim());
if (pBase && cmp(pPR, pBase) <= 0) {
if (dependabotManifestOnly) {
console.log(`::notice::${tag} is a dependabot dependency update (${changedFiles.join(", ")}); VERSION bump not required.`);
process.exit(0);
}
console.log(`::error::VERSION not bumped: ${tag} claims v${prVersion} but base is v${parsed.base_version}.`);
process.exit(1);
}
// Hard rule 2: no collision with another open PR's claimed VERSION.
const collision = claimed.find((c) => c.version.trim() === prVersion);
if (collision) {
console.log(`::error::VERSION collision: ${tag} claims v${prVersion} but #${collision.pr} (${collision.branch}) already claims the same slot.`);
console.log(`::error::Rerun /ship to pick a different slot, or coordinate with #${collision.pr} on landing order.`);
process.exit(1);
}
// Optional informational note: PR version is below the util's suggested next
// slot. This is allowed — the suggested slot is a recommendation for /ship's
// next run, but landing at a lower-but-unclaimed slot first preserves
// monotonic ordering on main when this PR merges ahead of higher-numbered
// queued PRs.
if (cmp(pPR, pNext) < 0) {
console.log(`::notice::${tag} claims v${prVersion}, below util's suggestion v${nextSlot}. Slot is unclaimed; gate passes. If this PR lands ahead of queued PRs at higher slots, version ordering on main remains monotonic.`);
}
console.log(`✓ ${tag} claims v${prVersion} — slot is free.`);
process.exit(0);