Code Mower gate #9009
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Code Mower workflow template: publish a single merge-gate commit status. | |
| name: Code Mower gate | |
| on: | |
| pull_request_target: | |
| types: [opened, reopened, synchronize, labeled, unlabeled, ready_for_review] | |
| issue_comment: | |
| types: [created, edited, deleted] | |
| workflow_run: | |
| workflows: ["Code Mower Local CLI Audits"] | |
| types: [completed] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "Pull request number" | |
| required: true | |
| head_sha: | |
| description: "Pull request head SHA" | |
| required: true | |
| permissions: | |
| actions: read | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| statuses: write | |
| env: | |
| FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: "true" | |
| CODE_MOWER_GATE_CONTEXT: code-mower/gate | |
| CODE_MOWER_CONTEXT_REQUIRED: "false" | |
| CODE_MOWER_GATE_ENABLE_AUTO_MERGE: "true" | |
| CODE_MOWER_OWNER_LABEL: "needs-owner" | |
| CODE_MOWER_OWNER_SITTING_LABEL: "owner-sitting" | |
| CODE_MOWER_OWNER_LOGIN: "" | |
| CODE_MOWER_OWNER_LOGIN_OVERRIDE: ${{ vars.CODE_MOWER_OWNER_LOGIN || '' }} | |
| CODE_MOWER_GATE_OVERRIDE_LABEL: "gate:override" | |
| CODE_MOWER_DECISION_AUTHORITIES: "" | |
| CODE_MOWER_DECISION_AUTHORITIES_OVERRIDE: ${{ vars.CODE_MOWER_DECISION_AUTHORITIES || '' }} | |
| CODE_MOWER_AUTHOR_EXCLUSION_JSON: "{\"authors\":{\"chatgpt-codex-connector[bot]\":\"codex\",\"claude[bot]\":\"claude\",\"cursor[bot]\":\"cursor\",\"devin-ai-integration\":\"devin\",\"devin-ai-integration[bot]\":\"devin\",\"grok-bot[bot]\":\"cursor\"},\"branch_prefixes\":{\"cursor/\":\"cursor\",\"devin/\":\"devin\"},\"enabled\":true,\"labels\":{\"builder:claude\":\"claude\",\"builder:codex\":\"codex\",\"builder:cursor\":\"cursor\",\"builder:devin\":\"devin\",\"builder:gitar\":\"gitar\",\"builder:grok-bot\":\"cursor\",\"builder:muse\":\"muse\"},\"require_verified_lineage\":true}" | |
| CODE_MOWER_GATE_LANES_JSON: "[{\"author_lane\":\"codex\",\"authors_env\":\"CODEX_BOT_AUTHORS\",\"blocked\":\"codex-audit-blocked\",\"bot_authors\":\"codex-audit-bot,codex-audit-bot[bot],github-actions[bot]\",\"builder_label\":\"builder:codex\",\"decision_coverage\":true,\"display_name\":\"Codex\",\"done\":\"codex-audit-done\",\"github_actions_workflows\":\".github/workflows/local-cli-audit.yml,.github/workflows/local-audit-publication.yml\",\"id\":\"codex\"},{\"author_lane\":\"claude\",\"authors_env\":\"CLAUDE_AUDIT_BOT_AUTHORS\",\"blocked\":\"claude-audit-blocked\",\"bot_authors\":\"claude-audit-bot,claude-audit-bot[bot],github-actions[bot]\",\"builder_label\":\"builder:claude\",\"decision_coverage\":true,\"display_name\":\"Claude\",\"done\":\"claude-audit-done\",\"github_actions_workflows\":\".github/workflows/local-cli-audit.yml,.github/workflows/local-audit-publication.yml\",\"id\":\"claude_audit\"}]" | |
| jobs: | |
| gate: | |
| name: publish Code Mower gate status | |
| if: >- | |
| (github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository) && | |
| (github.event_name != 'issue_comment' || (github.event.issue.pull_request && startsWith(github.event.comment.body, 'Code Mower context input') && contains(fromJSON('["OWNER","MEMBER","COLLABORATOR"]'), github.event.comment.author_association))) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| steps: | |
| - name: Check out Code Mower support files from default branch | |
| uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 | |
| with: | |
| ref: ${{ github.event.repository.default_branch }} | |
| persist-credentials: false | |
| - name: Clear stale Code Mower gate override | |
| if: >- | |
| github.event_name == 'pull_request_target' && | |
| github.event.action == 'synchronize' && | |
| env.CODE_MOWER_GATE_OVERRIDE_LABEL != '' | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| PR_NUMBER: ${{ github.event.pull_request.number }} | |
| run: | | |
| set -euo pipefail | |
| gh issue edit "${PR_NUMBER}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --remove-label "${CODE_MOWER_GATE_OVERRIDE_LABEL}" && exit 0 | |
| if gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" \ | |
| --jq '.[].name' | grep -Fxq -- "${CODE_MOWER_GATE_OVERRIDE_LABEL}"; then | |
| echo "::warning::Failed to remove stale ${CODE_MOWER_GATE_OVERRIDE_LABEL} label; publishing gate failure status." | |
| echo "CODE_MOWER_GATE_OVERRIDE_CLEAR_FAILED=true" >> "${GITHUB_ENV}" | |
| exit 0 | |
| fi | |
| echo "::notice::${CODE_MOWER_GATE_OVERRIDE_LABEL} was already absent." | |
| - name: Publish Code Mower gate status | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| CODE_MOWER_GATE_AUTOMERGE_TOKEN: ${{ secrets.CODE_MOWER_GATE_AUTOMERGE_TOKEN || secrets.DISPATCH_TOKEN || '' }} | |
| PR_NUMBER: ${{ github.event.pull_request.number || github.event.issue.number || github.event.inputs.pr_number || github.event.workflow_run.pull_requests[0].number }} | |
| HEAD_SHA: ${{ github.event.pull_request.head.sha || github.event.inputs.head_sha }} | |
| WORKFLOW_RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha || '' }} | |
| WORKFLOW_RUN_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch || '' }} | |
| WORKFLOW_RUN_HEAD_REPOSITORY: ${{ github.event.workflow_run.head_repository.full_name || '' }} | |
| CLAUDE_AUDIT_BOT_AUTHORS: ${{ vars.CLAUDE_AUDIT_BOT_AUTHORS || '' }} | |
| CODEX_BOT_AUTHORS: ${{ vars.CODEX_BOT_AUTHORS || '' }} | |
| run: | | |
| set -euo pipefail | |
| if [ -z "${PR_NUMBER:-}" ] && [ -n "${WORKFLOW_RUN_HEAD_SHA:-}" ]; then | |
| PR_NUMBER="$( | |
| gh api -H "Accept: application/vnd.github+json" \ | |
| "repos/${GITHUB_REPOSITORY}/commits/${WORKFLOW_RUN_HEAD_SHA}/pulls?per_page=100" \ | |
| --jq 'map(select(.state == "open")) | .[0].number // ""' 2>/dev/null || true | |
| )" | |
| export PR_NUMBER | |
| fi | |
| if [ -z "${PR_NUMBER:-}" ] && [ -n "${WORKFLOW_RUN_HEAD_BRANCH:-}" ]; then | |
| repo_owner="${GITHUB_REPOSITORY%%/*}" | |
| head_owner="${repo_owner}" | |
| if [ -n "${WORKFLOW_RUN_HEAD_REPOSITORY:-}" ]; then | |
| head_owner="${WORKFLOW_RUN_HEAD_REPOSITORY%%/*}" | |
| fi | |
| PR_NUMBER="$( | |
| gh api "repos/${GITHUB_REPOSITORY}/pulls" \ | |
| --method GET \ | |
| -f state=open \ | |
| -f "head=${head_owner}:${WORKFLOW_RUN_HEAD_BRANCH}" \ | |
| -F per_page=10 \ | |
| --jq '.[0].number // ""' 2>/dev/null || true | |
| )" | |
| export PR_NUMBER | |
| fi | |
| if [ -z "${PR_NUMBER:-}" ]; then | |
| echo "::notice::No PR number available for Code Mower gate." | |
| exit 0 | |
| fi | |
| labels_file="$(mktemp)" | |
| comments_file="$(mktemp)" | |
| events_file="$(mktemp)" | |
| pr_file="$(mktemp)" | |
| workflow_runs_file="$(mktemp)" | |
| audit_runs_file="$(mktemp)" | |
| gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels" > "${labels_file}" | |
| python3 - "${comments_file}" <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| try: | |
| from tools.audit_labeler_lib import ( | |
| GITHUB_COMMENT_RESPONSE_BYTES, | |
| GitHubCommentPage, | |
| decode_github_response, | |
| lineage_history, | |
| ) | |
| except ImportError: # pragma: no cover - package fallback | |
| from code_mower.audit_labeler_lib import ( | |
| GITHUB_COMMENT_RESPONSE_BYTES, | |
| GitHubCommentPage, | |
| decode_github_response, | |
| lineage_history, | |
| ) | |
| def fetch(page, size): | |
| completed = subprocess.run( | |
| ["gh", "api", f"repos/{os.environ['GITHUB_REPOSITORY']}/issues/" | |
| f"{os.environ['PR_NUMBER']}/comments?per_page={size}&page={page}"], | |
| check=False, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.DEVNULL, | |
| ) | |
| if completed.returncode != 0: | |
| raise RuntimeError("authenticated GitHub comment request failed") | |
| payload = decode_github_response( | |
| completed.stdout, maximum_bytes=GITHUB_COMMENT_RESPONSE_BYTES) | |
| return GitHubCommentPage(payload, len(completed.stdout)) | |
| try: | |
| comments = lineage_history(fetch, return_raw=True) | |
| result = [comments] | |
| except Exception as exc: | |
| result = {"code": "bounded_comment_history", "message": str(exc)} | |
| with open(sys.argv[1], "w", encoding="utf-8") as handle: | |
| json.dump(result, handle) | |
| PY | |
| gh api --paginate --slurp "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/timeline?per_page=100" > "${events_file}" | |
| gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}" > "${pr_file}" | |
| if [ -z "${HEAD_SHA:-}" ]; then | |
| HEAD_SHA="$(python3 - "${pr_file}" <<'PY' | |
| import json | |
| import sys | |
| with open(sys.argv[1], encoding="utf-8") as handle: | |
| payload = json.load(handle) | |
| print(str(((payload.get("head") or {}).get("sha")) or "")) | |
| PY | |
| )" | |
| export HEAD_SHA | |
| fi | |
| if [ -z "${HEAD_SHA:-}" ]; then | |
| echo "::notice::No PR head SHA available for Code Mower gate." | |
| exit 0 | |
| fi | |
| if python3 - "${workflow_runs_file}" <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| try: | |
| from tools.audit_labeler_lib import AUDIT_RUN_NON_TERMINAL_STATUSES | |
| except ImportError: # pragma: no cover - package fallback | |
| from code_mower.audit_labeler_lib import AUDIT_RUN_NON_TERMINAL_STATUSES | |
| pages = [] | |
| repo = os.environ["GITHUB_REPOSITORY"] | |
| for status in sorted(AUDIT_RUN_NON_TERMINAL_STATUSES): | |
| completed = subprocess.run( | |
| [ | |
| "gh", | |
| "api", | |
| "--paginate", | |
| "--slurp", | |
| f"repos/{repo}/actions/runs?event=pull_request_target&status={status}&per_page=100", | |
| ], | |
| check=False, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.DEVNULL, | |
| text=True, | |
| ) | |
| if completed.returncode != 0: | |
| raise SystemExit(1) | |
| try: | |
| payload = json.loads(completed.stdout or "[]") | |
| except json.JSONDecodeError: | |
| raise SystemExit(1) | |
| if isinstance(payload, list): | |
| pages.extend(payload) | |
| elif isinstance(payload, dict): | |
| pages.append(payload) | |
| with open(sys.argv[1], "w", encoding="utf-8") as handle: | |
| json.dump(pages, handle) | |
| PY | |
| then | |
| python3 - "${workflow_runs_file}" "${audit_runs_file}" <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| try: | |
| from tools.audit_labeler_lib import ( | |
| AUDIT_RUN_NON_TERMINAL_STATUSES, | |
| audit_run_matches_pr_head, | |
| audit_run_workflow_path, | |
| workflow_path_matches, | |
| ) | |
| except ImportError: # pragma: no cover - package fallback | |
| from code_mower.audit_labeler_lib import ( | |
| AUDIT_RUN_NON_TERMINAL_STATUSES, | |
| audit_run_matches_pr_head, | |
| audit_run_workflow_path, | |
| workflow_path_matches, | |
| ) | |
| def trusted_workflows_for_lanes(): | |
| values = set() | |
| for lane in lanes: | |
| raw = str(lane.get("github_actions_workflows") or "") | |
| values.update(item.strip() for item in raw.split(",") if item.strip()) | |
| return values | |
| def workflow_runs_from_payload(payload): | |
| if isinstance(payload, dict): | |
| return payload.get("workflow_runs", []) | |
| runs = [] | |
| if isinstance(payload, list): | |
| for page in payload: | |
| if isinstance(page, dict): | |
| page_runs = page.get("workflow_runs", []) | |
| if isinstance(page_runs, list): | |
| runs.extend(page_runs) | |
| return runs | |
| with open(sys.argv[1], encoding="utf-8") as handle: | |
| workflow_runs_payload = json.load(handle) | |
| lanes = json.loads(os.environ.get("CODE_MOWER_GATE_LANES_JSON", "[]")) | |
| lanes = [lane for lane in lanes if isinstance(lane, dict)] | |
| trusted_workflows = trusted_workflows_for_lanes() | |
| head_sha = os.environ.get("HEAD_SHA", "") | |
| pr_number = str(os.environ.get("PR_NUMBER", "")) | |
| runs = workflow_runs_from_payload(workflow_runs_payload) | |
| entries = [] | |
| for run in runs: | |
| if not isinstance(run, dict): | |
| continue | |
| if str(run.get("status") or "") not in AUDIT_RUN_NON_TERMINAL_STATUSES: | |
| continue | |
| if not workflow_path_matches(audit_run_workflow_path(run), trusted_workflows): | |
| continue | |
| if not audit_run_matches_pr_head(run, head_sha=head_sha, pr_number=pr_number): | |
| continue | |
| run_id = str(run.get("id") or "") | |
| jobs = [] | |
| jobs_fetch_failed = False | |
| if run_id: | |
| completed = subprocess.run( | |
| ["gh", "api", f"repos/{os.environ['GITHUB_REPOSITORY']}/actions/runs/{run_id}/jobs?per_page=100"], | |
| check=False, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.DEVNULL, | |
| text=True, | |
| ) | |
| if completed.returncode == 0: | |
| try: | |
| jobs_payload = json.loads(completed.stdout or "{}") | |
| raw_jobs = jobs_payload.get("jobs", []) if isinstance(jobs_payload, dict) else [] | |
| jobs = [job for job in raw_jobs if isinstance(job, dict)] | |
| except json.JSONDecodeError: | |
| jobs_fetch_failed = True | |
| else: | |
| jobs_fetch_failed = True | |
| entries.append({"run": run, "jobs": jobs, "jobs_fetch_failed": jobs_fetch_failed}) | |
| with open(sys.argv[2], "w", encoding="utf-8") as handle: | |
| json.dump(entries, handle) | |
| PY | |
| else | |
| printf '[]' > "${audit_runs_file}" | |
| fi | |
| eval "$( | |
| python3 - "${labels_file}" "${comments_file}" "${events_file}" "${pr_file}" "${audit_runs_file}" <<'PY' | |
| import json | |
| import os | |
| import shlex | |
| import sys | |
| try: | |
| from tools.context_review import required_for_checkout | |
| from tools.audit_labeler_lib import ( | |
| GitHubToken, | |
| attested_non_current_audit_heads, | |
| audit_run_in_flight_detail_for_lane, | |
| audit_verdict_newer_than_in_flight, | |
| flatten_paginated_items, | |
| lineage_core, | |
| lineage_decision, | |
| lineage_snapshot, | |
| github_actions_comment_attested, | |
| latest_current_audit_verdict, | |
| latest_current_audit_verdict_detail, | |
| ) | |
| except ImportError: # pragma: no cover - unit-test/package fallback | |
| from code_mower.context_review import required_for_checkout | |
| from code_mower.audit_labeler_lib import ( | |
| GitHubToken, | |
| attested_non_current_audit_heads, | |
| audit_run_in_flight_detail_for_lane, | |
| audit_verdict_newer_than_in_flight, | |
| flatten_paginated_items, | |
| lineage_core, | |
| lineage_decision, | |
| lineage_snapshot, | |
| github_actions_comment_attested, | |
| latest_current_audit_verdict, | |
| latest_current_audit_verdict_detail, | |
| ) | |
| # github_actions_comment_attested verifies the hidden CODE_MOWER_AUDIT_RUN marker | |
| # and requires comment_id/body_sha256 to match the issue comment being evaluated. | |
| def emit(state, description): | |
| raw_description = str(description).replace("\n", " ") | |
| description = raw_description[:140] | |
| print("gate_state=" + shlex.quote(state)) | |
| print("gate_description=" + shlex.quote(description)) | |
| print("gate_log_description=" + shlex.quote(raw_description)) | |
| try: | |
| lanes = json.loads(os.environ.get("CODE_MOWER_GATE_LANES_JSON", "[]")) | |
| exclusion = lineage_core.Identity.from_text(os.environ["CODE_MOWER_AUTHOR_EXCLUSION_JSON"]) | |
| with open(sys.argv[1], encoding="utf-8") as labels_handle: | |
| labels_payload = json.load(labels_handle) | |
| with open(sys.argv[2], encoding="utf-8") as comments_handle: | |
| comments_payload = lineage_core._json(comments_handle.read()) | |
| with open(sys.argv[3], encoding="utf-8") as events_handle: | |
| events_payload = json.load(events_handle) | |
| with open(sys.argv[4], encoding="utf-8") as pr_handle: | |
| pr_payload = lineage_core._json(pr_handle.read()) | |
| with open(sys.argv[5], encoding="utf-8") as audit_runs_handle: | |
| audit_runs_payload = json.load(audit_runs_handle) | |
| except Exception: | |
| emit("failure", "Code Mower gate config could not be parsed") | |
| raise SystemExit(0) | |
| head_sha = os.environ.get("HEAD_SHA", "") | |
| actual_head_sha = str(((pr_payload.get("head") or {}).get("sha")) or "") | |
| if not head_sha or head_sha != actual_head_sha: | |
| emit("failure", "workflow head_sha does not match PR head") | |
| raise SystemExit(0) | |
| labels = { | |
| str(item.get("name") or "") | |
| for item in labels_payload | |
| if isinstance(item, dict) | |
| } | |
| lanes = [lane for lane in lanes if isinstance(lane, dict)] | |
| try: | |
| history = lineage_core.History.from_pages(comments_payload) | |
| except ValueError: | |
| detail = (comments_payload.get("message") | |
| if isinstance(comments_payload, dict) | |
| and comments_payload.get("code") == "bounded_comment_history" | |
| else "lineage history unreadable") | |
| emit("failure", "Code Mower " + str(detail)) | |
| raise SystemExit(0) | |
| comments = flatten_paginated_items(comments_payload) | |
| events = flatten_paginated_items(events_payload) | |
| audit_runs = audit_runs_payload if isinstance(audit_runs_payload, list) else [] | |
| configured_decision_authorities = ( | |
| os.environ.get("CODE_MOWER_DECISION_AUTHORITIES_OVERRIDE", "").strip() | |
| or os.environ.get("CODE_MOWER_DECISION_AUTHORITIES", "") | |
| ) | |
| decision_authorities = { | |
| item.strip() | |
| for item in configured_decision_authorities.split(",") | |
| if item.strip() | |
| } | |
| owner_label = os.environ.get("CODE_MOWER_OWNER_LABEL", "needs-owner") | |
| owner_sitting_label = os.environ.get("CODE_MOWER_OWNER_SITTING_LABEL", "owner-sitting") | |
| gate_override_label = os.environ.get("CODE_MOWER_GATE_OVERRIDE_LABEL", "gate:override") | |
| override_clear_failed = ( | |
| os.environ.get("CODE_MOWER_GATE_OVERRIDE_CLEAR_FAILED", "").lower() | |
| == "true" | |
| ) | |
| owner_login = ( | |
| os.environ.get("CODE_MOWER_OWNER_LOGIN_OVERRIDE", "").strip() | |
| or os.environ.get("CODE_MOWER_OWNER_LOGIN", "").strip() | |
| ) | |
| repo_owner = os.environ["GITHUB_REPOSITORY"].split("/", 1)[0] | |
| configured_override_owner = ( | |
| owner_login | |
| if owner_login and owner_login.lower() != "todo_owner_login" | |
| else repo_owner | |
| ) | |
| override_owner = configured_override_owner.lower() | |
| pr_number = str(pr_payload.get("number") or os.environ.get("PR_NUMBER") or "") | |
| pr_author = str(((pr_payload.get("user") or {}).get("login") or "")) | |
| github_tokens = (GitHubToken("GH_TOKEN", os.environ.get("GH_TOKEN", "")),) | |
| def mapping(section): | |
| value = exclusion.get(section) if isinstance(exclusion, dict) else {} | |
| return value if isinstance(value, dict) else {} | |
| def trusted_authors(lane): | |
| raw = str(lane.get("bot_authors") or "") | |
| env_raw = os.environ.get(str(lane.get("authors_env") or ""), "") | |
| merged = raw + "," + env_raw | |
| return {item.strip().lower() for item in merged.split(",") if item.strip()} | |
| def trusted_github_actions_workflows(lane): | |
| raw = str(lane.get("github_actions_workflows") or "") | |
| return {item.strip() for item in raw.split(",") if item.strip()} | |
| def trusted_comment_author(lane, author, body, comment_id, expected_head_sha=None): | |
| authors = trusted_authors(lane) | |
| author_login = author.strip().lower() | |
| if author_login not in authors: | |
| return False | |
| if author_login != "github-actions[bot]": | |
| return True | |
| attested_head_sha = expected_head_sha or head_sha | |
| return github_actions_comment_attested( | |
| repo=os.environ["GITHUB_REPOSITORY"], | |
| body=body, | |
| comment_id=comment_id, | |
| issue_number=int(pr_number), | |
| head_sha=attested_head_sha, | |
| workflow_paths=trusted_github_actions_workflows(lane), | |
| tokens=github_tokens, | |
| ) | |
| context_required = required_for_checkout( | |
| 'code-mower.yml', fallback=os.environ.get('CODE_MOWER_CONTEXT_REQUIRED') == 'true' | |
| ) | |
| def latest_current_verdict(lane): | |
| return latest_current_audit_verdict( | |
| lane, | |
| comments, | |
| head_sha=head_sha, | |
| trusted_comment_author=trusted_comment_author, | |
| decision_authorities=tuple(decision_authorities), | |
| context_required=context_required, | |
| ) | |
| def latest_current_verdict_detail(lane): | |
| return latest_current_audit_verdict_detail( | |
| lane, | |
| comments, | |
| head_sha=head_sha, | |
| trusted_comment_author=trusted_comment_author, | |
| decision_authorities=tuple(decision_authorities), | |
| context_required=context_required, | |
| ) | |
| try: | |
| target, observed_author, observed_labels = lineage_snapshot( | |
| os.environ["GITHUB_REPOSITORY"], int(pr_number), pr_payload) | |
| if set(observed_labels) != labels: | |
| raise lineage_core.ContractError("Fetched label snapshots differ") | |
| _, lineage = lineage_decision(target, exclusion, | |
| lineage_core.Authorities(decision_authorities), history, | |
| author=observed_author, labels=observed_labels) | |
| except (ValueError, KeyError, TypeError): | |
| emit("failure", "Code Mower lineage contract unreadable; owner action required") | |
| raise SystemExit(0) | |
| if lineage.status != "ready": | |
| emit("failure" if lineage.status == "conflict" else "pending", "lineage " + lineage.reason) | |
| raise SystemExit(0) | |
| builder_matches = list(lineage.contributors) | |
| def lane_display(lane): | |
| return str(lane.get("display_name") or lane.get("id") or lane.get("done") or "") | |
| def in_flight_lanes(required): | |
| names = [] | |
| for lane in required: | |
| verdict = latest_current_verdict_detail(lane) | |
| for entry in audit_runs: | |
| if not isinstance(entry, dict): | |
| continue | |
| detail = audit_run_in_flight_detail_for_lane( | |
| entry, | |
| lane, | |
| head_sha=head_sha, | |
| pr_number=pr_number, | |
| ) | |
| if detail is None: | |
| continue | |
| if audit_verdict_newer_than_in_flight(verdict, detail): | |
| continue | |
| names.append( | |
| lane_display(lane) + " (" + detail.description() + ")" | |
| ) | |
| break | |
| return names | |
| blocked = [ | |
| lane_display(lane) | |
| for lane in lanes | |
| if latest_current_verdict(lane) == "blocked" | |
| ] | |
| override_actor = "" | |
| override_event_key = None | |
| head_move_event_key = None | |
| def timeline_sha(value): | |
| if isinstance(value, dict): | |
| return str(value.get("sha") or value.get("id") or "") | |
| return str(value or "") | |
| def event_key(index, event): | |
| created = str(event.get("created_at") or "") | |
| return (created, index) | |
| def is_current_head_move(event): | |
| event_name = str(event.get("event") or "") | |
| if event_name == "committed": | |
| return str(event.get("sha") or "") == head_sha | |
| if event_name == "head_ref_force_pushed": | |
| return ( | |
| timeline_sha(event.get("after_commit")) == head_sha | |
| or str(event.get("commit_id") or "") == head_sha | |
| ) | |
| if event_name == "head_ref_restored": | |
| return str(event.get("commit_id") or "") == head_sha | |
| return False | |
| if gate_override_label: | |
| for index, event in enumerate(events): | |
| if is_current_head_move(event): | |
| head_move_event_key = event_key(index, event) | |
| if event.get("event") != "labeled": | |
| continue | |
| if str(((event.get("label") or {}).get("name")) or "") != gate_override_label: | |
| continue | |
| override_actor = str(((event.get("actor") or {}).get("login")) or "").lower() | |
| override_event_key = event_key(index, event) | |
| override_current_head = ( | |
| override_event_key is not None | |
| and ( | |
| head_move_event_key is None | |
| or override_event_key > head_move_event_key | |
| ) | |
| ) | |
| print( | |
| "history_warning_heads=" | |
| + shlex.quote( | |
| json.dumps( | |
| attested_non_current_audit_heads( | |
| lanes, | |
| comments, | |
| head_sha=head_sha, | |
| trusted_comment_author=trusted_comment_author, | |
| ) | |
| ) | |
| ) | |
| ) | |
| if owner_label in labels: | |
| emit("pending", owner_label + ": waiting on owner") | |
| elif owner_sitting_label and owner_sitting_label in labels: | |
| emit("pending", owner_sitting_label + ": waiting on owner") | |
| elif gate_override_label and gate_override_label in labels and override_actor != override_owner: | |
| emit("failure", gate_override_label + " was not owner-applied") | |
| elif gate_override_label and gate_override_label in labels and override_clear_failed: | |
| emit("failure", gate_override_label + " could not be cleared after head change") | |
| elif gate_override_label and gate_override_label in labels and not override_current_head: | |
| emit("failure", gate_override_label + " is stale for current head") | |
| elif gate_override_label and gate_override_label in labels: | |
| emit("success", "owner gate override") | |
| elif blocked: | |
| emit("failure", "blocked audit: " + ", ".join(blocked)) | |
| else: | |
| excluded_builder = builder_matches[0] if builder_matches else "" | |
| required = [ | |
| lane | |
| for lane in lanes | |
| if lineage_core.admit(lineage, str(lane.get("author_lane") or lane.get("id") or "")) | |
| ] | |
| in_flight = in_flight_lanes(required) | |
| missing = [ | |
| lane_display(lane) | |
| for lane in required | |
| if str(lane.get("done") or "") not in labels | |
| or latest_current_verdict(lane) != "done" | |
| ] | |
| if not required: | |
| if excluded_builder: | |
| emit("failure", "no independent Code Mower audit lane remains") | |
| else: | |
| emit("failure", "no Code Mower merge-authority lanes configured") | |
| elif in_flight: | |
| emit("pending", "audit in flight: " + ", ".join(in_flight)) | |
| elif missing: | |
| emit("pending", "waiting for audit: " + ", ".join(missing)) | |
| else: | |
| emit("success", "Code Mower merge gate passed") | |
| PY | |
| )" | |
| history_warning_file="$(mktemp)" | |
| python3 - "${history_warning_heads:-[]}" "${comments_file}" "${history_warning_file}" <<'PY' | |
| import json | |
| import os | |
| import subprocess | |
| import sys | |
| try: | |
| heads = json.loads(sys.argv[1]) | |
| except json.JSONDecodeError: | |
| heads = [] | |
| heads = [str(head) for head in heads if str(head)] | |
| try: | |
| with open(sys.argv[2], encoding="utf-8") as handle: | |
| comments_payload = json.load(handle) | |
| except Exception: | |
| comments_payload = [] | |
| comments = [] | |
| for page in comments_payload: | |
| if isinstance(page, list): | |
| comments.extend(item for item in page if isinstance(item, dict)) | |
| head_sha = os.environ.get("HEAD_SHA", "") | |
| repo = os.environ.get("GITHUB_REPOSITORY", "") | |
| for previous in heads: | |
| marker = f"<!-- CODE_MOWER_HISTORY_REWRITE_WARNING previous={previous} current={head_sha} -->" | |
| if any(marker in str(comment.get("body") or "") for comment in comments): | |
| continue | |
| completed = subprocess.run( | |
| ["gh", "api", f"repos/{repo}/compare/{previous}...{head_sha}", "--jq", ".status"], | |
| check=False, | |
| stdout=subprocess.PIPE, | |
| stderr=subprocess.DEVNULL, | |
| text=True, | |
| ) | |
| if completed.returncode != 0: | |
| continue | |
| status = completed.stdout.strip() | |
| if status in {"ahead", "identical"}: | |
| continue | |
| body = ( | |
| marker | |
| + "\nCode Mower notice: previously audited head `" | |
| + previous[:12] | |
| + "` is no longer an ancestor of current head `" | |
| + head_sha[:12] | |
| + "`; commits may have been dropped. Only current-head audit verdicts count." | |
| ) | |
| with open(sys.argv[3], "w", encoding="utf-8") as handle: | |
| json.dump({"body": body}, handle) | |
| break | |
| PY | |
| if [ -s "${history_warning_file}" ]; then | |
| gh api -X POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ | |
| --input "${history_warning_file}" >/dev/null || { | |
| echo "::notice::Could not post Code Mower history-rewrite warning." | |
| } | |
| fi | |
| target_url="https://github.com/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" | |
| gh api -X POST "repos/${GITHUB_REPOSITORY}/statuses/${HEAD_SHA}" \ | |
| -f state="${gate_state}" \ | |
| -f context="${CODE_MOWER_GATE_CONTEXT}" \ | |
| -f description="${gate_description}" \ | |
| -f target_url="${target_url}" >/dev/null | |
| echo "Code Mower gate: ${gate_state} - ${gate_log_description:-${gate_description}}" | |
| # The merge signal is the commit status above. Keep expected | |
| # pending/failure verdicts from creating a second required check-run. | |
| if [ "${gate_state}" != "success" ] || [ "${CODE_MOWER_GATE_ENABLE_AUTO_MERGE}" != "true" ]; then | |
| exit 0 | |
| fi | |
| # One head-pinned merge path for both merge states: `gh pr merge | |
| # --auto` queues auto-merge while required checks are still pending | |
| # and merges promptly once the PR is mergeable, so queue-later and | |
| # merge-now share a single call. --match-head-commit refuses a moved | |
| # head instead of merging unaudited commits. | |
| # CODE_MOWER_GATE_AUTOMERGE_BEGIN | |
| automerge_token="${CODE_MOWER_GATE_AUTOMERGE_TOKEN:-${GH_TOKEN:-}}" | |
| automerge_attempt=1 | |
| automerge_max_attempts="${CODE_MOWER_GATE_AUTOMERGE_MAX_ATTEMPTS:-5}" | |
| automerge_delay="${CODE_MOWER_GATE_AUTOMERGE_RETRY_DELAY:-5}" | |
| case "${automerge_max_attempts}" in | |
| ''|*[!0-9]*) automerge_max_attempts=5 ;; | |
| esac | |
| case "${automerge_delay}" in | |
| ''|*[!0-9]*) automerge_delay=5 ;; | |
| esac | |
| while true; do | |
| automerge_stdout_file="$(mktemp)" | |
| automerge_stderr_file="$(mktemp)" | |
| if GH_TOKEN="${automerge_token}" gh pr merge "${PR_NUMBER}" \ | |
| --repo "${GITHUB_REPOSITORY}" \ | |
| --auto --squash --match-head-commit "${HEAD_SHA}" \ | |
| >"${automerge_stdout_file}" 2>"${automerge_stderr_file}"; then | |
| automerge_result="$(head -n 1 "${automerge_stdout_file}")" | |
| rm -f "${automerge_stdout_file}" "${automerge_stderr_file}" | |
| echo "::notice::Code Mower gate merge request accepted for head ${HEAD_SHA}: ${automerge_result:-merged or queued}." | |
| break | |
| fi | |
| automerge_error="$(head -n 1 "${automerge_stderr_file}")" | |
| rm -f "${automerge_stdout_file}" "${automerge_stderr_file}" | |
| case "${automerge_error}" in | |
| *[Uu]nstable*|*[Rr]ate\ limit*|*try\ again*|*timed\ out*|*[Tt]imeout*|*deadline\ exceeded*|*HTTP\ 502*|*HTTP\ 503*|*HTTP\ 504*|*Bad\ gateway*|*Service\ unavailable*) | |
| automerge_retry=true | |
| ;; | |
| *) | |
| automerge_retry=false | |
| ;; | |
| esac | |
| if [ "${automerge_retry}" != "true" ]; then | |
| echo "::notice::Code Mower gate is green, but auto-merge was refused: $(printf '%.200s' "${automerge_error:-unknown error}"). Confirm token permissions, branch policy, and PR head." | |
| break | |
| fi | |
| if [ "${automerge_attempt}" -ge "${automerge_max_attempts}" ]; then | |
| echo "::notice::Code Mower gate is green, but auto-merge stayed transiently unavailable after ${automerge_attempt} attempts: $(printf '%.200s' "${automerge_error:-unknown error}"). A later gate run or manual merge can complete it." | |
| break | |
| fi | |
| automerge_attempt="$((automerge_attempt + 1))" | |
| sleep "${automerge_delay}" | |
| automerge_delay="$((automerge_delay * 2))" | |
| done | |
| # CODE_MOWER_GATE_AUTOMERGE_END |