85453da49f
regression / regression-shards (style-16-prod style-9-prod style-17-prod iframe-render-compat variables-prod mp4-h265-sdr, shard-4) (push) Has been cancelled
regression / regression-shards (style-4-prod style-11-prod style-2-prod animejs-adapter typegpu-adapter parallel-capture-regression, shard-5) (push) Has been cancelled
regression / regression-shards (style-7-prod style-8-prod style-10-prod css-spinner-render-compat webm-transparency mp4-h264-sdr webm-vp9, shard-3) (push) Has been cancelled
regression / regression-shards (sub-composition-video style-18-prod raf-ball-render-compat font-variant-numeric sub-comp-t0 sub-comp-id-selector, shard-7) (push) Has been cancelled
Windows render verification / Detect changes (push) Has been cancelled
Windows render verification / Preflight (lint + format) (push) Has been cancelled
Windows render verification / Render on windows-latest (push) Has been cancelled
Windows render verification / Tests on windows-latest (push) Has been cancelled
CI / Detect changes (push) Has been cancelled
CI / Build (push) Has been cancelled
CI / Lint (push) Has been cancelled
CI / Fallow audit (push) Has been cancelled
CI / Format (push) Has been cancelled
CI / Typecheck (push) Has been cancelled
CI / Test (push) Has been cancelled
CI / Producer: integration tests (push) Has been cancelled
CI / Producer: unit tests (push) Has been cancelled
CI / File size check (push) Has been cancelled
CI / Test: skills (push) Has been cancelled
CI / Skills: manifest in sync (push) Has been cancelled
CI / CLI: npx shim (macos-latest) (push) Has been cancelled
CI / CLI: npx shim (ubuntu-latest) (push) Has been cancelled
CI / CLI: npx shim (windows-latest) (push) Has been cancelled
CI / SDK: unit + contract + smoke (push) Has been cancelled
CI / Test: runtime contract (push) Has been cancelled
CI / Studio: load smoke (push) Has been cancelled
CI / Smoke: global install (push) Has been cancelled
CI / CLI smoke (required) (push) Has been cancelled
CI / Semantic PR title (push) Has been cancelled
Player perf / Detect changes (push) Has been cancelled
Player perf / Preflight (lint + format) (push) Has been cancelled
Player perf / player-perf (push) Has been cancelled
Player perf / Perf: drift (push) Has been cancelled
Player perf / Perf: fps (push) Has been cancelled
Player perf / Perf: parity (push) Has been cancelled
Player perf / Perf: scrub (push) Has been cancelled
Player perf / Perf: load (push) Has been cancelled
preview-regression / Detect changes (push) Has been cancelled
preview-regression / Preflight (lint + format) (push) Has been cancelled
preview-regression / Preview parity (push) Has been cancelled
preview-regression / preview-regression (push) Has been cancelled
regression / regression (push) Has been cancelled
regression / Detect changes (push) Has been cancelled
regression / Preflight (lint + format) (push) Has been cancelled
regression / regression-shards (hdr-regression style-5-prod style-3-prod mov-prores, shard-1) (push) Has been cancelled
regression / regression-shards (overlay-montage-prod style-12-prod chat missing-host-comp-id png-sequence portrait-edge-bleed, shard-6) (push) Has been cancelled
regression / regression-shards (style-13-prod style-6-prod vignelli-stacking gsap-letters-render-compat audio-mux-parity, shard-8) (push) Has been cancelled
regression / regression-shards (style-15-prod hdr-hlg-regression style-1-prod many-cuts vfr-screen-recording render-symlinked-assets, shard-2) (push) Has been cancelled
CodeQL / Analyze (actions) (push) Has been cancelled
CodeQL / Analyze (javascript-typescript) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Docs / Validate docs (push) Has been cancelled
Sync skills to ClawHub / Publish changed skills (push) Has been cancelled
235 lines
9.0 KiB
JavaScript
235 lines
9.0 KiB
JavaScript
#!/usr/bin/env node
|
||
// Step 1 — PR fetch (deterministic; runs gh; large-PR-safe; NO scratch dir).
|
||
//
|
||
// Replaces a bare `gh pr view … > capture/pr.json` in the orchestrator. It folds
|
||
// the PR into the two artifacts ingest.mjs consumes:
|
||
// capture/pr.json the gh pr view core (title, body, author, refs, commits,
|
||
// reviews, comments, assignees, +/− stats, …) with its `files`
|
||
// list COMPLETED via a paginated `gh api .../pulls/N/files`
|
||
// call — `gh pr view --json files` truncates at ~100 files, so a
|
||
// big PR would otherwise lose the tail. Commits keep gh pr view's
|
||
// rich `authors[]` (co-authors) — only `files` needs the override.
|
||
// For MERGED PRs it also stamps a best-effort `shipped_version`
|
||
// (+ `version_source`) so the end card / cta doesn't invent one.
|
||
// capture/diff.patch the full unified diff (`gh pr diff`).
|
||
//
|
||
// gh runs HERE so auth / not-found / private-repo errors surface with gh's own stderr
|
||
// and exit 1 (the orchestrator then stops). Intermediates are held in memory — this
|
||
// writes ONLY the two files above, so there is no `_ingest_tmp/` scratch to clean up
|
||
// (the previous "let the agent fetch in pieces" approach polluted videos/ and was
|
||
// non-deterministic). ingest.mjs stays a pure offline transform downstream.
|
||
//
|
||
// Usage:
|
||
// node fetch-pr.mjs --pr "<url | owner/repo#N | N>" [--out-dir ./capture]
|
||
//
|
||
// Exit 0 = capture/pr.json + capture/diff.patch written + summary on stdout.
|
||
// Exit 1 = gh not authenticated / PR not found / pr view failed.
|
||
|
||
import { execFileSync } from "node:child_process";
|
||
import { mkdirSync, writeFileSync } from "node:fs";
|
||
import { join, resolve } from "node:path";
|
||
|
||
const argv = process.argv.slice(2);
|
||
const flag = (name, def) => {
|
||
const i = argv.indexOf(`--${name}`);
|
||
return i >= 0 && i + 1 < argv.length ? argv[i + 1] : def;
|
||
};
|
||
function die(msg) {
|
||
console.error(`✗ fetch-pr.mjs: ${msg}`);
|
||
process.exit(1);
|
||
}
|
||
|
||
const prRef = flag("pr", null);
|
||
if (!prRef) die('--pr "<url | owner/repo#N | N>" is required');
|
||
const outDir = resolve(flag("out-dir", "./capture"));
|
||
|
||
// Run gh, capture stdout. Returns { ok, stdout, stderr } — never throws (callers
|
||
// decide whether a failure is fatal). 64 MB buffer covers large diffs / file lists.
|
||
function ghTry(args) {
|
||
try {
|
||
const stdout = execFileSync("gh", args, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
||
return { ok: true, stdout, stderr: "" };
|
||
} catch (e) {
|
||
return {
|
||
ok: false,
|
||
stdout: (e.stdout || "").toString(),
|
||
stderr: (e.stderr || e.message || "").toString().trim(),
|
||
};
|
||
}
|
||
}
|
||
|
||
// ── 0. auth — fail fast with gh's own hint ───────────────────────────────────
|
||
if (!ghTry(["auth", "status"]).ok) {
|
||
die("gh is not authenticated — run: gh auth login");
|
||
}
|
||
|
||
// ── 1. core PR object (gh pr view) ───────────────────────────────────────────
|
||
const FIELDS = [
|
||
"number",
|
||
"title",
|
||
"body",
|
||
"author",
|
||
"url",
|
||
"baseRefName",
|
||
"headRefName",
|
||
"commits",
|
||
"files",
|
||
"additions",
|
||
"deletions",
|
||
"changedFiles",
|
||
"labels",
|
||
"reviews",
|
||
"latestReviews",
|
||
"comments",
|
||
"assignees",
|
||
"reviewDecision",
|
||
"mergedBy",
|
||
"state",
|
||
"mergedAt",
|
||
].join(",");
|
||
|
||
const view = ghTry(["pr", "view", prRef, "--json", FIELDS]);
|
||
if (!view.ok) die(`gh pr view "${prRef}" failed (auth / not found / private?):\n${view.stderr}`);
|
||
|
||
let pr;
|
||
try {
|
||
pr = JSON.parse(view.stdout);
|
||
} catch (e) {
|
||
die(`gh pr view returned unparseable JSON (${e.message})`);
|
||
}
|
||
|
||
// ── 2. complete the files list via paginated gh api (the truncation fix) ──────
|
||
// gh pr view --json files caps at ~100 files; the REST endpoint paginates with no
|
||
// cap. --jq runs per page, so the output is NDJSON (one file object per line).
|
||
const number = pr.number;
|
||
const m = /github\.com\/([^/]+)\/([^/]+)\/pull\/\d+/.exec(pr.url || "");
|
||
const owner = m?.[1];
|
||
const repo = m?.[2];
|
||
let filesNote = `${Array.isArray(pr.files) ? pr.files.length : 0} (from pr view)`;
|
||
if (owner && repo && number != null) {
|
||
const apiFiles = ghTry([
|
||
"api",
|
||
"--paginate",
|
||
`repos/${owner}/${repo}/pulls/${number}/files`,
|
||
"--jq",
|
||
".[] | {path: .filename, additions, deletions, status}",
|
||
]);
|
||
if (apiFiles.ok) {
|
||
const files = apiFiles.stdout
|
||
.split("\n")
|
||
.filter(Boolean)
|
||
.map((l) => {
|
||
try {
|
||
return JSON.parse(l);
|
||
} catch {
|
||
return null;
|
||
}
|
||
})
|
||
.filter(Boolean);
|
||
if (files.length) {
|
||
pr.files = files;
|
||
if (pr.changedFiles == null || files.length > pr.changedFiles) pr.changedFiles = files.length;
|
||
filesNote = `${files.length} (completed via gh api)`;
|
||
}
|
||
} else {
|
||
console.error(
|
||
` (warn: gh api files failed — keeping pr view's files: ${apiFiles.stderr.split("\n")[0]})`,
|
||
);
|
||
}
|
||
} else {
|
||
console.error(" (warn: could not parse owner/repo from PR url — keeping pr view's files)");
|
||
}
|
||
|
||
// ── 2.5 best-effort shipping version (MERGED PRs only) ───────────────────────
|
||
// The end card / cta ("upgrade to vN", "what's new in vN") wants a real version;
|
||
// a PR carries none, so the agent would otherwise guess. We resolve one here and
|
||
// stamp it onto pr.json as `shipped_version` (+ a `version_source` note that keeps
|
||
// it honest). `git tag --contains` isn't available on a remote-only fetch, so we
|
||
// use gh api proxies: the first release published at/after the merge is the first
|
||
// tag that can contain the merge commit; failing that, the default branch's
|
||
// package manifest version (unreleased); else null. Always best-effort — a lookup
|
||
// failure just leaves the fields null (the skill then falls back to the repo URL).
|
||
pr.shipped_version = null;
|
||
pr.version_source = null;
|
||
if (pr.state === "MERGED") {
|
||
const mergedAt = pr.mergedAt ? Date.parse(pr.mergedAt) : NaN;
|
||
|
||
// (a) earliest non-draft release published on/after the merge.
|
||
if (owner && repo && !Number.isNaN(mergedAt)) {
|
||
const rel = ghTry([
|
||
"api",
|
||
"--paginate",
|
||
`repos/${owner}/${repo}/releases`,
|
||
"--jq",
|
||
".[] | select(.draft == false) | {tag: .tag_name, published: .published_at}",
|
||
]);
|
||
if (rel.ok) {
|
||
let best = null;
|
||
for (const line of rel.stdout.split("\n").filter(Boolean)) {
|
||
let r;
|
||
try {
|
||
r = JSON.parse(line);
|
||
} catch {
|
||
continue;
|
||
}
|
||
if (!r?.tag || !r?.published) continue;
|
||
const t = Date.parse(r.published);
|
||
if (Number.isNaN(t) || t < mergedAt) continue;
|
||
if (!best || t < best.t) best = { tag: r.tag, t };
|
||
}
|
||
if (best) {
|
||
pr.shipped_version = best.tag;
|
||
pr.version_source = "first release published at/after merge";
|
||
}
|
||
} else {
|
||
console.error(` (warn: gh api releases failed: ${rel.stderr.split("\n")[0]})`);
|
||
}
|
||
}
|
||
|
||
// (b) fallback — default branch's package manifest version (change merged but not
|
||
// yet in a tagged release). Marked as unreleased so the skill doesn't present
|
||
// it as a shipped tag.
|
||
if (pr.shipped_version == null && owner && repo) {
|
||
const pkg = ghTry(["api", `repos/${owner}/${repo}/contents/package.json`, "--jq", ".content"]);
|
||
if (pkg.ok && pkg.stdout.trim()) {
|
||
try {
|
||
const manifest = JSON.parse(Buffer.from(pkg.stdout.trim(), "base64").toString("utf8"));
|
||
if (manifest?.version) {
|
||
pr.shipped_version = String(manifest.version);
|
||
pr.version_source = "default-branch package.json (unreleased)";
|
||
}
|
||
} catch {
|
||
/* not JSON / no version — leave null */
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// ── 3. write capture/pr.json + capture/diff.patch ────────────────────────────
|
||
mkdirSync(outDir, { recursive: true });
|
||
const prJsonPath = join(outDir, "pr.json");
|
||
writeFileSync(prJsonPath, JSON.stringify(pr, null, 2) + "\n");
|
||
|
||
const diff = ghTry(["pr", "diff", prRef]);
|
||
const diffPath = join(outDir, "diff.patch");
|
||
if (diff.ok) {
|
||
writeFileSync(diffPath, diff.stdout);
|
||
} else {
|
||
// The brief still builds without the diff (ingest treats it as optional), so this
|
||
// is a warning, not fatal — but surface it.
|
||
console.error(
|
||
` (warn: gh pr diff failed — brief builds without it: ${diff.stderr.split("\n")[0]})`,
|
||
);
|
||
}
|
||
|
||
// ── 4. summary ───────────────────────────────────────────────────────────────
|
||
const repoLabel = owner && repo ? `${owner}/${repo}` : "(repo?)";
|
||
console.log(
|
||
[
|
||
`✓ fetch-pr: ${repoLabel} PR #${number ?? "?"} — "${(pr.title || "").slice(0, 72)}"`,
|
||
` files: ${filesNote}; diff: ${diff.ok ? `${diff.stdout.length} chars` : "MISSING"}`,
|
||
` shipped_version: ${pr.shipped_version ?? "null"}${pr.version_source ? ` (${pr.version_source})` : ""}`,
|
||
` wrote ${prJsonPath}${diff.ok ? ` + ${diffPath}` : ""}`,
|
||
].join("\n"),
|
||
);
|