Skip to content

Add pre-push ancestry guard to prevent rewritten PR history#464

Merged
strawgate merged 9 commits intomainfrom
fix/mention-pr-history-rewrite-guard-b9148a384abc45fa
Feb 28, 2026
Merged

Add pre-push ancestry guard to prevent rewritten PR history#464
strawgate merged 9 commits intomainfrom
fix/mention-pr-history-rewrite-guard-b9148a384abc45fa

Conversation

@github-actions
Copy link
Contributor

@github-actions github-actions bot commented Feb 28, 2026

Summary

Adds history-safety and workflow-path guards for patch-based PR updates so workflows fail fast on rewritten history, merge commits, or prohibited workflow-file edits.

What changed

  • Added pre-push guards in github/workflows/gh-aw-fragments/safe-output-push-to-pr.md (source) and regenerated .github/workflows/gh-aw-fragments/safe-output-push-to-pr.md that:
    • Block push when local changes include .github/workflows/ paths (which push_to_pull_request_branch rejects)
    • Read the original PR head SHA (headRefOid) from /tmp/pr-context/pr.json
    • Block push when the original PR head is not an ancestor of HEAD (git merge-base --is-ancestor)
    • Block push when merge commits exist since the PR head (git rev-list --min-parents=2)
    • Return actionable remediation (reset to PR head, re-apply as direct file edits, create regular single-parent commits)
  • Renamed the push readiness safe-input from ready_to_make_pr to ready_to_push_to_pr and updated related guidance/call sites in regenerated lock workflows.
  • Updated .github/workflows/gh-aw-fragments/pr-context.md to fetch and document headRefOid in pr.json.
  • Added a guard in .github/workflows/gh-aw-fragments/safe-output-create-pr.md that:
    • Finds an upstream fork point using @{upstream}, origin/HEAD, then origin/main
    • Fails closed when no upstream fork point can be determined
    • Blocks create_pull_request when merge commits exist in the commit range
  • Updated merge-conflict guidance in the push fragment to use git diff HEAD...origin/(base-branch) with baseRefName from /tmp/pr-context/pr.json.
  • Added regression coverage in tests/test_safe_input_ready_to_make_pr.py for history rewrite detection, merge commit detection, and no-upstream fail-closed behavior.
  • Regenerated affected .lock.yml workflows so runtime behavior matches fragment updates.

Why

Issue #463 exposed a failure mode where rewritten history and merge commits break the git format-patch flow used by safe-output PR operations. These guards catch that state early and provide deterministic recovery steps.

Validation

  • Ran: make lint (passes)
  • Added/updated tests: tests/test_safe_input_ready_to_make_pr.py

Fixes #463

Generated by Update PR Body for issue #464

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@cla-checker-service
Copy link

cla-checker-service bot commented Feb 28, 2026

❌ Author of the following commits did not sign a Contributor Agreement:
a27a85a, ,

Please, read and sign the above mentioned agreement if you want to contribute to this project

@coderabbitai
Copy link

coderabbitai bot commented Feb 28, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds history-safety guards and renames PR readiness flow across multiple workflow fragments and safe-inputs scripts. When /tmp/pr-context/pr.json exists the workflows now: (1) verify the PR head SHA is an ancestor of HEAD (history-rewrite check) and (2) detect merge commits (using merge-base/upstream fork-point + git rev-list --min-parents=2). If either check fails the flow emits a JSON-formatted error and exits before diff generation, self-review or push-to-PR steps. Also updates PR-context JSON to include headRefOid and adjusts tooling names from ready-to-make-pr to ready-to-push-to-pr; tests were added for these guards.

Possibly related PRs

🚥 Pre-merge checks | ✅ 2
✅ Passed checks (2 passed)
Check name Status Explanation
Linked Issues check ✅ Passed PR implements all primary requirements from #463: adds pre-push ancestry/merge-commit guards, upstream fork detection, fail-fast behavior, remediation guidance, and regression tests.
Out of Scope Changes check ✅ Passed All changes directly address issue #463 requirements: guard implementations, PR context metadata, fragment updates, and test coverage. No unrelated modifications detected.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch fix/mention-pr-history-rewrite-guard-b9148a384abc45fa

Comment @coderabbitai help to get the list of available commands and usage tips.

@strawgate
Copy link
Collaborator

/ai is there something we could do like forbid merge commits on the git cli? Are our instructions clear enough on not doing bad things?

@github-actions
Copy link
Contributor Author

Yes—this is already mostly clear, and the new ancestry guard is a strong safety net.
If you want a stricter CLI-level guard against merge commits specifically, add an explicit pre-push check like git rev-list --merges "(pr-head-sha)"..HEAD and require empty output before push_to_pull_request_branch; this complements git merge-base --is-ancestor (which catches rewrites but not all merge-commit scenarios by itself).
Given the current wording (no git merge, no git rebase, single-parent commit requirement), I’d say the instructions are clear; adding the merge-commit command makes enforcement even more deterministic.


What is this? | From workflow: Mention in PR

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@strawgate
Copy link
Collaborator

/ai can you look at the run logs and see what actually happened? You should be able to get the prompt too, and figure out what went wrong

@github-actions
Copy link
Contributor Author

The prior run (22513924543) behaved consistently with its prompt; the mismatch is that it processed your earlier comment (comment-id: 3976398896), not this newer “check the logs/prompt” request (3976406255).

Evidence from that run:

  • Activation prompt context had comment-id: 3976398896 and Request: "".
  • The agent read PR context/diff files and then made a single safeoutputs-add_comment call with the merge-commit guidance text.
  • Safe outputs show exactly one emitted action: comment #issuecomment-3976403516.

So what went wrong is not tool execution failure; it was that the previous run answered the previous /ai prompt and did not include a log-inspection task. This run is the one tied to your latest comment asking for log/prompt inspection.


What is this? | From workflow: Mention in PR

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@strawgate
Copy link
Collaborator

/ai please investigate why Activation prompt context had comment-id: 3976398896 and Request: "". That doesn't make sense

@strawgate
Copy link
Collaborator

@copilot move the workflow into the right location in the repo

Copy link
Contributor

Copilot AI commented Feb 28, 2026

@strawgate I've opened a new pull request, #469, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: strawgate <6384545+strawgate@users.noreply.github.com>
@strawgate
Copy link
Collaborator

/ai maybe we just run the command when the agent calls ready to make pr command and return an error with instructions?

coderabbitai[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

Move the ancestry and merge commit checks from prompt-based instructions
into the ready_to_make_pr Python script so they are enforced automatically.
When the agent calls ready_to_make_pr, the script now:

- push-to-pr: checks that the original PR head SHA (from pr.json) is an
  ancestor of HEAD (detects rebase/reset) and that no merge commits exist
  since the PR head
- create-pr: checks that no merge commits exist since the upstream fork
  point

Both return a status:error JSON with actionable fix instructions.

Also adds headRefOid to the pr-context gh pr view fields so the SHA is
available in pr.json.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
coderabbitai[bot]

This comment was marked as resolved.

github-actions[bot]

This comment was marked as resolved.

strawgate and others added 4 commits February 28, 2026 00:50
…y-rewrite-guard-b9148a384abc45fa

# Conflicts:
#	.github/workflows/gh-aw-mention-in-pr.lock.yml
- Remove `---` from Python comments in safe-input fragments that broke
  the test's YAML frontmatter extraction (split on `---`)
- Update test_fragments_have_identical_py to test_fragments_share_common_logic
  since push and create fragments now have different guard sections
- Add TestPushGuards: ancestry rewrite detection, merge commit detection,
  no-pr-json passthrough, normal commit passthrough
- Add TestCreateGuards: merge commit detection, normal commit passthrough
- Recompile lock files after merging origin/main

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Create fragment: fail closed when upstream fork point cannot be
  resolved instead of silently skipping the merge-commit guard
- Push fragment: replace `git checkout {sha}` (detaches HEAD) with
  `git reset --hard {sha}` in recovery guidance to keep branch checked out
- Add test for create fragment's no-upstream fail-closed behavior

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Fix comment describing diff fallback chain: `git diff @{upstream}`
  includes working tree changes, not "committed only"
- Replace vague `git merge-tree or git diff` with specific
  `git diff HEAD...origin/<base-branch>` for merge conflict resolution

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
coderabbitai[bot]

This comment was marked as resolved.

def py_code(self):
return extract_py_block(PUSH_FRAGMENT)

@pytest.fixture(autouse=True)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[MEDIUM] Cleanup scope allows /tmp/pr-context/pr.json to leak into unrelated tests

The autouse cleanup fixture is scoped to TestPushGuards, but many earlier tests in this file also execute the push-fragment code path. Since the new guard reads global /tmp/pr-context/pr.json when present, stale context can flip otherwise-ok tests into error (e.g., with {"headRefOid":"deadbeef"} set).

Please make cleanup module-wide (or clear /tmp/pr-context/pr.json before every push-fragment test) so all tests that call extract_py_block(PUSH_FRAGMENT) are hermetic and not coupled to ambient runner state.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
.github/workflows/gh-aw-bug-exterminator.lock.yml (1)

959-976: ⚠️ Potential issue | 🟠 Major

Use the resolved upstream_sha variable in diff/stat generation instead of re-attempting @{upstream} resolution.

Line 948 uses upstream_sha successfully for merge-commit validation, but lines 963 and 973 still reference @{upstream} for diff and stat generation. If the tracking branch is not configured (but upstream_sha was resolved via origin/HEAD or origin/main), the diff/stat will fail on @{upstream} and fall back to HEAD, missing committed changes.

Suggested patch
           for diff_cmd in [
-              ['git', 'diff', '--merge-base', '@{upstream}'],
-              ['git', 'diff', '@{upstream}'],
+              ['git', 'diff', upstream_sha],
               ['git', 'diff', 'HEAD'],
           ]:
               result = run(diff_cmd)
               if result.stdout.strip():
                   diff_text = result.stdout.strip()
                   break
           stat_text = ''
           for stat_cmd in [
-              ['git', 'diff', '--stat', '--merge-base', '@{upstream}'],
-              ['git', 'diff', '--stat', '@{upstream}'],
+              ['git', 'diff', '--stat', upstream_sha],
               ['git', 'diff', '--stat', 'HEAD'],
           ]:
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gh-aw-bug-exterminator.lock.yml around lines 959 - 976,
The diff/stat generation still uses the literal '@{upstream}' instead of the
resolved upstream_sha; update the command lists used to build diff_cmd and
stat_cmd so any occurrence of '@{upstream}' is replaced with the upstream_sha
variable (e.g., ['git','diff','--merge-base', upstream_sha], ['git','diff',
upstream_sha], and same for the --stat variants), preserving the existing
fallback order and using the upstream_sha variable (not a quoted string) when
constructing the commands passed to run().
♻️ Duplicate comments (1)
tests/test_safe_input_ready_to_make_pr.py (1)

418-422: ⚠️ Potential issue | 🟠 Major

Test cleanup is scoped too narrowly and can make unrelated tests flaky.

Line 418-Line 422 only cleans /tmp/pr-context/pr.json for TestPushGuards, but earlier tests also execute the push fragment and can pick up stale PR context from the environment. Move this cleanup to a module-level autouse fixture so every push-fragment test is hermetic.

Suggested fix
+@pytest.fixture(autouse=True)
+def cleanup_pr_json():
+    yield
+    _cleanup_pr_json()
+
 class TestPushGuards:
     """Test the ancestry and merge-commit guards in the push fragment."""
@@
-    `@pytest.fixture`(autouse=True)
-    def cleanup(self):
-        yield
-        _cleanup_pr_json()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/test_safe_input_ready_to_make_pr.py` around lines 418 - 422, The
current autouse fixture named cleanup (def cleanup) that yields then calls
_cleanup_pr_json() is defined inside TestPushGuards and only runs for that
class; move that fixture to module scope so it runs for every test in the file:
create a top-level `@pytest.fixture`(autouse=True) def cleanup(): yield;
_cleanup_pr_json() at the module level (outside any Test... class) so all
push-fragment tests get a fresh /tmp/pr-context/pr.json and avoid cross-test
flakiness.
🧹 Nitpick comments (1)
.github/workflows/gh-aw-issue-fixer.lock.yml (1)

989-995: Use DEFAULT_BRANCH in upstream fallback to avoid false fail-closed on non-main repos.

Line 989 hardcodes origin/main as the final fallback. If origin/HEAD is unavailable and the repo default branch is not main, this returns an avoidable error.

Proposed fix
-          upstream_sha = ''
-          for ref in ['@{upstream}', 'origin/HEAD', 'origin/main']:
+          upstream_sha = ''
+          default_branch = os.getenv('DEFAULT_BRANCH', '').strip()
+          refs = ['@{upstream}', 'origin/HEAD']
+          if default_branch:
+              refs.append(f'origin/{default_branch}')
+          refs.append('origin/main')
+          for ref in refs:
               r = run(['git', 'merge-base', 'HEAD', ref])
               if r.returncode == 0 and r.stdout.strip():
                   upstream_sha = r.stdout.strip()
                   break
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In @.github/workflows/gh-aw-issue-fixer.lock.yml around lines 989 - 995, The
fallback list currently hardcodes 'origin/main' causing failures on repos whose
default branch isn't main; update the refs iteration to use the repository
default (DEFAULT_BRANCH) instead of 'origin/main' by constructing the final
fallback as f'origin/{DEFAULT_BRANCH}' (ensure DEFAULT_BRANCH is read from the
environment or a config earlier), leaving the existing loop and use of
run([...]) and upstream_sha logic unchanged so the code still checks
['@{upstream}', 'origin/HEAD', f'origin/{DEFAULT_BRANCH}'] and prints the same
error message if upstream_sha remains unset.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Outside diff comments:
In @.github/workflows/gh-aw-bug-exterminator.lock.yml:
- Around line 959-976: The diff/stat generation still uses the literal
'@{upstream}' instead of the resolved upstream_sha; update the command lists
used to build diff_cmd and stat_cmd so any occurrence of '@{upstream}' is
replaced with the upstream_sha variable (e.g., ['git','diff','--merge-base',
upstream_sha], ['git','diff', upstream_sha], and same for the --stat variants),
preserving the existing fallback order and using the upstream_sha variable (not
a quoted string) when constructing the commands passed to run().

---

Duplicate comments:
In `@tests/test_safe_input_ready_to_make_pr.py`:
- Around line 418-422: The current autouse fixture named cleanup (def cleanup)
that yields then calls _cleanup_pr_json() is defined inside TestPushGuards and
only runs for that class; move that fixture to module scope so it runs for every
test in the file: create a top-level `@pytest.fixture`(autouse=True) def
cleanup(): yield; _cleanup_pr_json() at the module level (outside any Test...
class) so all push-fragment tests get a fresh /tmp/pr-context/pr.json and avoid
cross-test flakiness.

---

Nitpick comments:
In @.github/workflows/gh-aw-issue-fixer.lock.yml:
- Around line 989-995: The fallback list currently hardcodes 'origin/main'
causing failures on repos whose default branch isn't main; update the refs
iteration to use the repository default (DEFAULT_BRANCH) instead of
'origin/main' by constructing the final fallback as f'origin/{DEFAULT_BRANCH}'
(ensure DEFAULT_BRANCH is read from the environment or a config earlier),
leaving the existing loop and use of run([...]) and upstream_sha logic unchanged
so the code still checks ['@{upstream}', 'origin/HEAD',
f'origin/{DEFAULT_BRANCH}'] and prints the same error message if upstream_sha
remains unset.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6e583d4 and 47e1d3b.

📒 Files selected for processing (22)
  • .github/workflows/downstream-users.lock.yml
  • .github/workflows/gh-aw-bug-exterminator.lock.yml
  • .github/workflows/gh-aw-code-duplication-fixer.lock.yml
  • .github/workflows/gh-aw-code-simplifier.lock.yml
  • .github/workflows/gh-aw-fragments/safe-output-create-pr.md
  • .github/workflows/gh-aw-fragments/safe-output-push-to-pr.md
  • .github/workflows/gh-aw-issue-fixer.lock.yml
  • .github/workflows/gh-aw-mention-in-issue-no-sandbox.lock.yml
  • .github/workflows/gh-aw-mention-in-issue.lock.yml
  • .github/workflows/gh-aw-mention-in-pr-by-id.lock.yml
  • .github/workflows/gh-aw-mention-in-pr-no-sandbox.lock.yml
  • .github/workflows/gh-aw-mention-in-pr.lock.yml
  • .github/workflows/gh-aw-newbie-contributor-fixer.lock.yml
  • .github/workflows/gh-aw-pr-actions-fixer.lock.yml
  • .github/workflows/gh-aw-pr-review-addresser.lock.yml
  • .github/workflows/gh-aw-release-update.lock.yml
  • .github/workflows/gh-aw-scheduled-fix.lock.yml
  • .github/workflows/gh-aw-small-problem-fixer.lock.yml
  • .github/workflows/gh-aw-test-improvement.lock.yml
  • .github/workflows/gh-aw-test-improver.lock.yml
  • .github/workflows/gh-aw-text-beautifier.lock.yml
  • tests/test_safe_input_ready_to_make_pr.py

@strawgate
Copy link
Collaborator

/ai I think maybe we should also block writes to the .github/workflows folder

@github-actions
Copy link
Contributor Author

Good call — this is already blocked at push time.

push_to_pull_request_branch rejects any patch that modifies .github/workflows/ (also documented in the PR fragment under push limitations), so writes to that folder cannot be submitted through this workflow path.


What is this? | From workflow: Mention in PR

Give us feedback! React with 🚀 if perfect, 👍 if helpful, 👎 if not.

@strawgate
Copy link
Collaborator

/ai right but let's flag it in the ready to push tool so we can tell the agent their push will not work

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@strawgate strawgate closed this Feb 28, 2026
@strawgate strawgate reopened this Feb 28, 2026
@strawgate strawgate merged commit ef655d0 into main Feb 28, 2026
17 of 18 checks passed
@strawgate strawgate deleted the fix/mention-pr-history-rewrite-guard-b9148a384abc45fa branch February 28, 2026 18:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Investigate pr merge failure

2 participants