Skip to content
8 changes: 5 additions & 3 deletions docs/fork-governance.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,21 @@ This repository is the public `ZGEnergy/paseo` fork. Keep the fork's `main` bran
- `main` tracks `getpaseo/paseo:main`. Do not add fork-only commits to it.
- `internal/main` receives reviewed upstream imports and internal features.
- The scheduled or manually dispatched **Upstream sync** workflow fetches upstream `main`, fast-forwards fork `main` only when fork `main` is an ancestor, then opens or updates a `main` → `internal/main` pull request when `main` contains commits absent from `internal/main`. If there are no such commits, it exits successfully without creating or editing a pull request.
- When the sync pull request conflicts, do not resolve it in the GitHub web editor. That commits to `main`, which breaks the mirror and fails the next sync's ancestor check. Branch from `internal/main`, merge `main` into it, resolve there, and open a `Downstream sync: true` pull request. Commit the conflict resolution and nothing else: a daily sync diff runs to a few hundred files, so an unrelated edit buried in the merge will not be caught by reading it.
- GitHub Actions scheduled workflows run from the repository default branch. Set the repository default branch to `internal/main`; the workflow asserts this configuration before any write.

## Provenance and review

Internal pull requests targeting `internal/main` use one of three mutually exclusive modes:
Internal pull requests targeting `internal/main` use one of four mutually exclusive modes:

- **Upstream import** (default): identifies its upstream issue, pull request, head repository, and upstream pull-request head SHA. The **Upstream provenance** check verifies that metadata against the public upstream API, confirms the base repository, actual head repository, and SHA, and compares every upstream and fork pull-request patch ID in order (direct imports), or validates the `[upstreamHead, forkMain]` reconciliation merge commit (reconciled imports, with `Fork main:` and `Reconciliation merge:` metadata).
- **Downstream governance**: uses the exact body marker `Downstream governance: true` and is restricted to the governance allowlist below. It does not assert upstream patch equivalence.
- **Downstream feature**: uses exactly `Downstream feature: true` plus one non-empty `Downstream rationale:` line. It is fork-only and cannot change governance-allowlisted files or any `.github/workflows/` file.
- **Downstream sync**: uses exactly `Downstream sync: true`. It carries the resolution of a `main` → `internal/main` sync that conflicts and so cannot be merged as-is. The checker requires the pull-request head to be a two-parent merge commit whose second parent is the live `refs/heads/main` SHA and whose first parent is an ancestor of `internal/main`. No path allowlist applies, and the merge shape constrains the commit topology rather than the tree — a merge commit can carry any content as "conflict resolution" — so nothing mechanical stops an unrelated edit riding along in this mode. Keep such work in a separate pull request under a mode that does restrict paths.

Feature and governance markers cannot coexist with each other or with upstream-provenance metadata.
Feature, governance, and sync markers cannot coexist with each other or with upstream-provenance metadata.

None of the three modes requires a pull-request review. This is a single-maintainer fork, and GitHub refuses to let an author approve their own pull request, so any review requirement here would be permanently unsatisfiable. Provenance is established mechanically — from the labeled metadata fields and, for upstream imports, patch-ID or reconciliation-merge verification — not from a second human's sign-off.
None of the four modes requires a pull-request review. This is a single-maintainer fork, and GitHub refuses to let an author approve their own pull request, so any review requirement here would be permanently unsatisfiable. Provenance is established mechanically — from the labeled metadata fields and, for upstream imports, patch-ID or reconciliation-merge verification — not from a second human's sign-off.

If an upstream import is stale, the final provenance run fails and the bot does not merge it. Update its metadata or reconcile against the new upstream head, then rerun checks. Never merge an import whose upstream head changed after review.

Expand Down
96 changes: 93 additions & 3 deletions scripts/check-upstream-provenance.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,17 @@ function downstreamGovernanceMarker(body) {
return true;
}

function downstreamSyncMarker(body) {
const markers = markerLines(body, "Downstream sync");
if (!markers.length) return false;
if (markers.length !== 1 || markers[0] !== "Downstream sync: true") {
throw new Error(
"Pull request body must contain exactly `Downstream sync: true` when using downstream sync mode",
);
}
return true;
}

function downstreamFeatureMarker(body) {
const markers = markerLines(body, "Downstream feature");
const rationales = markerLines(body, "Downstream rationale");
Expand All @@ -115,6 +126,27 @@ function downstreamFeatureMarker(body) {
return { rationale };
}

function assertAncestor(status, description) {
if (status !== "ahead" && status !== "identical") {
throw new Error(
`Downstream sync ${description} (comparison status ${status ?? "unavailable"})`,
);
}
}

function assertSyncMergeShape(parents, forkMain) {
if (parents.length !== 2) {
throw new Error(
`Downstream sync head must be a merge commit with exactly two parents (found ${parents.length})`,
);
}
if (parents[1] !== forkMain) {
throw new Error(
`Downstream sync head second parent must be fork main ${forkMain} (found ${parents[1]})`,
);
}
}

function validateChangedFileCount(files, expectedCount) {
if (!Number.isInteger(expectedCount) || files.length !== expectedCount) {
throw new Error(
Expand All @@ -131,21 +163,26 @@ function metadataLine(body, label) {
function exceptionMode(body) {
const governance = downstreamGovernanceMarker(body);
const feature = downstreamFeatureMarker(body);
const sync = downstreamSyncMarker(body);
const provenanceLabels = UPSTREAM_PROVENANCE_LABELS.filter((label) => metadataLine(body, label));
if (governance && feature) {
throw new Error("Downstream feature and downstream governance modes are mutually exclusive");
if ([governance, Boolean(feature), sync].filter(Boolean).length > 1) {
throw new Error(
"Downstream feature, downstream governance, and downstream sync modes are mutually exclusive",
);
}
if ((governance || feature) && provenanceLabels.length) {
if ((governance || feature || sync) && provenanceLabels.length) {
throw new Error(
`Downstream exception mode cannot include upstream provenance metadata: ${provenanceLabels.join(", ")}`,
);
}
if (feature) return { mode: "downstream-feature", rationale: feature.rationale };
if (governance) return { mode: "downstream-governance" };
if (sync) return { mode: "downstream-sync" };
return { mode: "upstream-import" };
}

function validateChangedPaths(paths, mode) {
if (mode === "downstream-sync") return paths;
for (const path of paths) {
if (
mode === "downstream-governance"
Expand Down Expand Up @@ -196,6 +233,34 @@ function exceptionEvidence(repository, pullRequest, mode, rationale, scope) {
};
}

function downstreamSyncEvidence(current, repository, pullRequest) {
const forkMain = shaFrom(
ghJson(`repos/${repository}/git/ref/heads/main`).object?.sha ?? "",
"Fork main",
);
const scope = changedFiles(current, repository, pullRequest, "downstream-sync");
const head = ghJson(`repos/${repository}/commits/${scope.currentHead}`);
const mergeParents = Array.isArray(head.parents)
? head.parents.map((parent) => shaFrom(parent?.sha ?? "", "Downstream sync merge parent"))
: [];
assertSyncMergeShape(mergeParents, forkMain);
assertAncestor(
ghJson(`repos/${repository}/compare/${mergeParents[0]}...internal/main`).status,
"first parent must be an ancestor of internal/main",
);
return {
repository,
pullRequest,
mode: "downstream-sync",
exception: "downstream-sync",
forkMain,
mergeParents,
scope: { changedFiles: scope.changedFiles },
currentHead: scope.currentHead,
result: "sync-exception",
};
}

function downstreamExceptionEvidence(current, repository, pullRequest, mode, rationale) {
const scope = changedFiles(current, repository, pullRequest, mode);
return exceptionEvidence(repository, pullRequest, mode, rationale, scope);
Expand Down Expand Up @@ -340,6 +405,20 @@ function writeEvidence(path, evidence) {
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`);
return;
}
if (evidence.mode === "downstream-sync") {
const lines = [
"## Downstream sync exception evidence",
"",
`- Fork main: \`${evidence.forkMain}\``,
`- Merge parents (ordered): \`${evidence.mergeParents.join(", ")}\``,
`- Changed files: \`${evidence.scope.changedFiles.join(", ")}\``,
`- Current pull request head: \`${evidence.currentHead}\``,
"- Result: **downstream sync exception accepted; no upstream patch equivalence asserted**",
"",
];
appendFileSync(process.env.GITHUB_STEP_SUMMARY, `${lines.join("\n")}\n`);
return;
}
if (evidence.mode === "downstream-governance") {
const lines = [
"## Downstream governance exception evidence",
Expand Down Expand Up @@ -604,6 +683,14 @@ function run() {

const body = current.body ?? "";
const selectedMode = exceptionMode(body);
if (selectedMode.mode === "downstream-sync") {
const evidence = downstreamSyncEvidence(current, repository, currentPullRequest);
writeEvidence(evidencePath, evidence);
console.log(
`Downstream sync exception verified: ${repository}#${currentPullRequest} at ${evidence.currentHead} contains fork main ${evidence.forkMain}`,
);
return;
}
if (selectedMode.mode !== "upstream-import") {
const evidence = downstreamExceptionEvidence(
current,
Expand Down Expand Up @@ -639,11 +726,14 @@ function run() {
if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) run();

export {
assertAncestor,
assertSyncMergeShape,
DOWNSTREAM_GOVERNANCE_PATHS,
downstreamFeatureMarker,
downstreamGovernanceMarker,
exceptionEvidence,
exceptionMode,
validateChangedFileCount,
validateChangedPaths,
writeEvidence,
};
121 changes: 121 additions & 0 deletions scripts/check-upstream-provenance.test.mjs
Original file line number Diff line number Diff line change
@@ -1,15 +1,22 @@
import assert from "node:assert/strict";
import { mkdtempSync, readFileSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import test from "node:test";

import {
assertAncestor,
assertSyncMergeShape,
downstreamFeatureMarker,
exceptionEvidence,
exceptionMode,
validateChangedFileCount,
validateChangedPaths,
writeEvidence,
} from "./check-upstream-provenance.mjs";

const currentHead = "a".repeat(40);
const otherHead = "b".repeat(40);

test("accepts exact downstream-feature marker and rationale", () => {
assert.deepEqual(
Expand Down Expand Up @@ -99,3 +106,117 @@ test("feature evidence does not require or store review approval", () => {
assert.equal("approval" in evidence, false);
assert.equal(evidence.currentHead, currentHead);
});

test("accepts exact downstream-sync marker", () => {
assert.deepEqual(exceptionMode("Downstream sync: true"), { mode: "downstream-sync" });
});

test("rejects invalid downstream-sync markers", () => {
for (const body of [
"Downstream sync: false",
"Downstream sync: true\nDownstream sync: true",
" Downstream sync: true",
"downstream sync : true",
]) {
assert.throws(() => exceptionMode(body), /Downstream sync/);
}
});

test("rejects downstream-sync combined with other exception modes", () => {
assert.throws(
() => exceptionMode("Downstream sync: true\nDownstream governance: true"),
/mutually exclusive/,
);
assert.throws(
() => exceptionMode("Downstream sync: true\nDownstream feature: true\nDownstream rationale: r"),
/mutually exclusive/,
);
assert.throws(
() => exceptionMode("Downstream sync: true\nUpstream issue: 1"),
/upstream provenance metadata/,
);
});

test("allows governance and workflow paths in sync mode", () => {
const paths = [
".github/workflows/ci.yml",
"scripts/check-upstream-provenance.mjs",
"packages/example.ts",
];
assert.deepEqual(validateChangedPaths(paths, "downstream-sync"), paths);
});

test("sync mode requires a two-parent merge whose second parent is fork main", () => {
assert.doesNotThrow(() => assertSyncMergeShape([otherHead, currentHead], currentHead));
for (const parents of [[], [otherHead], [otherHead, currentHead, otherHead]]) {
assert.throws(() => assertSyncMergeShape(parents, currentHead), /exactly two parents/);
}
assert.throws(
() => assertSyncMergeShape([currentHead, otherHead], currentHead),
/second parent must be fork main/,
);
});

test("ancestor assertion accepts only ahead or identical comparisons", () => {
const description = "first parent must be an ancestor of internal/main";
for (const status of ["ahead", "identical"]) {
assert.doesNotThrow(() => assertAncestor(status, description));
}
for (const status of ["behind", "diverged", undefined]) {
assert.throws(() => assertAncestor(status, description), /first parent must be an ancestor/);
}
});

test("renders a step summary for every mode without throwing", () => {
const directory = mkdtempSync(join(tmpdir(), "provenance-summary-"));
const summary = join(directory, "summary.md");
const previous = process.env.GITHUB_STEP_SUMMARY;
process.env.GITHUB_STEP_SUMMARY = summary;
const evidenceByMode = {
"downstream-sync": {
forkMain: otherHead,
mergeParents: [currentHead, otherHead],
currentHead,
scope: { changedFiles: ["packages/example.ts"] },
result: "sync-exception",
},
"downstream-governance": {
currentHead,
scope: { changedFiles: ["docs/fork-governance.md"] },
result: "governance-exception",
},
"downstream-feature": {
currentHead,
rationale: "reason",
scope: { changedFiles: ["packages/example.ts"] },
result: "feature-exception",
},
direct: {
upstreamRepository: "getpaseo/paseo",
upstreamIssue: 1,
upstreamPullRequest: 2,
upstreamHeadRepository: "getpaseo/paseo",
upstreamHead: currentHead,
forkPatchIds: ["c".repeat(40)],
upstreamPatchIds: ["c".repeat(40)],
result: "equivalent",
},
};
try {
for (const [mode, evidence] of Object.entries(evidenceByMode)) {
writeEvidence(join(directory, `${mode}.json`), {
repository: "fork/project",
pullRequest: 7,
mode,
...evidence,
});
}
const rendered = readFileSync(summary, "utf8");
assert.match(rendered, /Downstream sync exception evidence/);
assert.match(rendered, new RegExp(`Fork main: \`${otherHead}\``));
} finally {
if (previous === undefined) delete process.env.GITHUB_STEP_SUMMARY;
else process.env.GITHUB_STEP_SUMMARY = previous;
rmSync(directory, { recursive: true, force: true });
}
});
Loading