perf(release): cache changelog GitHub discovery

This commit is contained in:
Vincent Koc
2026-07-10 15:46:59 -07:00
committed by Vincent Koc
parent f1447e9737
commit 58b094cfce
3 changed files with 243 additions and 5 deletions
@@ -49,6 +49,13 @@ every human `Thanks @...` attribution.
--write-ledger
```
The verifier automatically reuses public GitHub GraphQL responses from an
exact base/target SHA snapshot under the worktree's git metadata. Iterative
rewrites at the same target avoid repeated network discovery. Use
`--refresh-github-snapshot` after suspect API data, `--github-snapshot
<path>` for an explicit artifact, or `--no-github-snapshot` for a live-only
audit. GitHub release bodies are always read live.
- the manifest is the required input to the rewrite, not an after-the-fact
audit; it contains every referenced PR, eligible contributor credit,
inline issue context, every direct commit, and an editorial-eligibility
@@ -1,7 +1,8 @@
#!/usr/bin/env node
import { execFileSync, spawnSync } from "node:child_process";
import { readFileSync, writeFileSync } from "node:fs";
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
import path from "node:path";
import { pathToFileURL } from "node:url";
import {
extractChangelogReleaseSections,
@@ -12,6 +13,7 @@ import {
} from "../../../../scripts/render-github-release-notes.mjs";
const repo = "openclaw/openclaw";
const githubSnapshotSchemaVersion = 1;
const commitAssociationQueryBatchSize = 20;
const excludedHandles = new Set(["openclaw", "clawsweeper", "claude", "codex", "steipete"]);
const nonEditorialTypes = new Set([
@@ -48,6 +50,7 @@ const genericDirectCommitTerms = new Set([
"restore",
"update",
]);
let githubSnapshotState;
function fail(message) {
throw new Error(message);
@@ -65,6 +68,11 @@ Required:
Options:
--manifest <path> Read or write the complete contribution record ledger.
--github-snapshot <path>
Override the exact-range GitHub GraphQL snapshot path.
--no-github-snapshot Disable GitHub GraphQL snapshot reuse.
--refresh-github-snapshot
Ignore an existing exact-range snapshot and rebuild it.
--seed-ref <ref> Use an existing release section as editorial input.
--shipped-ref <tag> Exclude PRs already recorded by this shipped tag; repeatable.
--write-ledger Write the verified ledger back into CHANGELOG.md.
@@ -81,6 +89,9 @@ function parseArgs(argv) {
help: false,
json: false,
manifestPath: undefined,
githubSnapshotPath: undefined,
noGithubSnapshot: false,
refreshGithubSnapshot: false,
seedRef: undefined,
shippedRefs: [],
writeLedger: false,
@@ -92,9 +103,23 @@ function parseArgs(argv) {
options.help = true;
continue;
}
if (arg === "--check-github" || arg === "--json" || arg === "--write-ledger") {
if (
arg === "--check-github" ||
arg === "--json" ||
arg === "--no-github-snapshot" ||
arg === "--refresh-github-snapshot" ||
arg === "--write-ledger"
) {
options[
arg === "--check-github" ? "checkGithub" : arg === "--write-ledger" ? "writeLedger" : "json"
arg === "--check-github"
? "checkGithub"
: arg === "--write-ledger"
? "writeLedger"
: arg === "--no-github-snapshot"
? "noGithubSnapshot"
: arg === "--refresh-github-snapshot"
? "refreshGithubSnapshot"
: "json"
] = true;
continue;
}
@@ -104,6 +129,7 @@ function parseArgs(argv) {
arg === "--version" ||
arg === "--release-tag" ||
arg === "--shipped-ref" ||
arg === "--github-snapshot" ||
arg === "--manifest" ||
arg === "--seed-ref"
) {
@@ -117,6 +143,8 @@ function parseArgs(argv) {
options.shippedRefs.push(value);
} else if (arg === "--manifest") {
options.manifestPath = value;
} else if (arg === "--github-snapshot") {
options.githubSnapshotPath = value;
} else if (arg === "--seed-ref") {
options.seedRef = value;
} else {
@@ -140,6 +168,12 @@ function parseArgs(argv) {
if (!options.help && options.checkGithub && options.releaseTags.length === 0) {
fail("--check-github requires at least one --release-tag");
}
if (options.noGithubSnapshot && options.githubSnapshotPath) {
fail("--no-github-snapshot cannot be combined with --github-snapshot");
}
if (options.noGithubSnapshot && options.refreshGithubSnapshot) {
fail("--no-github-snapshot cannot be combined with --refresh-github-snapshot");
}
const uniqueShippedRefs = new Set(options.shippedRefs);
if (uniqueShippedRefs.size !== options.shippedRefs.length) {
fail("--shipped-ref values must be unique");
@@ -184,7 +218,7 @@ function gitIsAncestor(base, target) {
);
}
function githubApi(args) {
function fetchGithubApi(args) {
try {
return JSON.parse(run("ghx", ["api", ...args]).replace(/\u001B\[[0-?]*[ -/]*[@-~]/g, ""));
} catch (error) {
@@ -195,6 +229,120 @@ function githubApi(args) {
}
}
export function createGithubSnapshotState({
base,
filePath,
refresh = false,
repository = repo,
target,
}) {
let responses = {};
if (!refresh && existsSync(filePath)) {
let parsed;
try {
parsed = JSON.parse(readFileSync(filePath, "utf8"));
} catch (error) {
fail(
`could not read GitHub snapshot ${filePath}: ${
error instanceof Error ? error.message : String(error)
}`,
);
}
if (
parsed.schemaVersion !== githubSnapshotSchemaVersion ||
parsed.repository !== repository ||
parsed.base !== base ||
parsed.target !== target ||
!parsed.responses ||
typeof parsed.responses !== "object" ||
Array.isArray(parsed.responses)
) {
fail(
`GitHub snapshot ${filePath} does not match ${repository} ${base}..${target}; use --refresh-github-snapshot`,
);
}
responses = parsed.responses;
}
return {
base,
dirty: refresh && existsSync(filePath),
filePath,
hits: 0,
misses: 0,
repository,
responses,
target,
};
}
export function githubApiWithSnapshot(args, fetchApi, snapshotState) {
if (!snapshotState || args[0] !== "graphql") {
return fetchApi(args);
}
const key = JSON.stringify(args);
const cached = snapshotState.responses[key];
if (cached !== undefined) {
snapshotState.hits += 1;
return structuredClone(cached);
}
const response = fetchApi(args);
snapshotState.responses[key] = structuredClone(response);
snapshotState.misses += 1;
snapshotState.dirty = true;
return response;
}
export function persistGithubSnapshot(snapshotState) {
if (!snapshotState?.dirty) {
return;
}
const output = `${JSON.stringify(
{
schemaVersion: githubSnapshotSchemaVersion,
repository: snapshotState.repository,
base: snapshotState.base,
target: snapshotState.target,
responses: snapshotState.responses,
},
null,
2,
)}\n`;
mkdirSync(path.dirname(snapshotState.filePath), { recursive: true });
const tempPath = `${snapshotState.filePath}.${process.pid}.tmp`;
try {
writeFileSync(tempPath, output);
renameSync(tempPath, snapshotState.filePath);
snapshotState.dirty = false;
} finally {
rmSync(tempPath, { force: true });
}
}
function githubApi(args) {
return githubApiWithSnapshot(args, fetchGithubApi, githubSnapshotState);
}
function initializeGithubSnapshot(options) {
if (options.noGithubSnapshot) {
return undefined;
}
const base = git(["rev-parse", `${options.base}^{commit}`]);
const target = git(["rev-parse", `${options.target}^{commit}`]);
const defaultName = `verify-release-notes-${base}-${target}.json`;
const filePath = path.resolve(
options.githubSnapshotPath ??
git(["rev-parse", "--git-path", `openclaw-release-cache/${defaultName}`]),
);
const state = createGithubSnapshotState({
base,
filePath,
refresh: options.refreshGithubSnapshot,
target,
});
process.once("exit", () => persistGithubSnapshot(state));
return state;
}
function escapeRegExp(value) {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
@@ -1653,6 +1801,7 @@ function main() {
printUsage();
return;
}
githubSnapshotState = initializeGithubSnapshot(options);
let changelog = readFileSync("CHANGELOG.md", "utf8");
let section = sectionFor(changelog, options.version);
const source = sourceCommits(options.base, options.target);
@@ -1863,13 +2012,24 @@ function main() {
unlinkedCommits: manifest.unlinkedCommits.length,
},
github,
githubSnapshot: githubSnapshotState
? {
path: githubSnapshotState.filePath,
hits: githubSnapshotState.hits,
misses: githubSnapshotState.misses,
}
: null,
errors,
};
persistGithubSnapshot(githubSnapshotState);
if (options.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
} else {
const snapshotSummary = githubSnapshotState
? `, GitHub snapshot ${githubSnapshotState.hits} hits/${githubSnapshotState.misses} misses`
: "";
process.stdout.write(
`${options.version}: ${ledger.pullRequests.length} PRs, ${ledger.issues.length} issues, ${errors.length === 0 ? "verified" : `${errors.length} errors`}\n`,
`${options.version}: ${ledger.pullRequests.length} PRs, ${ledger.issues.length} issues, ${errors.length === 0 ? "verified" : `${errors.length} errors`}${snapshotSummary}\n`,
);
}
if (errors.length > 0) {
+71
View File
@@ -6,8 +6,11 @@ import { describe, expect, it } from "vitest";
import {
contaminatingPullRequestReferences,
countTopLevelSectionBullets,
createGithubSnapshotState,
cumulativeShippedPullRequests,
githubApiWithSnapshot,
highlightCountError,
persistGithubSnapshot,
releaseNoteReferences,
standardRevertedHash,
subtractShippedPullRequests,
@@ -33,6 +36,74 @@ function git(cwd: string, args: string[]): string {
}
describe("release-note verification", () => {
it("reuses exact-range GitHub GraphQL snapshots without caching REST reads", () => {
const cwd = mkdtempSync(join(tmpdir(), "openclaw-release-notes-snapshot-"));
try {
const filePath = join(cwd, "snapshot.json");
let fetches = 0;
const fetchApi = (args: string[]) => {
fetches += 1;
return { request: args, fetches };
};
const first = createGithubSnapshotState({
base: "a".repeat(40),
filePath,
target: "b".repeat(40),
});
expect(githubApiWithSnapshot(["graphql", "-f", "query=one"], fetchApi, first)).toEqual({
request: ["graphql", "-f", "query=one"],
fetches: 1,
});
expect(
githubApiWithSnapshot(["repos/openclaw/openclaw/releases/tags/v1"], fetchApi, first),
).toEqual({
request: ["repos/openclaw/openclaw/releases/tags/v1"],
fetches: 2,
});
persistGithubSnapshot(first);
const second = createGithubSnapshotState({
base: "a".repeat(40),
filePath,
target: "b".repeat(40),
});
expect(githubApiWithSnapshot(["graphql", "-f", "query=one"], fetchApi, second)).toEqual({
request: ["graphql", "-f", "query=one"],
fetches: 1,
});
expect(second.hits).toBe(1);
expect(second.misses).toBe(0);
expect(fetches).toBe(2);
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it("rejects a snapshot bound to a different release target", () => {
const cwd = mkdtempSync(join(tmpdir(), "openclaw-release-notes-snapshot-"));
try {
const filePath = join(cwd, "snapshot.json");
const state = createGithubSnapshotState({
base: "a".repeat(40),
filePath,
target: "b".repeat(40),
});
githubApiWithSnapshot(["graphql", "-f", "query=one"], () => ({ data: true }), state);
persistGithubSnapshot(state);
expect(() =>
createGithubSnapshotState({
base: "a".repeat(40),
filePath,
target: "c".repeat(40),
}),
).toThrow("use --refresh-github-snapshot");
} finally {
rmSync(cwd, { recursive: true, force: true });
}
});
it("ignores nested revert markers in squash-merge bodies", () => {
const nestedRevert = [
"feat(android): render display math (#101435)",