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
3 changes: 3 additions & 0 deletions docs/guide/bug-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,9 @@ After plan approval, Forge:
8. **Human review** gate.
9. **Post-merge summary:** After merge, Forge posts a fix summary and release note to the Jira ticket.

!!! note "PR Merge Reconciliation"
A merged pull request is terminally reconciled by its repository namespace and pull request number, not by its head SHA. This design ensures that merge detection remains robust and unaffected by commit rebuilds, squashing, or rebase operations during review.

---

## Comment Syntax
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/feature-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,9 @@ The PR is now ready for human review. Merge when satisfied, or request changes t

Once the PR is merged, Forge automatically completes the workflow by transitioning the Feature, all associated Tasks and Epics, and its parent Epic (if present) to **`Closed`** status in Jira.

!!! note "PR Merge Reconciliation"
A merged pull request is terminally reconciled by its repository namespace and pull request number, not by its head SHA. This design ensures that merge detection remains robust and unaffected by commit rebuilds, squashing, or rebase operations during review.

## Q&A Mode

At any approval gate, you can ask questions without triggering regeneration:
Expand Down
3 changes: 3 additions & 0 deletions docs/guide/task-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ If the agent classifies any review comments as *contested* (e.g., if they contra

When the PR is merged, Forge marks the Task workflow complete.

!!! note "PR Merge Reconciliation"
A merged pull request is terminally reconciled by its repository namespace and pull request number, not by its head SHA. This design ensures that merge detection remains robust and unaffected by commit rebuilds, squashing, or rebase operations during review.

## Comment Syntax

At the triage and plan gates, Forge classifies comments by prefix:
Expand Down
48 changes: 38 additions & 10 deletions tests/unit/workflow/test_pr_state.py
Original file line number Diff line number Diff line change
Expand Up @@ -265,7 +265,9 @@ def test_save_with_unknown_number_keys_by_url() -> None:
)

assert "acme/docs:https://github.com/acme/docs/pull/30" in saved["pull_requests"]
assert saved["pull_requests"]["acme/docs:https://github.com/acme/docs/pull/30"]["number"] is None
assert (
saved["pull_requests"]["acme/docs:https://github.com/acme/docs/pull/30"]["number"] is None
)


def test_save_without_number_or_url_is_noop() -> None:
Expand Down Expand Up @@ -359,18 +361,14 @@ def _legacy_state() -> dict:

def test_event_targets_pull_request_matches_legacy_bare_repo_key() -> None:
state = _legacy_state()
event = _event(
repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99"
)
event = _event(repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99")

assert event_targets_pull_request(state, event)


def test_activate_pull_request_for_event_hydrates_from_legacy_bare_repo_key() -> None:
state = _legacy_state()
event = _event(
repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99"
)
event = _event(repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99")

activated = activate_pull_request_for_event(state, event)

Expand All @@ -381,9 +379,7 @@ def test_activate_pull_request_for_event_hydrates_from_legacy_bare_repo_key() ->

def test_save_migrates_legacy_bare_repo_key_to_numbered_key() -> None:
state = _legacy_state()
event = _event(
repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99"
)
event = _event(repo="acme/legacy", native_id=99, url="https://github.com/acme/legacy/pull/99")
activated = activate_pull_request_for_event(state, event)
activated["ci_status"] = "passed"

Expand All @@ -392,3 +388,35 @@ def test_save_migrates_legacy_bare_repo_key_to_numbered_key() -> None:
assert "acme/legacy" not in saved["pull_requests"]
assert saved["pull_requests"]["acme/legacy:99"]["ci_status"] == "passed"
assert saved["pull_requests"]["acme/legacy:99"]["lifecycle_node"] == "ci_evaluator"


def test_reconciliation_is_independent_of_head_sha() -> None:
"""A pull request is terminally reconciled by its repository and PR number, not by its head SHA."""
# Create two events with different head_sha but the same repo and native_id
event_1 = _event(
repo="acme/payments", native_id=42, url="https://github.com/acme/payments/pull/42"
)
# Set different head SHAs on the ChangeRequests
event_1.change_request.head_sha = "sha1"

event_2 = _event(
repo="acme/payments", native_id=42, url="https://github.com/acme/payments/pull/42"
)
event_2.change_request.head_sha = "sha2"

# State with key based on repo and number
key = "acme/payments:42"
state = {
"pull_requests": {key: {"number": 42, "url": event_1.change_request.url, "merged": False}}
}

# Both events target the exact same pull request record regardless of the head SHA
assert event_targets_pull_request(state, event_1) is True
assert event_targets_pull_request(state, event_2) is True

# Activating either event works and targets the same record
activated_1 = activate_pull_request_for_event(state, event_1)
activated_2 = activate_pull_request_for_event(state, event_2)

assert activated_1["current_pr_number"] == 42
assert activated_2["current_pr_number"] == 42
Loading