Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions .github/scripts/check-source-stack.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
#!/usr/bin/env bash
# Verify that a manually supplied nightly source contains every fork patch
# present on main. This catches accidentally stale stacks; it is not a
# security boundary or a net-tree comparison (a later revert can still retain
# an earlier commit's patch-id). The refs must already exist locally, and the
# fork's squash-merged patch stack is expected to have linear history.
set -euo pipefail

main_ref="${1:?main ref is required}"
candidate_ref="${2:?candidate ref is required}"
upstream_ref="${3:?upstream ref is required}"
allow_missing="${4:-false}"

for ref in "$main_ref" "$candidate_ref" "$upstream_ref"; do
git rev-parse --verify "${ref}^{commit}" >/dev/null
done

cherry_output=$(git cherry "$candidate_ref" "$main_ref" "$upstream_ref")

missing_commits=()
while read -r status commit; do
if [[ "$status" == "+" ]]; then
missing_commits+=("$commit")
fi
done <<< "$cherry_output"

if (( ${#missing_commits[@]} == 0 )); then
echo "source_ref contains every patch currently carried by main."
exit 0
fi

{
echo "source_ref is missing patch-id-equivalent commits currently carried by main:"
echo
for commit in "${missing_commits[@]}"; do
printf ' %s %s\n' \
"$(git rev-parse --short=12 "$commit")" \
"$(git show -s --format=%s "$commit")"
done
echo
} >&2

if [[ "$allow_missing" == "true" ]]; then
echo "Conflict resolution may intentionally reshape patches; continuing after the explicit override." >&2
echo "::warning title=Missing main patches explicitly allowed::Continuing because allow_missing_main_patches was enabled." >&2
exit 0
fi

echo "Refresh the resolution from current main before dispatching it." >&2
echo "If conflict resolution intentionally reshaped these patches, re-dispatch" >&2
echo "with allow_missing_main_patches enabled after reviewing this list." >&2
echo "::error title=source_ref is missing patches from main::Refresh the stack or explicitly allow the reported patch-id differences." >&2
exit 1
25 changes: 25 additions & 0 deletions .github/workflows/fork-nightly.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ on:
required: false
type: string
default: ""
allow_missing_main_patches:
description: >-
Allow source_ref when patch-id checks report main patches missing;
use only after reviewing intentionally reshaped conflict resolutions
required: false
type: boolean
default: false

permissions:
contents: write
Expand Down Expand Up @@ -93,6 +100,7 @@ jobs:
name: Rebase candidate onto upstream
shell: bash
env:
ALLOW_MISSING_MAIN_PATCHES: ${{ inputs.allow_missing_main_patches == true }}
DRY_RUN: ${{ inputs.dry_run == true }}
SOURCE_REF: ${{ inputs.source_ref }}
run: |
Expand All @@ -109,6 +117,23 @@ jobs:
echo "origin has no main branch to promote onto." >&2
exit 1
fi

# Fetch the immutable main snapshot and current upstream before
# rebasing. The guard itself is loaded from main so a stale
# source_ref cannot omit or roll back the check it must pass.
git fetch --no-tags origin "$main_ref"
git fetch --no-tags https://github.com/pingdotgg/t3code.git main
guard_upstream_ref=$(git rev-parse FETCH_HEAD)
guard_script="$RUNNER_TEMP/check-source-stack.sh"
if ! git show "${main_ref}:.github/scripts/check-source-stack.sh" > "$guard_script"; then
echo "main snapshot ${main_ref} does not contain the source_ref patch guard; update main before dispatching a source_ref run." >&2
exit 1
fi
bash "$guard_script" \
"$main_ref" \
"$fork_ref" \
"$guard_upstream_ref" \
"$ALLOW_MISSING_MAIN_PATCHES"
else
main_ref=$fork_ref
fi
Expand Down
7 changes: 6 additions & 1 deletion docs/operations/fork-nightly.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,12 @@ publishes a GitHub prerelease.
when it did), verifies it, publishes the release, and promotes it to `main` through the same backup
and lease mechanics as an automated run. The ruleset blocks force-pushing `main` from the CLI, so
this dispatch is how a resolved stack reaches `main`. Pair `source_ref` with `dry_run` first to
verify a resolution without publishing or promoting anything.
verify a resolution without publishing or promoting anything. Prepare compares the supplied
stack's patch IDs with every fork patch currently carried by `main` and fails with the missing
commit subjects before rebasing or verifying a stale stack. Refresh the resolution from current
`main` immediately before dispatching it. Conflict resolution can intentionally reshape a patch
enough to change its patch ID; after reviewing every reported commit, set
`allow_missing_main_patches` to acknowledge those differences explicitly.

## Fork features summary

Expand Down
221 changes: 221 additions & 0 deletions scripts/check-source-stack.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
// @effect-diagnostics nodeBuiltinImport:off
import * as NodeChildProcess from "node:child_process";
import * as NodeFS from "node:fs";
import * as NodeOS from "node:os";
import * as NodePath from "node:path";
import * as NodeURL from "node:url";
import { assert, describe, it } from "@effect/vitest";

const repoRoot = NodePath.resolve(NodePath.dirname(NodeURL.fileURLToPath(import.meta.url)), "..");
const helperPath = NodePath.resolve(repoRoot, ".github/scripts/check-source-stack.sh");

function runGit(cwd: string, ...args: ReadonlyArray<string>): string {
const result = NodeChildProcess.spawnSync("git", args, { cwd, encoding: "utf8" });
if (result.error) throw result.error;
if (result.status !== 0) {
throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
}
return result.stdout.trim();
}

function commitFile(cwd: string, fileName: string, contents: string, subject: string): string {
NodeFS.writeFileSync(NodePath.join(cwd, fileName), contents);
runGit(cwd, "add", fileName);
runGit(cwd, "commit", "-m", subject);
return runGit(cwd, "rev-parse", "HEAD");
}

function checkStack(
cwd: string,
mainRef: string,
candidateRef: string,
upstreamRef: string,
allowMissing = false,
env: NodeJS.ProcessEnv = process.env,
): NodeChildProcess.SpawnSyncReturns<string> {
return NodeChildProcess.spawnSync(
"bash",
[helperPath, mainRef, candidateRef, upstreamRef, allowMissing ? "true" : "false"],
{ cwd, encoding: "utf8", env },
);
}

function withRepository(run: (fixtureRoot: string, upstreamRef: string) => void): void {
const fixtureRoot = NodeFS.mkdtempSync(NodePath.join(NodeOS.tmpdir(), "t3-source-stack-"));

try {
runGit(fixtureRoot, "init");
runGit(fixtureRoot, "symbolic-ref", "HEAD", "refs/heads/main");
runGit(fixtureRoot, "config", "user.name", "Source Stack Test");
runGit(fixtureRoot, "config", "user.email", "source-stack@example.com");
const upstreamRef = commitFile(fixtureRoot, "base.txt", "base\n", "feat: upstream base");
run(fixtureRoot, upstreamRef);
} finally {
NodeFS.rmSync(fixtureRoot, { recursive: true, force: true });
}
}

describe("nightly source stack guard", () => {
it("accepts a candidate with patch-id-equivalent main commits", () => {
withRepository((fixtureRoot, upstreamRef) => {
commitFile(fixtureRoot, "one.txt", "one\n", "feat: first fork patch");
commitFile(fixtureRoot, "two.txt", "two\n", "fix: second fork patch");

runGit(fixtureRoot, "switch", "-c", "candidate", upstreamRef);
runGit(fixtureRoot, "cherry-pick", "main~1", "main");

const result = checkStack(fixtureRoot, "main", "candidate", upstreamRef);

assert.equal(result.status, 0, result.stderr);
assert.include(result.stdout, "contains every patch currently carried by main");
});
});

it("does not require upstream-only commits to be present in the candidate", () => {
withRepository((fixtureRoot, originalUpstreamRef) => {
runGit(fixtureRoot, "switch", "-c", "candidate");
const candidatePatch = commitFile(
fixtureRoot,
"fork.txt",
"fork\n",
"feat: retained fork patch",
);

runGit(fixtureRoot, "switch", "-c", "upstream-next", originalUpstreamRef);
const currentUpstreamRef = commitFile(
fixtureRoot,
"upstream.txt",
"upstream\n",
"feat: newer upstream change",
);

runGit(fixtureRoot, "switch", "main");
runGit(fixtureRoot, "merge", "--ff-only", "upstream-next");
runGit(fixtureRoot, "cherry-pick", candidatePatch);

const result = checkStack(fixtureRoot, "main", "candidate", currentUpstreamRef);

assert.equal(result.status, 0, result.stderr);
});
});

it("accepts a main patch that has since been absorbed by upstream", () => {
withRepository((fixtureRoot, originalUpstreamRef) => {
const mainPatch = commitFile(
fixtureRoot,
"feature.txt",
"fork feature\n",
"feat: fork patch accepted upstream",
);

runGit(fixtureRoot, "switch", "-c", "upstream-next", originalUpstreamRef);
runGit(fixtureRoot, "cherry-pick", mainPatch);
runGit(fixtureRoot, "commit", "--amend", "-m", "feat: upstream version of fork patch");
const currentUpstreamRef = runGit(fixtureRoot, "rev-parse", "HEAD");

const result = checkStack(fixtureRoot, "main", "upstream-next", currentUpstreamRef);

assert.equal(result.status, 0, result.stderr);
});
});

it("accepts equivalent patches applied on top of a newer upstream base", () => {
withRepository((fixtureRoot, originalUpstreamRef) => {
const mainPatch = commitFile(
fixtureRoot,
"fork.txt",
"fork\n",
"feat: fork patch rebased after upstream",
);

runGit(fixtureRoot, "switch", "-c", "upstream-next", originalUpstreamRef);
const currentUpstreamRef = commitFile(
fixtureRoot,
"upstream.txt",
"new upstream\n",
"feat: newer upstream base",
);
runGit(fixtureRoot, "switch", "-c", "candidate");
runGit(fixtureRoot, "cherry-pick", mainPatch);

const result = checkStack(fixtureRoot, "main", "candidate", currentUpstreamRef);

assert.equal(result.status, 0, result.stderr);
});
});

it("reports main patches when the candidate has no fork commits", () => {
withRepository((fixtureRoot, upstreamRef) => {
commitFile(fixtureRoot, "missing.txt", "missing\n", "fix: absent from empty candidate");
runGit(fixtureRoot, "branch", "candidate", upstreamRef);

const result = checkStack(fixtureRoot, "main", "candidate", upstreamRef);

assert.equal(result.status, 1);
assert.include(result.stderr, "fix: absent from empty candidate");
});
});

it("fails closed when git cherry fails", () => {
withRepository((fixtureRoot, upstreamRef) => {
const wrapperDirectory = NodePath.join(fixtureRoot, "bin");
const gitWrapper = NodePath.join(wrapperDirectory, "git");
NodeFS.mkdirSync(wrapperDirectory);
NodeFS.writeFileSync(
gitWrapper,
`#!/bin/sh
if [ "$1" = "cherry" ]; then
exit 42
fi
PATH="$ORIGINAL_PATH" exec git "$@"
`,
);
NodeFS.chmodSync(gitWrapper, 0o755);

const result = checkStack(fixtureRoot, "main", "main", upstreamRef, false, {
...process.env,
ORIGINAL_PATH: process.env.PATH,
PATH: `${wrapperDirectory}:${process.env.PATH}`,
});

assert.equal(result.status, 42);
assert.notInclude(result.stdout, "contains every patch currently carried by main");
});
});

it("fails a stale candidate and lists each missing main commit by subject", () => {
withRepository((fixtureRoot, upstreamRef) => {
runGit(fixtureRoot, "switch", "-c", "candidate");
commitFile(fixtureRoot, "one.txt", "one\n", "feat: retained fork patch");

runGit(fixtureRoot, "switch", "main");
runGit(fixtureRoot, "merge", "--ff-only", "candidate");
commitFile(fixtureRoot, "two.txt", "two\n", "fix: dropped workflow guard (#91)");
commitFile(fixtureRoot, "three.txt", "three\n", "feat: dropped follow-up (#92)");

const result = checkStack(fixtureRoot, "main", "candidate", upstreamRef);

assert.equal(result.status, 1);
assert.include(result.stderr, "fix: dropped workflow guard (#91)");
assert.include(result.stderr, "feat: dropped follow-up (#92)");
assert.include(result.stderr, "Refresh the resolution from current main");
});
});

it("requires an explicit override for intentionally reshaped patches", () => {
withRepository((fixtureRoot, upstreamRef) => {
commitFile(fixtureRoot, "feature.txt", "original resolution\n", "feat: resolved feature");

runGit(fixtureRoot, "switch", "-c", "candidate", upstreamRef);
commitFile(fixtureRoot, "feature.txt", "reshaped resolution\n", "feat: resolved feature");

const rejected = checkStack(fixtureRoot, "main", "candidate", upstreamRef);
const allowed = checkStack(fixtureRoot, "main", "candidate", upstreamRef, true);

assert.equal(rejected.status, 1);
assert.equal(allowed.status, 0, allowed.stderr);
assert.include(allowed.stderr, "feat: resolved feature");
assert.include(allowed.stderr, "Missing main patches explicitly allowed");
});
});
});
Loading