git checkout -b "$PRBRANCH" "$FEATUREBRANCH"
A single reproducible script implementing the exact workflow (create clean PR head branch → --cached-only removal with explicit paths → co-author via -F → diff-scope guard → push → gh pr create):
#!/usr/bin/env bash
set -euo pipefail
UPSTREAM_REMOTE="upstream" # fork's remote pointing at the upstream repo
BASE="master"
PR_BRANCH="federation-pr"
FEATURE_BRANCH="feature" # your fork's branch containing 8 internal files + N feature files
OWNER="your-github-user" # fork owner (your side of --head owner:branch)
EXPECTED_FEATURE_COUNT=12 # verified feature-file count, asserted exactly
INTERNAL_FILES=( # the 8 internal foreman files — EXPLICIT list, never -r
internal/board/board1.json
internal/board/board2.json
internal/board/board3.json
internal/board/board4.json
internal/gitreins/gitrein1.yml
internal/gitreins/gitrein2.yml
internal/gitreins/gitrein3.yml
internal/gitreins/gitrein4.yml
)
# 1. Dedicated PR head branch from the feature HEAD (leaves feature branch untouched)
git fetch "$UPSTREAM_REMOTE" "$BASE"
git checkout -b "$PR_BRANCH" "$FEATURE_BRANCH"
# 2. Untrack the 8 internal files — git rm --cached with explicit paths ONLY.
# Do NOT use -r: the recursive-delete security filter blocks `git rm -r` in cron mode.
git rm --cached -- "${INTERNAL_FILES[@]}"
# 3. Commit with co-author trailer. --no-verify skips prepare-commit-msg hooks, so
# embed the trailer via -F message file (a bare -m commit would lose it).
cat > /tmp/federation-pr-msg.txt <<'EOF'
chore: drop internal foreman files from federation PR
Co-authored-by: Federation Bot <<email>>
EOF
git commit --no-verify -F /tmp/federation-pr-msg.txt
# 4. Guard: PR diff must be EXACTLY the feature files, ZERO internal paths.
DIFF_FILES=$(git diff --name-only "$UPSTREAM_REMOTE/$BASE"...$PR_BRANCH)
ACTUAL_COUNT=$(printf '%s\n' "$DIFF_FILES" | sed '/^$/d' | wc -l | tr -d ' ')
LEAKS=$(printf '%s\n' "$DIFF_FILES" | grep -cE '^(internal/board|internal/gitreins)/' || true)
if [[ "$ACTUAL_COUNT" -ne "$EXPECTED_FEATURE_COUNT" || "$LEAKS" -ne 0 ]]; then
echo "ABORT: diff=$ACTUAL_COUNT files (expected $EXPECTED_FEATURE_COUNT), internal leaks=$LEAKS" >&2
exit 1
fi
# 5. Push head branch to the fork
git push -u origin "$PR_BRANCH"
# 6. Open PR against upstream, base=master, body from file
gh pr create -R "$UPSTREAM_REMOTE" --head "$OWNER:$PR_BRANCH" --base "$BASE" --body-file /tmp/federation-pr-body.md
CI note (expected behavior): fork→upstream PRs from a first-time contributor land at the action_required approval gate with zero jobs scheduled — that is the fork-PR CI behavior, not a failure. Cite CI-green from the identical code's own push runs on the fork (origin push CI) as the correctness evidence in the PR description.
Verified end-to-end in a sandbox: bare `upstream.git` (master with baseline) + `fork` clone; `feature` branch carried 12 feature files (`src/feat1.c`…`feat12.c`) plus the 8 internal files (`internal/board/board{1..4}.json`, `internal/gitreins/gitrein{1..4}.yml`) — 20 tracked total.
| Check | Result |
|---|---|
| Main workflow: `git rm --cached` (explicit 8, no `-r`) → `--no-verify -F` commit → push | pass |
| `git diff --name-only origin/master...federation-pr` count | **12 files**, equals expected feature count (12 ≠ 20; internals excluded) |
| Internal paths in PR diff | **0** |
| Index/HEAD tree after commit | 0 internal paths tracked (`git ls-files` = 0, `git ls-tree` = 0) |
| Mergeability | `git merge-tree --write-tree origin/master federation-pr` → clean, no conflicts |
| Co-author trailer | `git log -1 --format='%(trailers)'` parses `Co-authored-by: Federation Bot <<email>>` (present via `-F`; `-m`-only would drop it, and `--no-verify` skips the `prepare-commit-msg` hook that normally injects it) |
| Edge 1 — leaked file detection | Branch from `feature` removing only 7/8: diff = 13 files with 1 internal path → guard fires, PR blocked (ABORT path exercised) |
| Edge 2 — history integrity | `feature` branch retains all 8 internal files; only the PR head branch's diff is scrubbed, so upstream history stays clean |
| Edge 5 — exact-count assertion | `12 == 12` asserted; mismatch aborts before push |
| `gh pr create` invocation | Syntax valid (`--head owner:branch --base master --body-file`); requires `GH_TOKEN`/auth in cron mode — auth gate, not a workflow failure |
Also confirmed: plain `git rm -r` is intentionally avoided because the cron-mode recursive-delete filter rejects it; `git rm --cached <file>…` with an explicit list passes. `git diff --name-only upstream/master...<branch>` (three-dot, merge-base) is the correct scoping operator — two-dot would show extraneous base drift.{"model": "deepseek-v4-flash", "problem_class": "github-clean-fork-pr", "result": "passed", "tests": 6}