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
41 changes: 41 additions & 0 deletions examples/canary/premerge-validation-gate-smoke.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
from loopx.canary.premerge import ( # noqa: E402
_gate_status,
_public_boundary_changed_files_run,
apply_change_quality_verification,
build_premerge_validation_gate,
downgrade_inherited_baseline_failures,
)
Expand Down Expand Up @@ -309,6 +310,45 @@ def assert_no_changes_does_not_mask_direct_failures() -> None:
assert status["merge_gate_passed"] is False, status


def assert_passed_validation_does_not_grant_self_merge_authority() -> None:
status = _gate_status(
execute=True,
changed_files=["docs/example.md"],
direct_checks=[{"ok": True, "status": "passed", "id": "diff_check"}],
catalog_run={"ok": True},
risk_profile_run=None,
boundary_run=None,
manual_holds=[],
)
assert status["status"] == "passed", status
assert status["merge_gate_passed"] is True, status
assert status["self_merge_validation_passed"] is True, status
assert status["self_merge_allowed"] is False, status
assert status["self_merge_authority"] == {
"granted": False,
"reason": "repository_policy_required",
"next_gate": "apply repository policy and exact-head merge readiness",
}, status

payload = {
"ok": True,
"gate": status,
"validation_summary": {"failure_count": 0},
"recommended_pr_comment_fields": [],
}
apply_change_quality_verification(
payload,
{
"enforcement_applied": True,
"ok": False,
"status": "receipt_invalid",
},
)
assert payload["gate"]["merge_gate_passed"] is False, payload
assert payload["gate"]["self_merge_validation_passed"] is False, payload
assert payload["gate"]["self_merge_allowed"] is False, payload


def assert_installed_wrapper_uses_bound_python_and_redirects_to_checkout() -> None:
with tempfile.TemporaryDirectory() as temp_dir:
release_root = Path(temp_dir) / "release"
Expand Down Expand Up @@ -527,6 +567,7 @@ def main() -> None:
assert_cli_json_preview()
assert_cli_premerge_reports_progress_by_default()
assert_no_changes_does_not_mask_direct_failures()
assert_passed_validation_does_not_grant_self_merge_authority()
assert_installed_wrapper_uses_bound_python_and_redirects_to_checkout()
assert_external_dirty_worktree_uses_caller_repo()
assert_inherited_maintainability_red_is_advisory_only()
Expand Down
22 changes: 19 additions & 3 deletions loopx/canary/premerge.py
Original file line number Diff line number Diff line change
Expand Up @@ -600,10 +600,20 @@ def _gate_status(
status = "preview_only"
else:
status = "passed"
self_merge_validation_passed = status == "passed" and not manual_holds
return {
"status": status,
"merge_gate_passed": status == "passed",
"self_merge_allowed": status == "passed" and not manual_holds,
# This gate owns validation, not repository merge authority. Keep the
# historical field fail-closed so a green canary cannot be mistaken for
# permission to bypass a repository's independent-maintainer policy.
"self_merge_allowed": False,
"self_merge_validation_passed": self_merge_validation_passed,
"self_merge_authority": {
"granted": False,
"reason": "repository_policy_required",
"next_gate": "apply repository policy and exact-head merge readiness",
},
"direct_failure_count": len(direct_failures),
"run_failure_count": len(run_failures),
"manual_hold_count": len(manual_holds),
Expand Down Expand Up @@ -722,6 +732,7 @@ def apply_change_quality_verification(
"status": f"quality_{status}",
"merge_gate_passed": False,
"self_merge_allowed": False,
"self_merge_validation_passed": False,
}
)
summary["failure_count"] = int(summary.get("failure_count") or 0) + 1
Expand Down Expand Up @@ -1028,8 +1039,9 @@ def build_premerge_validation_gate(
"Pre-merge validation is a risk-based gate: it runs diff hygiene, "
"changed Python compile checks, catalog-selected canaries, risk-profile "
"smokes, and public/private boundary checks. It reports manual holds for "
"benchmark-sensitive or reviewer-gated surfaces instead of treating local "
"smoke success as self-merge permission."
"benchmark-sensitive or reviewer-gated surfaces. Passing validation is "
"not merge authority: repository policy and exact-head merge readiness "
"remain separate mandatory gates."
),
}
if progress_callback and execute:
Expand Down Expand Up @@ -1095,6 +1107,10 @@ def render_premerge_validation_gate_markdown(payload: dict[str, Any]) -> str:
f"- ok: `{str(payload.get('ok')).lower()}`",
f"- merge_gate_passed: `{str(gate.get('merge_gate_passed')).lower()}`",
f"- self_merge_allowed: `{str(gate.get('self_merge_allowed')).lower()}`",
f"- self_merge_validation_passed: "
f"`{str(gate.get('self_merge_validation_passed')).lower()}`",
f"- self_merge_authority: "
f"`{str((gate.get('self_merge_authority') or {}).get('reason') or '')}`",
f"- tier: `{payload.get('tier')}`",
f"- dry_run: `{str(payload.get('dry_run')).lower()}`",
f"- changed_files: `{classification.get('changed_file_count')}`",
Expand Down
Loading