Root-cause fix for main erosion (phantom merge): code of ORCH-067/069 reached `done` while absent from origin/main (only their auto docs-PRs landed). - FR-1: verify_merged_to_main confirms merge ONLY by `git merge-base --is-ancestor <validated_sha> origin/main`; the OR-branch pr_already_merged is removed (a merged PR no longer confirms). Empty SHA / git error -> False. - FR-2: pr_already_merged demoted to merge_pr idempotency-guard; counts a PR only when merged & head.ref==<branch> & base.ref=="main" (explicit in-loop filter). - FR-3: merge_pr selects the open code-PR by head==<branch> AND base==main. - FR-5: new deterministic check_main_regression in _handle_merge_verify (after confirmed SHA-in-main, before done) verifies MAIN_REGRESSION_MARKERS still in origin/main; deterministic count==0 -> alert "main regressed" + HOLD (NOT done, no rollback); git error of the grep -> fail-open. Kill-switch ORCH_REGRESSION_GUARD_ENABLED; non-self -> no-op. - FR-4: root .gitattributes `CHANGELOG.md merge=union` so Unreleased edits auto-merge on rebase without conflict (branch not rolled back). Invariants unchanged (STAGE_TRANSITIONS, QG_CHECKS, deploy-status, merge-gate, image-freshness, DB schema, external HTTP API); non-self repos no-op (INV-5); never-raise (INV-1); merge only via Gitea PR-API (INV-2). Docs: CHANGELOG, .env.example (README/ADR updated by architect). Tests: tests/test_orch073_*.py (TC-01..18); existing merge-gate tests updated for the new code-PR filter. Refs: ORCH-073 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
86 lines
3.6 KiB
Python
86 lines
3.6 KiB
Python
"""ORCH-073 FR-4 — .gitattributes: CHANGELOG.md merge=union.
|
|
|
|
Covers TC-11/TC-12 / AC-4. TC-11 asserts the repo-root .gitattributes declares the
|
|
union driver (git check-attr). TC-12 proves, in a throwaway git repo, that two
|
|
branches both editing '## [Unreleased]' merge WITHOUT a conflict and BOTH entries
|
|
survive — exactly what stops auto_rebase_onto_main from rolling a branch back.
|
|
"""
|
|
|
|
import subprocess
|
|
from pathlib import Path
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
|
|
|
|
def _git(cwd, *args, env=None):
|
|
return subprocess.run(
|
|
["git", *args], cwd=str(cwd), capture_output=True, text=True, env=env,
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-11 (AC-4): the repo-root .gitattributes declares CHANGELOG.md merge=union.
|
|
# ---------------------------------------------------------------------------
|
|
def test_tc11_gitattributes_declares_union():
|
|
ga = REPO_ROOT / ".gitattributes"
|
|
assert ga.is_file(), ".gitattributes must exist at the repo root"
|
|
assert "CHANGELOG.md merge=union" in ga.read_text(encoding="utf-8")
|
|
|
|
r = _git(REPO_ROOT, "check-attr", "merge", "CHANGELOG.md")
|
|
assert r.returncode == 0, r.stderr
|
|
# Output form: 'CHANGELOG.md: merge: union'
|
|
assert "merge: union" in r.stdout, r.stdout
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# TC-12 (AC-4): two Unreleased edits merge with no conflict; both kept.
|
|
# ---------------------------------------------------------------------------
|
|
def _init_repo(tmp_path):
|
|
env = {
|
|
"GIT_AUTHOR_NAME": "t", "GIT_AUTHOR_EMAIL": "t@t",
|
|
"GIT_COMMITTER_NAME": "t", "GIT_COMMITTER_EMAIL": "t@t",
|
|
"GIT_CONFIG_GLOBAL": "/dev/null", "GIT_CONFIG_SYSTEM": "/dev/null",
|
|
"PATH": __import__("os").environ.get("PATH", ""),
|
|
"HOME": str(tmp_path),
|
|
}
|
|
repo = tmp_path / "repo"
|
|
repo.mkdir()
|
|
assert _git(repo, "init", "-b", "main", env=env).returncode == 0
|
|
(repo / ".gitattributes").write_text("CHANGELOG.md merge=union\n", encoding="utf-8")
|
|
base = (
|
|
"# Changelog\n\n## [Unreleased]\n\n### Common\n\n## [0.1.0]\n- initial\n"
|
|
)
|
|
(repo / "CHANGELOG.md").write_text(base, encoding="utf-8")
|
|
_git(repo, "add", ".", env=env)
|
|
assert _git(repo, "commit", "-m", "base", env=env).returncode == 0
|
|
return repo, env
|
|
|
|
|
|
def test_tc12_union_merge_keeps_both_entries(tmp_path):
|
|
repo, env = _init_repo(tmp_path)
|
|
|
|
# Branch A adds its Unreleased line.
|
|
_git(repo, "checkout", "-b", "task-a", env=env)
|
|
txt = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
(repo / "CHANGELOG.md").write_text(
|
|
txt.replace("### Common\n", "### Common\n- ORCH-A: feature A\n"), encoding="utf-8"
|
|
)
|
|
_git(repo, "commit", "-am", "task A changelog", env=env)
|
|
|
|
# Branch B (from main) adds a DIFFERENT Unreleased line at the same spot.
|
|
_git(repo, "checkout", "main", env=env)
|
|
_git(repo, "checkout", "-b", "task-b", env=env)
|
|
txt = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
(repo / "CHANGELOG.md").write_text(
|
|
txt.replace("### Common\n", "### Common\n- ORCH-B: feature B\n"), encoding="utf-8"
|
|
)
|
|
_git(repo, "commit", "-am", "task B changelog", env=env)
|
|
|
|
# Merge A into B — union must avoid a conflict and keep BOTH lines.
|
|
m = _git(repo, "merge", "--no-edit", "task-a", env=env)
|
|
result = (repo / "CHANGELOG.md").read_text(encoding="utf-8")
|
|
assert m.returncode == 0, f"union merge must not conflict: {m.stdout}\n{m.stderr}"
|
|
assert "<<<<<<<" not in result and ">>>>>>>" not in result
|
|
assert "ORCH-A: feature A" in result
|
|
assert "ORCH-B: feature B" in result
|