mirror of
https://github.com/garrytan/gstack.git
synced 2026-10-31 14:06:50 +01:00
* 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>
121 lines
5.2 KiB
TypeScript
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);
|