diff --git a/skills/relay/references/plain-git-publication.md b/skills/relay/references/plain-git-publication.md new file mode 100644 index 00000000..9f1a7cbc --- /dev/null +++ b/skills/relay/references/plain-git-publication.md @@ -0,0 +1,21 @@ +# Plain Git publication + +Publish an already closed Reviewed Result to a plain Git remote. This is not +review, recovery, Change Request creation, or Landing. + +```bash +node "${RELAY_SKILL_ROOT:-skills}/relay/scripts/publish-reviewed-revision.js" \ + --run-dir "$RUN_DIR" \ + --remote "$REMOTE_URL" \ + --ref refs/heads/published \ + --expected-old "$EXPECTED_OLD_OID" \ + --json +``` + +The immutable input is the terminal `reviewed_result_ready` close plus its +bound passing review and verification. The push uses a compare-and-swap +`--force-with-lease=:` update and confirms success with +`git ls-remote`. Retries are idempotent: an already-published OID converges to +`already_published`; concurrent destination movement fails closed without +`--force`. The receipt records remote URL identity, destination ref, expected +old OID, published OID, and the post-push observation. No run fact is appended. diff --git a/skills/relay/scripts/publish-reviewed-revision.js b/skills/relay/scripts/publish-reviewed-revision.js new file mode 100644 index 00000000..e363079d --- /dev/null +++ b/skills/relay/scripts/publish-reviewed-revision.js @@ -0,0 +1,420 @@ +#!/usr/bin/env node +"use strict"; + +/** Plain Git publication of an already reviewed revision. Not recover, merge, or a change request. */ + +const crypto = require("node:crypto"); +const fs = require("node:fs"); +const path = require("node:path"); + +const { execGit } = require("../../relay-dispatch/scripts/exec"); +const { readFacts } = require("../../relay-dispatch/scripts/facts"); +const { readRunRecord } = require("../../relay-dispatch/scripts/run-store"); + +const ZERO_OID = "0".repeat(40); +const SHA1_RE = /^[0-9a-f]{40}$/i; +const DEST_REF_RE = /^refs\/heads\/[A-Za-z0-9][A-Za-z0-9._/-]*$/; +const PASSING_VERDICTS = new Set(["pass", "lgtm"]); +const SUCCESS_OUTCOMES = new Set(["published", "already_published"]); +const FLAGS = [ + "--run-dir", "--repo", "--remote", "--ref", "--expected-old", + "--json", "--help", "-h", +]; +const CLI = { + reservedFlags: FLAGS, + booleanFlags: ["--json", "--help", "-h"], + verbatimValueFlags: ["--run-dir", "--repo", "--remote", "--ref", "--expected-old"], +}; + +function fail(code, message, details = {}) { + const error = new Error(message); + error.code = code; + Object.assign(error, details); + throw error; +} + +function parseCli(argv) { + const known = new Set(FLAGS); + const bool = new Set(CLI.booleanFlags); + const verbatim = new Set(CLI.verbatimValueFlags); + const consumed = new Set(); + const name = (token) => String(token).split("=", 1)[0]; + const accepts = (flag, value) => value !== undefined && ( + verbatim.has(flag) || (!String(value).startsWith("--") && !known.has(String(value))) + ); + argv.forEach((token, index) => { + const flag = name(token); + if (known.has(flag) && !bool.has(flag) && !String(token).includes("=") && accepts(flag, argv[index + 1])) { + consumed.add(index + 1); + } + }); + const unknown = argv.filter((token, index) => ( + !consumed.has(index) && String(token).startsWith("-") && !known.has(name(token)) + )); + if (unknown.length) throw new Error(`unknown flags: ${unknown.join(", ")}`); + return { + hasFlag: (flags) => (Array.isArray(flags) ? flags : [flags]).some((flag) => ( + argv.some((token, index) => !consumed.has(index) && (token === flag || String(token).startsWith(`${flag}=`))) + )), + getArg: (flag, fallback) => { + for (let index = 0; index < argv.length; index += 1) { + if (consumed.has(index)) continue; + const token = String(argv[index]); + if (token === flag || token.startsWith(`${flag}=`)) { + const value = token === flag ? argv[index + 1] : token.slice(flag.length + 1); + if (!accepts(flag, value)) return fallback; + if (verbatim.has(flag) && !String(value).trim()) throw new Error(`${flag} requires a non-empty value`); + return value; + } + } + return fallback; + }, + }; +} + +function usage() { + return [ + "Usage:", + " publish-reviewed-revision.js --run-dir --remote --ref --expected-old [--repo ] [--json]", + "", + "Publishes one already reviewed revision to a plain Git remote with a compare-and-swap ref update.", + "Does not open a change request, authorize landing, or append run facts.", + ].join("\n"); +} + +function normalizeOid(value) { + if (value == null || value === "") return ZERO_OID; + if (typeof value !== "string" || !SHA1_RE.test(value)) { + fail("PUBLICATION_OID_INVALID", `OID must be 40 hex characters: ${JSON.stringify(value)}`); + } + return value.toLowerCase(); +} + +function assertDestinationRef(value) { + if (typeof value !== "string" || !DEST_REF_RE.test(value) || value.includes("..") || value.includes("//")) { + fail("PUBLICATION_REF_INVALID", `destination ref must be a refs/heads/ name: ${JSON.stringify(value)}`); + } + return value; +} + +function assertRemoteUrl(value) { + if (typeof value !== "string" || !value.trim() || value.startsWith("-")) { + fail("PUBLICATION_REMOTE_INVALID", "remote URL identity is required"); + } + return value.trim(); +} + +function fsyncDirectory(directory) { + const fd = fs.openSync(directory, fs.constants.O_RDONLY | (fs.constants.O_DIRECTORY || 0)); + try { fs.fsyncSync(fd); } finally { fs.closeSync(fd); } +} + +function lastMatching(facts, predicate) { + for (let index = facts.length - 1; index >= 0; index -= 1) { + if (predicate(facts[index])) return facts[index]; + } + return null; +} + +function loadReviewedResult(runDir) { + const canonical = fs.realpathSync(runDir); + const record = readRunRecord({ runDir: canonical }); + const { facts } = readFacts({ eventsPath: path.join(canonical, "events.jsonl") }); + const closed = lastMatching(facts, (fact) => ( + fact.type === "run_closed" && fact.payload.reason === "reviewed_result_ready" + )); + if (!closed) { + fail("REVIEWED_RESULT_REQUIRED", "publication requires a terminal reviewed result"); + } + const closedIndex = facts.indexOf(closed); + const preceding = facts.slice(0, closedIndex); + const reviewedSha = normalizeOid(closed.payload.last_sha); + const review = lastMatching(preceding, (fact) => ( + fact.type === "review_recorded" + && PASSING_VERDICTS.has(fact.payload.verdict) + && normalizeOid(fact.payload.reviewed_sha) === reviewedSha + && fact.payload.done_criteria_sha256 === record.contract.done_criteria_sha256 + )); + if (!review) { + fail("REVIEWED_RESULT_REQUIRED", "publication requires a passing review bound to the closed revision"); + } + const verification = lastMatching(preceding, (fact) => ( + fact.type === "verification_recorded" + && fact.payload.status === "passed" + && normalizeOid(fact.payload.head_sha) === reviewedSha + && fact.payload.done_criteria_sha256 === record.contract.done_criteria_sha256 + )); + if (!verification) { + fail("REVIEWED_RESULT_REQUIRED", "publication requires passed verification bound to the closed revision"); + } + return Object.freeze({ + kind: "reviewed_result", + run_id: record.run_id, + run_dir: canonical, + reviewed_sha: reviewedSha, + tree_sha: normalizeOid(verification.payload.tree_sha), + done_criteria_sha256: record.contract.done_criteria_sha256, + close_event_id: closed.event_id, + review_event_id: review.event_id, + verification_event_id: verification.event_id, + source_repo: record.repo.root, + }); +} + +function observeRemoteOid(sourceRepo, remoteUrl, destinationRef) { + let output; + try { + output = execGit(sourceRepo, ["ls-remote", remoteUrl, destinationRef]); + } catch (error) { + fail("PUBLICATION_TRANSPORT_AMBIGUOUS", `remote observation failed: ${error.stderr || error.message}`, { + cause: error, + }); + } + const lines = String(output || "").split("\n").map((line) => line.trim()).filter(Boolean); + for (const line of lines) { + const [oid, ref] = line.split("\t"); + if (ref === destinationRef && SHA1_RE.test(oid || "")) return normalizeOid(oid); + } + return ZERO_OID; +} + +function classifyObservation(observedOid, expectedOldOid, publishedOid) { + const observed = normalizeOid(observedOid); + if (observed === publishedOid) return "already_published"; + if (observed === expectedOldOid) return "awaiting_cas"; + return "conflict"; +} + +function assertCasPushArgv(args) { + for (const arg of args) { + if (arg === "--force" || arg === "-f" || arg === "--force-with-lease") { + fail("PUBLICATION_FORCE_REFUSED", "publication refuses force-push"); + } + } + const lease = args.find((arg) => String(arg).startsWith("--force-with-lease=")); + if (!lease || !lease.includes(":")) { + fail("PUBLICATION_FORCE_REFUSED", "publication requires an expected-old-object lease"); + } +} + +function casPush(sourceRepo, remoteUrl, destinationRef, expectedOldOid, publishedOid) { + const args = [ + "push", + `--force-with-lease=${destinationRef}:${expectedOldOid}`, + remoteUrl, + `${publishedOid}:${destinationRef}`, + ]; + assertCasPushArgv(args); + execGit(sourceRepo, args); +} + +function gitErrorText(error) { + return `${error.stderr || ""}\n${error.message || ""}`; +} + +function isLeaseRejection(error) { + return /stale info|non-fast-forward|failed to push some refs/i.test(gitErrorText(error)); +} + +function receiptIdentity(receipt) { + return { + schema_version: receipt.schema_version, + kind: receipt.kind, + run_id: receipt.run_id, + reviewed_sha: receipt.reviewed_sha, + remote_url: receipt.remote_url, + destination_ref: receipt.destination_ref, + expected_old_oid: receipt.expected_old_oid, + published_oid: receipt.published_oid, + }; +} + +function writeReceipt(filePath, receipt) { + const bytes = Buffer.from(`${JSON.stringify(receipt)}\n`); + const directory = path.dirname(filePath); + fs.mkdirSync(directory, { recursive: true }); + try { + const fd = fs.openSync( + filePath, + fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL | (fs.constants.O_NOFOLLOW || 0), + 0o600, + ); + try { + fs.writeFileSync(fd, bytes); + fs.fsyncSync(fd); + } finally { + fs.closeSync(fd); + } + fsyncDirectory(directory); + return receipt; + } catch (error) { + if (error.code !== "EEXIST") throw error; + const existing = JSON.parse(fs.readFileSync(filePath, "utf8")); + const sameIdentity = JSON.stringify(receiptIdentity(existing)) === JSON.stringify(receiptIdentity(receipt)); + if (sameIdentity && SUCCESS_OUTCOMES.has(existing.outcome) && SUCCESS_OUTCOMES.has(receipt.outcome)) { + return existing; + } + if (JSON.stringify(existing) === JSON.stringify(receipt)) return existing; + fail("PUBLICATION_RECEIPT_CONFLICT", "publication receipt already exists with different bytes"); + } +} + +function requireLocalCommit(sourceRepo, oid) { + try { + execGit(sourceRepo, ["cat-file", "-e", `${oid}^{commit}`]); + } catch (error) { + fail("PUBLICATION_OBJECT_MISSING", `reviewed commit is not in the source repository: ${oid}`, { cause: error }); + } +} + +function receiptPathFor(runDir, remoteUrl, destinationRef, publishedOid) { + const digest = crypto.createHash("sha256") + .update([remoteUrl, destinationRef, publishedOid].join("\n")) + .digest("hex") + .slice(0, 16); + return path.join(runDir, `publication-receipt-${digest}.json`); +} + +function buildReceipt({ reviewedResult, remoteUrl, destinationRef, expectedOldOid, publishedOid, observedOid, outcome }) { + return { + schema_version: 1, + kind: "plain-git-publication", + run_id: reviewedResult.run_id, + reviewed_sha: reviewedResult.reviewed_sha, + remote_url: remoteUrl, + destination_ref: destinationRef, + expected_old_oid: expectedOldOid, + published_oid: publishedOid, + observation: { + method: "ls-remote", + remote_oid: observedOid, + at: new Date().toISOString(), + }, + outcome, + }; +} + +function settle({ reviewedResult, remoteUrl, destinationRef, expectedOldOid, publishedOid, observedOid, outcome }) { + if (observedOid !== publishedOid) { + fail("PUBLICATION_OBSERVATION_MISMATCH", "post-operation remote observation did not confirm the published OID"); + } + const receipt = buildReceipt({ + reviewedResult, remoteUrl, destinationRef, expectedOldOid, publishedOid, observedOid, outcome, + }); + const filePath = receiptPathFor(reviewedResult.run_dir, remoteUrl, destinationRef, publishedOid); + const stored = writeReceipt(filePath, receipt); + return { ...stored, receipt_path: filePath }; +} + +function conflict(observedOid, publishedOid) { + fail("PUBLICATION_CONFLICT", "destination moved concurrently; publication fails closed without force-push", { + observed_oid: observedOid, + published_oid: publishedOid, + }); +} + +function pushWithObserve(sourceRepo, remoteUrl, destinationRef, expectedOldOid, publishedOid) { + try { + casPush(sourceRepo, remoteUrl, destinationRef, expectedOldOid, publishedOid); + } catch (error) { + const observed = observeRemoteOid(sourceRepo, remoteUrl, destinationRef); + const classified = classifyObservation(observed, expectedOldOid, publishedOid); + if (classified === "already_published") return { observed, outcome: "already_published" }; + if (classified === "conflict" || isLeaseRejection(error)) { + conflict(observed, publishedOid); + } + fail("PUBLICATION_TRANSPORT_AMBIGUOUS", `publication transport failed before a confirmed observation: ${gitErrorText(error).trim()}`, { + cause: error, + }); + } + const observed = observeRemoteOid(sourceRepo, remoteUrl, destinationRef); + if (observed === publishedOid) return { observed, outcome: "published" }; + if (classifyObservation(observed, expectedOldOid, publishedOid) === "conflict") { + conflict(observed, publishedOid); + } + fail("PUBLICATION_OBSERVATION_MISMATCH", "post-operation remote observation did not confirm the published OID", { + observed_oid: observed, + published_oid: publishedOid, + }); +} + +function publishReviewedRevision({ + reviewedResult, + remoteUrl, + destinationRef, + expectedOldOid, + sourceRepo, +}) { + if (!reviewedResult || reviewedResult.kind !== "reviewed_result") { + fail("REVIEWED_RESULT_REQUIRED", "a reviewed result is the immutable input to publication"); + } + const remote = assertRemoteUrl(remoteUrl); + const dest = assertDestinationRef(destinationRef); + const expected = normalizeOid(expectedOldOid); + const published = normalizeOid(reviewedResult.reviewed_sha); + const repo = sourceRepo || reviewedResult.source_repo; + if (typeof repo !== "string" || !repo.trim()) { + fail("PUBLICATION_REPO_INVALID", "source repository is required"); + } + requireLocalCommit(repo, published); + + const before = observeRemoteOid(repo, remote, dest); + const classified = classifyObservation(before, expected, published); + if (classified === "already_published") { + return settle({ + reviewedResult, remoteUrl: remote, destinationRef: dest, expectedOldOid: expected, + publishedOid: published, observedOid: before, outcome: "already_published", + }); + } + if (classified === "conflict") conflict(before, published); + + let pushed; + try { + pushed = pushWithObserve(repo, remote, dest, expected, published); + } catch (error) { + if (error.code !== "PUBLICATION_TRANSPORT_AMBIGUOUS") throw error; + pushed = pushWithObserve(repo, remote, dest, expected, published); + } + return settle({ + reviewedResult, remoteUrl: remote, destinationRef: dest, expectedOldOid: expected, + publishedOid: published, observedOid: pushed.observed, outcome: pushed.outcome, + }); +} + +function main(argv = process.argv.slice(2)) { + try { + const args = parseCli(argv); + if (args.hasFlag(["--help", "-h"])) { + console.log(usage()); + return 0; + } + const runDir = args.getArg("--run-dir"); + if (!runDir) throw new Error("--run-dir is required"); + const expectedOld = args.getArg("--expected-old"); + if (expectedOld === undefined) throw new Error("--expected-old is required"); + const result = publishReviewedRevision({ + reviewedResult: loadReviewedResult(path.resolve(runDir)), + remoteUrl: args.getArg("--remote"), + destinationRef: args.getArg("--ref"), + expectedOldOid: expectedOld, + sourceRepo: args.getArg("--repo") ? path.resolve(args.getArg("--repo")) : undefined, + }); + if (args.hasFlag("--json")) console.log(JSON.stringify(result, null, 2)); + else console.log(`${result.outcome} ${result.published_oid} ${result.destination_ref}`); + return 0; + } catch (error) { + console.error(`Error: ${error.message}`); + if (!String(error.message || "").startsWith("unknown flags:")) console.error(usage()); + return 1; + } +} + +if (require.main === module) process.exitCode = main(); + +module.exports = { + ZERO_OID, + classifyObservation, + loadReviewedResult, + main, + publishReviewedRevision, +}; diff --git a/tests/relay/scripts/publish-reviewed-revision.test.js b/tests/relay/scripts/publish-reviewed-revision.test.js new file mode 100644 index 00000000..dfafc296 --- /dev/null +++ b/tests/relay/scripts/publish-reviewed-revision.test.js @@ -0,0 +1,358 @@ +"use strict"; + +const assert = require("node:assert/strict"); +const crypto = require("node:crypto"); +const { execFileSync, spawnSync } = require("node:child_process"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const test = require("node:test"); + +const { createRunRecord } = require("../../../skills/relay-dispatch/scripts/run-store"); +const { + ZERO_OID, + loadReviewedResult, + publishReviewedRevision, +} = require("../../../skills/relay/scripts/publish-reviewed-revision"); + +const ROOT = path.resolve(__dirname, "../../.."); +const SCRIPT = path.join(ROOT, "skills/relay/scripts/publish-reviewed-revision.js"); +const SOURCE = fs.readFileSync(SCRIPT, "utf8"); + +function git(repo, args) { + return execFileSync("git", ["-C", repo, ...args], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] }).trim(); +} + +function sha256(value) { + return crypto.createHash("sha256").update(value).digest("hex"); +} + +function fact(runId, type, payload, eventId = type) { + return { + event_id: eventId, + run_id: runId, + type, + at: "2026-09-14T00:00:00.000Z", + actor: "owner", + payload, + }; +} + +function writeFacts(runDir, facts) { + fs.writeFileSync(path.join(runDir, "events.jsonl"), `${facts.map((entry) => JSON.stringify(entry)).join("\n")}\n`); +} + +function fixture({ closed = true, passingReview = true } = {}) { + const root = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), "relay-plain-git-pub-"))); + const repo = path.join(root, "repo"); + const remote = path.join(root, "remote.git"); + const runId = "issue-1211-plain-git-pub"; + const runDir = path.join(root, runId); + fs.mkdirSync(repo); + fs.mkdirSync(runDir); + execFileSync("git", ["init", "-b", "main", repo], { stdio: "ignore" }); + execFileSync("git", ["init", "--bare", remote], { stdio: "ignore" }); + git(repo, ["config", "user.name", "Publication Test"]); + git(repo, ["config", "user.email", "pub@example.test"]); + fs.writeFileSync(path.join(repo, "README.md"), "base\n"); + git(repo, ["add", "README.md"]); + git(repo, ["commit", "-m", "base"]); + const startSha = git(repo, ["rev-parse", "HEAD"]); + fs.writeFileSync(path.join(repo, "README.md"), "reviewed\n"); + git(repo, ["add", "README.md"]); + git(repo, ["commit", "-m", "reviewed"]); + const reviewedSha = git(repo, ["rev-parse", "HEAD"]); + const treeSha = git(repo, ["rev-parse", "HEAD^{tree}"]); + git(repo, ["branch", "reviewed", reviewedSha]); + git(repo, ["checkout", "-B", "mover", startSha]); + fs.writeFileSync(path.join(repo, "README.md"), "concurrent\n"); + git(repo, ["add", "README.md"]); + git(repo, ["commit", "-m", "concurrent"]); + const concurrentSha = git(repo, ["rev-parse", "HEAD"]); + git(repo, ["checkout", "-B", "work", reviewedSha]); + const criteria = "done\n"; + fs.writeFileSync(path.join(runDir, "done-criteria.md"), criteria); + const criteriaHash = sha256(criteria); + createRunRecord({ + runDir, + record: { + version: 3, + run_id: runId, + repo: { root: repo, remote: "local/repo" }, + git: { branch: "work", base_branch: "main", worktree: repo, start_sha: startSha }, + contract: { + done_criteria_path: path.join(runDir, "done-criteria.md"), + done_criteria_sha256: criteriaHash, + }, + roles: { orchestrator: "codex", executor: "codex", reviewer: "claude" }, + parent: null, + ownership_digest: null, + created_at: "2026-09-14T00:00:00.000Z", + }, + }); + const facts = [ + fact(runId, "verification_recorded", { + head_sha: reviewedSha, + tree_sha: treeSha, + done_criteria_sha256: criteriaHash, + command: "node --test", + verification_request_sha256: criteriaHash, + declared_command_count: 1, + completed_command_count: 1, + result_path: path.join(runDir, "verification.log"), + result_sha256: sha256("ok\n"), + exit_code: 0, + status: "passed", + operator: "owner", + }, "verification-1"), + ]; + if (passingReview) { + facts.push(fact(runId, "review_recorded", { + round: 1, + verdict: "lgtm", + reviewed_sha: reviewedSha, + base_sha: startSha, + done_criteria_sha256: criteriaHash, + reviewer: "claude", + review_artifact: path.join(runDir, "review.json"), + override: null, + }, "review-1")); + } + if (closed) { + facts.push(fact(runId, "run_closed", { + reason: "reviewed_result_ready", + operator: "owner", + last_sha: reviewedSha, + pr_number: null, + }, "close-1")); + } + writeFacts(runDir, facts); + fs.writeFileSync(path.join(runDir, "verification.log"), "ok\n"); + const eventsBefore = fs.readFileSync(path.join(runDir, "events.jsonl")); + const gitLog = path.join(root, "git.log"); + const gitBin = path.join(root, "git-wrapper.js"); + const pushCount = path.join(root, "push-count"); + fs.writeFileSync(pushCount, "0"); + fs.writeFileSync(gitBin, `#!/usr/bin/env node +const fs=require("fs"),{spawnSync}=require("child_process"); +const args=process.argv.slice(2); +fs.appendFileSync(${JSON.stringify(gitLog)}, JSON.stringify(args)+"\\n"); +const push = args.includes("push"); +const countFile=${JSON.stringify(pushCount)}; +if (push) { + const n=Number(fs.readFileSync(countFile,"utf8")||"0"); + fs.writeFileSync(countFile, String(n+1)); + if (process.env.RELAY_PUSH_FAULT==="reject-first" && n===0) { + process.stderr.write("fatal: unable to access remote: Connection timed out\\n"); + process.exit(128); + } +} +const result=spawnSync("git", args, {encoding:"utf8"}); +if (result.stdout) process.stdout.write(result.stdout); +if (result.stderr) process.stderr.write(result.stderr); +if (push && process.env.RELAY_PUSH_FAULT==="hangup-after-apply" && Number(fs.readFileSync(countFile,"utf8"))===1) { + process.stderr.write("fatal: the remote end hung up unexpectedly\\n"); + process.exit(128); +} +process.exit(result.status===null?1:result.status); +`); + fs.chmodSync(gitBin, 0o755); + const ghMarker = path.join(root, "gh-called"); + const gh = path.join(root, "gh-trap.js"); + fs.writeFileSync(gh, `#!/usr/bin/env node\nrequire("fs").writeFileSync(${JSON.stringify(ghMarker)}, "called");\nprocess.exit(91);\n`); + fs.chmodSync(gh, 0o755); + return { + root, repo, remote, runDir, runId, startSha, reviewedSha, concurrentSha, + eventsBefore, gitBin, gitLog, gh, ghMarker, + }; +} + +function restoreEnv(name, previous) { + if (previous === undefined) delete process.env[name]; + else process.env[name] = previous; +} + +function publish(value, overrides = {}) { + const previousGit = process.env.RELAY_GIT_BIN; + const previousGh = process.env.RELAY_GH_BIN; + process.env.RELAY_GIT_BIN = value.gitBin; + process.env.RELAY_GH_BIN = value.gh; + try { + return publishReviewedRevision({ + reviewedResult: loadReviewedResult(value.runDir), + remoteUrl: value.remote, + destinationRef: "refs/heads/published", + expectedOldOid: ZERO_OID, + sourceRepo: value.repo, + ...overrides, + }); + } finally { + restoreEnv("RELAY_GIT_BIN", previousGit); + restoreEnv("RELAY_GH_BIN", previousGh); + } +} + +function remoteOid(value, ref = "refs/heads/published") { + try { + return git(value.remote, ["rev-parse", ref]); + } catch { + return ZERO_OID; + } +} + +function gitArgv(value) { + if (!fs.existsSync(value.gitLog)) return []; + return fs.readFileSync(value.gitLog, "utf8").trim().split("\n").filter(Boolean).map(JSON.parse); +} + +test("#1211 production seam does not synthesize change-request or landing facts", () => { + assert.doesNotMatch(SOURCE, /appendFact/); + assert.doesNotMatch(SOURCE, /execGh/); + assert.doesNotMatch(SOURCE, /merge_recorded/); + assert.doesNotMatch(SOURCE, /pull_request_recorded/); +}); + +test("#1211 a reviewed result is the immutable input; unpublished work is refused", (t) => { + const value = fixture({ closed: false }); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + assert.throws(() => loadReviewedResult(value.runDir), /terminal reviewed result/); + const closed = fixture({ passingReview: false }); + t.after(() => fs.rmSync(closed.root, { recursive: true, force: true })); + assert.throws(() => loadReviewedResult(closed.runDir), /passing review/); +}); + +test("#1211 CAS publish records remote identity, ref, expected old, published OID, and observation", (t) => { + const value = fixture(); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + const result = publish(value); + assert.equal(result.outcome, "published"); + assert.equal(result.kind, "plain-git-publication"); + assert.equal(result.run_id, value.runId); + assert.equal(result.reviewed_sha, value.reviewedSha); + assert.equal(result.remote_url, value.remote); + assert.equal(result.destination_ref, "refs/heads/published"); + assert.equal(result.expected_old_oid, ZERO_OID); + assert.equal(result.published_oid, value.reviewedSha); + assert.equal(result.observation.method, "ls-remote"); + assert.equal(result.observation.remote_oid, value.reviewedSha); + assert.equal(remoteOid(value), value.reviewedSha); + const stored = JSON.parse(fs.readFileSync(result.receipt_path, "utf8")); + assert.equal(stored.published_oid, value.reviewedSha); + assert.equal(stored.observation.remote_oid, value.reviewedSha); + assert.deepEqual(fs.readFileSync(path.join(value.runDir, "events.jsonl")), value.eventsBefore); + assert.equal(fs.existsSync(value.ghMarker), false); + for (const argv of gitArgv(value)) { + assert.equal(argv.includes("--force"), false); + assert.equal(argv.includes("-f"), false); + } + const retry = publish(value); + assert.equal(retry.outcome, "published"); + assert.equal(retry.receipt_path, result.receipt_path); + assert.equal(retry.published_oid, value.reviewedSha); + assert.equal(remoteOid(value), value.reviewedSha); + assert.deepEqual(fs.readFileSync(path.join(value.runDir, "events.jsonl")), value.eventsBefore); + assert.equal(gitArgv(value).filter((argv) => argv.includes("push")).length, 1); +}); + +test("#1211 concurrent destination movement fails closed without force-push", (t) => { + const value = fixture(); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + git(value.repo, ["push", value.remote, `${value.startSha}:refs/heads/published`]); + git(value.repo, ["push", value.remote, `${value.concurrentSha}:refs/heads/published`]); + assert.equal(remoteOid(value), value.concurrentSha); + assert.throws( + () => publish(value, { expectedOldOid: value.startSha }), + (error) => { + assert.equal(error.code, "PUBLICATION_CONFLICT"); + assert.match(error.message, /fails closed without force-push/); + return true; + }, + ); + assert.equal(remoteOid(value), value.concurrentSha); + assert.equal(fs.existsSync(value.ghMarker), false); + assert.deepEqual(fs.readFileSync(path.join(value.runDir, "events.jsonl")), value.eventsBefore); + assert.equal(fs.readdirSync(value.runDir).some((name) => name.startsWith("publication-receipt-")), false); + for (const argv of gitArgv(value)) { + assert.equal(argv.includes("--force"), false); + assert.equal(argv.includes("-f"), false); + const lease = argv.find((arg) => String(arg).startsWith("--force-with-lease=")); + if (argv.includes("push") && lease) assert.match(lease, /:[0-9a-f]{40}$/i); + } +}); + +test("#1211 retry after ambiguous transport failure converges to published", (t) => { + const value = fixture(); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + const previousFault = process.env.RELAY_PUSH_FAULT; + process.env.RELAY_PUSH_FAULT = "reject-first"; + try { + const result = publish(value); + assert.equal(result.outcome, "published"); + assert.equal(result.observation.remote_oid, value.reviewedSha); + assert.equal(remoteOid(value), value.reviewedSha); + } finally { + restoreEnv("RELAY_PUSH_FAULT", previousFault); + } +}); + +test("#1211 retry after ambiguous success converges to already-published without a second update", (t) => { + const value = fixture(); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + const previousFault = process.env.RELAY_PUSH_FAULT; + process.env.RELAY_PUSH_FAULT = "hangup-after-apply"; + try { + const result = publish(value); + assert.equal(result.outcome, "already_published"); + assert.equal(result.published_oid, value.reviewedSha); + assert.equal(result.observation.remote_oid, value.reviewedSha); + assert.equal(remoteOid(value), value.reviewedSha); + assert.equal(gitArgv(value).filter((argv) => argv.includes("push")).length, 1); + } finally { + restoreEnv("RELAY_PUSH_FAULT", previousFault); + } +}); + +test("#1211 CLI publishes from a closed run and rejects unknown flags", (t) => { + const value = fixture(); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + const unknown = spawnSync(process.execPath, [SCRIPT, "--run-dir", value.runDir, "--no-merge"], { encoding: "utf8" }); + assert.notEqual(unknown.status, 0); + assert.match(unknown.stderr, /unknown flags/); + assert.match(unknown.stderr, /--no-merge/); + const result = spawnSync(process.execPath, [ + SCRIPT, "--run-dir", value.runDir, "--remote", value.remote, + "--ref", "refs/heads/published", "--expected-old", ZERO_OID, "--json", + ], { + encoding: "utf8", + env: { ...process.env, RELAY_GIT_BIN: value.gitBin, RELAY_GH_BIN: value.gh }, + }); + assert.equal(result.status, 0, result.stderr); + const payload = JSON.parse(result.stdout); + assert.equal(payload.outcome, "published"); + assert.equal(payload.published_oid, value.reviewedSha); + assert.equal(remoteOid(value), value.reviewedSha); + assert.equal(fs.existsSync(value.ghMarker), false); + assert.deepEqual(fs.readFileSync(path.join(value.runDir, "events.jsonl")), value.eventsBefore); +}); + +test("#1211 publication binds only review facts preceding the terminal close", (t) => { + const value = fixture({ closed: true, passingReview: false }); + t.after(() => fs.rmSync(value.root, { recursive: true, force: true })); + const events = fs.readFileSync(path.join(value.runDir, "events.jsonl"), "utf8") + .trim() + .split("\n") + .map(JSON.parse); + const record = JSON.parse(fs.readFileSync(path.join(value.runDir, "run.json"), "utf8")); + events.push(fact(value.runId, "review_recorded", { + round: 1, + verdict: "lgtm", + reviewed_sha: value.reviewedSha, + base_sha: value.startSha, + done_criteria_sha256: record.contract.done_criteria_sha256, + reviewer: "claude", + review_artifact: path.join(value.runDir, "review.json"), + override: null, + }, "review-after-close")); + writeFacts(value.runDir, events); + assert.throws(() => loadReviewedResult(value.runDir), { code: "REVIEWED_RESULT_REQUIRED" }); +});