code-mower 0.5.0b5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- code_mower/__init__.py +3 -0
- code_mower/adapters/__init__.py +39 -0
- code_mower/adapters/_base.py +148 -0
- code_mower/adapters/cursor_bugbot.py +97 -0
- code_mower/adapters/gitar.py +111 -0
- code_mower/adapters/greptile.py +191 -0
- code_mower/adapters/qodo.py +140 -0
- code_mower/antigravity_cli_audit_pr.py +241 -0
- code_mower/audit_handoff_log.py +345 -0
- code_mower/audit_labeler_lib.py +311 -0
- code_mower/audit_progress.py +203 -0
- code_mower/blind_review_artifacts.py +562 -0
- code_mower/blind_review_coordinator.py +276 -0
- code_mower/bootstrap.py +524 -0
- code_mower/builder_experiment.py +539 -0
- code_mower/calibration/__init__.py +181 -0
- code_mower/calibration/arms.py +267 -0
- code_mower/calibration/auto_discovery.py +348 -0
- code_mower/calibration/commands.py +192 -0
- code_mower/calibration/context_inputs.py +199 -0
- code_mower/calibration/corpus.py +91 -0
- code_mower/calibration/evidence.py +20 -0
- code_mower/calibration/evidence_report.py +360 -0
- code_mower/calibration/identity.py +27 -0
- code_mower/calibration/metrics.py +18 -0
- code_mower/calibration/overlap.py +103 -0
- code_mower/calibration/planning.py +309 -0
- code_mower/calibration/policy.py +207 -0
- code_mower/calibration/results.py +315 -0
- code_mower/calibration/run_results.py +147 -0
- code_mower/calibration/run_status.py +64 -0
- code_mower/calibration/runner.py +360 -0
- code_mower/calibration/truth.py +188 -0
- code_mower/calibration/value_report.py +142 -0
- code_mower/checks.py +402 -0
- code_mower/claude_audit_pr.py +1126 -0
- code_mower/claude_cli_bounce.py +303 -0
- code_mower/claude_cli_environment.py +73 -0
- code_mower/clear_stale.py +374 -0
- code_mower/cli.py +537 -0
- code_mower/cloud.py +674 -0
- code_mower/cloud_client/__init__.py +173 -0
- code_mower/cloud_client/bundle.py +155 -0
- code_mower/cloud_client/doctor.py +206 -0
- code_mower/cloud_client/dogfood.py +78 -0
- code_mower/cloud_client/endpoints.py +114 -0
- code_mower/cloud_client/errors.py +7 -0
- code_mower/cloud_client/events.py +278 -0
- code_mower/cloud_client/export.py +272 -0
- code_mower/cloud_client/git_metadata.py +46 -0
- code_mower/cloud_client/manifest.py +39 -0
- code_mower/cloud_client/operations.py +448 -0
- code_mower/cloud_client/reports.py +45 -0
- code_mower/cloud_client/setup.py +205 -0
- code_mower/cloud_client/upload.py +97 -0
- code_mower/code_mower_calibration.py +598 -0
- code_mower/code_mower_context_packs.py +591 -0
- code_mower/code_mower_merge.py +227 -0
- code_mower/code_mower_telemetry.py +561 -0
- code_mower/coderabbit_cli_audit_pr.py +526 -0
- code_mower/codex_audit_env_preflight.py +220 -0
- code_mower/codex_audit_pr.py +1738 -0
- code_mower/codex_audit_schema_smoke.py +160 -0
- code_mower/codex_audit_verdict.schema.json +44 -0
- code_mower/config.py +655 -0
- code_mower/doctor.py +161 -0
- code_mower/doctor_checks/__init__.py +104 -0
- code_mower/doctor_checks/cloud.py +129 -0
- code_mower/doctor_checks/common.py +215 -0
- code_mower/doctor_checks/github.py +128 -0
- code_mower/doctor_checks/github_actions.py +11 -0
- code_mower/doctor_checks/github_actions_cost.py +99 -0
- code_mower/doctor_checks/github_actions_cost_summary.py +111 -0
- code_mower/doctor_checks/github_actions_failure_annotations.py +27 -0
- code_mower/doctor_checks/github_actions_failure_models.py +47 -0
- code_mower/doctor_checks/github_actions_failure_scan.py +200 -0
- code_mower/doctor_checks/github_actions_failure_selection.py +63 -0
- code_mower/doctor_checks/github_actions_failures.py +103 -0
- code_mower/doctor_checks/github_actions_permissions.py +55 -0
- code_mower/doctor_checks/github_api.py +79 -0
- code_mower/doctor_checks/github_branch.py +56 -0
- code_mower/doctor_checks/github_config.py +25 -0
- code_mower/doctor_checks/github_provider.py +61 -0
- code_mower/doctor_checks/github_repo.py +120 -0
- code_mower/doctor_checks/groups.py +36 -0
- code_mower/doctor_checks/models.py +96 -0
- code_mower/doctor_checks/output.py +86 -0
- code_mower/doctor_checks/presets.py +64 -0
- code_mower/doctor_checks/privacy.py +20 -0
- code_mower/doctor_checks/provider_api_model.py +138 -0
- code_mower/doctor_checks/provider_api_model_openai.py +29 -0
- code_mower/doctor_checks/provider_api_model_profiles.py +137 -0
- code_mower/doctor_checks/provider_env.py +113 -0
- code_mower/doctor_checks/provider_env_required.py +56 -0
- code_mower/doctor_checks/provider_env_tokens.py +100 -0
- code_mower/doctor_checks/provider_local_cli.py +162 -0
- code_mower/doctor_checks/provider_local_cli_commands.py +47 -0
- code_mower/doctor_checks/provider_local_cli_probe_config.py +70 -0
- code_mower/doctor_checks/provider_probe.py +20 -0
- code_mower/doctor_checks/provider_probe_auth.py +52 -0
- code_mower/doctor_checks/provider_probe_evaluation.py +109 -0
- code_mower/doctor_checks/provider_probe_json.py +45 -0
- code_mower/doctor_checks/provider_probe_remediation.py +39 -0
- code_mower/doctor_checks/providers.py +159 -0
- code_mower/doctor_checks/registry.py +69 -0
- code_mower/doctor_checks/runner.py +188 -0
- code_mower/doctor_checks/runtime.py +89 -0
- code_mower/doctor_checks/runtime_github_auth.py +148 -0
- code_mower/gemini_cli_audit_pr.py +897 -0
- code_mower/hermes_cli_audit_pr.py +436 -0
- code_mower/init.py +888 -0
- code_mower/lane_configs/__init__.py +37 -0
- code_mower/lane_configs/aider.py +32 -0
- code_mower/lane_configs/antigravity_cli.py +35 -0
- code_mower/lane_configs/claude.py +35 -0
- code_mower/lane_configs/codex.py +32 -0
- code_mower/lane_configs/devin.py +33 -0
- code_mower/lane_configs/gemini_cli.py +35 -0
- code_mower/lane_configs/hermes_cli.py +35 -0
- code_mower/lane_configs/local_llm.py +31 -0
- code_mower/local_llm_audit_pr.py +1364 -0
- code_mower/local_llm_bakeoff.py +458 -0
- code_mower/local_llm_calibration.py +441 -0
- code_mower/local_llm_profiles.py +66 -0
- code_mower/migration.py +508 -0
- code_mower/migration_install.py +292 -0
- code_mower/migration_mirror.py +392 -0
- code_mower/migration_readiness.py +237 -0
- code_mower/migration_rehearsal.py +718 -0
- code_mower/next_steps.py +441 -0
- code_mower/package.py +673 -0
- code_mower/package_content.py +444 -0
- code_mower/package_manifest.py +452 -0
- code_mower/package_paths.py +53 -0
- code_mower/package_rendering.py +90 -0
- code_mower/package_static.py +585 -0
- code_mower/prompts.py +267 -0
- code_mower/provider_registry.py +469 -0
- code_mower/provider_runners/__init__.py +60 -0
- code_mower/provider_runners/comments.py +31 -0
- code_mower/provider_runners/git.py +46 -0
- code_mower/provider_runners/github_auth.py +61 -0
- code_mower/provider_runners/github_pr.py +120 -0
- code_mower/provider_runners/process.py +58 -0
- code_mower/provider_runners/repo_paths.py +23 -0
- code_mower/provider_runners/text_schema.py +41 -0
- code_mower/provider_runners/verdict_artifacts.py +103 -0
- code_mower/provider_runners/workspace.py +57 -0
- code_mower/release_readiness.py +549 -0
- code_mower/reviewer_metrics.py +389 -0
- code_mower/saas_reviewer_labeler.py +809 -0
- code_mower/secrets.py +89 -0
- code_mower/templates/builder-experiment.example.json +55 -0
- code_mower/templates/calibration-corpus.example.json +129 -0
- code_mower/templates/calibration-corpus.json +129 -0
- code_mower/templates/code-mower.example.yml +423 -0
- code_mower/templates/context-packs.example.json +150 -0
- code_mower/templates/lane_prompts/base-audit.md +22 -0
- code_mower/templates/lane_prompts/calibration-policy.md +21 -0
- code_mower/templates/lane_prompts/context-driven-quality.md +21 -0
- code_mower/templates/lane_prompts/docs-design.md +12 -0
- code_mower/templates/lane_prompts/generic-programming.md +21 -0
- code_mower/templates/lane_prompts/operability.md +22 -0
- code_mower/templates/lane_prompts/package-runtime.md +12 -0
- code_mower/templates/lane_prompts/security-threat-model.md +22 -0
- code_mower/templates/product-support/code_mower +216 -0
- code_mower/templates/product-support/code_mower_standalone_pin.env +7 -0
- code_mower/templates/product-support/code_mower_standalone_shadow.sh +151 -0
- code_mower/templates/product-support/run_claude_audit_pr.sh +32 -0
- code_mower/templates/product-support/run_codex_audit_pr.sh +32 -0
- code_mower/templates/product-support/safe_gh_comment.py +96 -0
- code_mower/templates/providers.yml +454 -0
- code_mower/templates/reviewer-spend.example.json +28 -0
- code_mower/templates/reviewer-value-report.example.md +20 -0
- code_mower/templates/workflows/private-standalone-shadow.yml.j2 +106 -0
- code_mower/templates/workflows/review-clear-stale.yml.j2 +83 -0
- code_mower/trailer_comment_labeler.py +207 -0
- code_mower/versioning.py +32 -0
- code_mower-0.5.0b5.dist-info/METADATA +302 -0
- code_mower-0.5.0b5.dist-info/RECORD +185 -0
- code_mower-0.5.0b5.dist-info/WHEEL +5 -0
- code_mower-0.5.0b5.dist-info/entry_points.txt +2 -0
- code_mower-0.5.0b5.dist-info/licenses/LICENSE +202 -0
- code_mower-0.5.0b5.dist-info/licenses/NOTICE +10 -0
- code_mower-0.5.0b5.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Clear stale terminal audit labels after a pull request head changes."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import sys
|
|
10
|
+
from dataclasses import dataclass
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any, Optional, Sequence
|
|
13
|
+
|
|
14
|
+
if __package__ and __package__.startswith("code_mower"):
|
|
15
|
+
from .audit_labeler_lib import (
|
|
16
|
+
GitHubToken,
|
|
17
|
+
IssueCommentPaginationLimitExceeded,
|
|
18
|
+
LabelDecision,
|
|
19
|
+
apply_label_decision,
|
|
20
|
+
fetch_issue_comments,
|
|
21
|
+
fetch_issue_labels,
|
|
22
|
+
fetch_pull_request,
|
|
23
|
+
extract_reviewed_sha,
|
|
24
|
+
github_request_with_fallback,
|
|
25
|
+
load_json,
|
|
26
|
+
sha_matches_reviewed_head,
|
|
27
|
+
)
|
|
28
|
+
from .lane_configs import load_lane_config
|
|
29
|
+
from .trailer_comment_labeler import classify_audit_comment
|
|
30
|
+
else:
|
|
31
|
+
try:
|
|
32
|
+
from tools.audit_labeler_lib import (
|
|
33
|
+
GitHubToken,
|
|
34
|
+
IssueCommentPaginationLimitExceeded,
|
|
35
|
+
LabelDecision,
|
|
36
|
+
apply_label_decision,
|
|
37
|
+
fetch_issue_comments,
|
|
38
|
+
fetch_issue_labels,
|
|
39
|
+
fetch_pull_request,
|
|
40
|
+
extract_reviewed_sha,
|
|
41
|
+
github_request_with_fallback,
|
|
42
|
+
load_json,
|
|
43
|
+
sha_matches_reviewed_head,
|
|
44
|
+
)
|
|
45
|
+
from tools.lane_configs import load_lane_config
|
|
46
|
+
from tools.trailer_comment_labeler import classify_audit_comment
|
|
47
|
+
except ImportError: # pragma: no cover - direct `python tools/foo.py` execution
|
|
48
|
+
from audit_labeler_lib import (
|
|
49
|
+
GitHubToken,
|
|
50
|
+
IssueCommentPaginationLimitExceeded,
|
|
51
|
+
LabelDecision,
|
|
52
|
+
apply_label_decision,
|
|
53
|
+
fetch_issue_comments,
|
|
54
|
+
fetch_issue_labels,
|
|
55
|
+
fetch_pull_request,
|
|
56
|
+
extract_reviewed_sha,
|
|
57
|
+
github_request_with_fallback,
|
|
58
|
+
load_json,
|
|
59
|
+
sha_matches_reviewed_head,
|
|
60
|
+
)
|
|
61
|
+
from lane_configs import load_lane_config
|
|
62
|
+
from trailer_comment_labeler import classify_audit_comment
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@dataclass(frozen=True)
|
|
66
|
+
class TrustedTerminalComment:
|
|
67
|
+
status: str
|
|
68
|
+
reviewed_sha: Optional[str]
|
|
69
|
+
author: str
|
|
70
|
+
created_at: str
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _status_label(config: Any, status: str) -> str:
|
|
74
|
+
return config.done_label if status == "done" else config.blocked_label
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@dataclass(frozen=True)
|
|
78
|
+
class StaleClearResult:
|
|
79
|
+
decision: Optional[LabelDecision]
|
|
80
|
+
reason: str
|
|
81
|
+
requeue_added: bool = False
|
|
82
|
+
dispatch_requested: bool = False
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def _comment_created_at(comment: dict[str, Any]) -> str:
|
|
86
|
+
return str(comment.get("created_at") or "")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _extract_reviewed_sha(body: str) -> Optional[str]:
|
|
90
|
+
# Keep this local wrapper so no-SHA fallback comments never preserve terminal labels.
|
|
91
|
+
return extract_reviewed_sha(body)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def latest_trusted_terminal_comment(
|
|
95
|
+
comments: Sequence[dict[str, Any]],
|
|
96
|
+
*,
|
|
97
|
+
config: Any,
|
|
98
|
+
) -> Optional[TrustedTerminalComment]:
|
|
99
|
+
trusted_authors = config.comment_authors()
|
|
100
|
+
ordered = sorted(comments, key=_comment_created_at, reverse=True)
|
|
101
|
+
for comment in ordered:
|
|
102
|
+
author = str(((comment.get("user") or {}).get("login") or ""))
|
|
103
|
+
if author.lower() not in trusted_authors:
|
|
104
|
+
continue
|
|
105
|
+
body = str(comment.get("body") or "")
|
|
106
|
+
status = classify_audit_comment(body, config)
|
|
107
|
+
if status not in {"done", "blocked"}:
|
|
108
|
+
continue
|
|
109
|
+
reviewed_sha = _extract_reviewed_sha(body)
|
|
110
|
+
return TrustedTerminalComment(
|
|
111
|
+
status=status,
|
|
112
|
+
reviewed_sha=reviewed_sha,
|
|
113
|
+
author=author,
|
|
114
|
+
created_at=_comment_created_at(comment),
|
|
115
|
+
)
|
|
116
|
+
return None
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def resolve_stale_clear_decision(
|
|
120
|
+
*,
|
|
121
|
+
issue_number: int,
|
|
122
|
+
current_head_sha: str,
|
|
123
|
+
labels: Sequence[str],
|
|
124
|
+
comments: Sequence[dict[str, Any]],
|
|
125
|
+
config: Any,
|
|
126
|
+
) -> StaleClearResult:
|
|
127
|
+
label_set = set(labels)
|
|
128
|
+
terminal_labels = {config.done_label, config.blocked_label}
|
|
129
|
+
present_terminal = terminal_labels.intersection(label_set)
|
|
130
|
+
if not present_terminal:
|
|
131
|
+
return StaleClearResult(None, "no terminal audit labels present")
|
|
132
|
+
|
|
133
|
+
current_comment = latest_trusted_terminal_comment(comments, config=config)
|
|
134
|
+
if (
|
|
135
|
+
current_comment
|
|
136
|
+
and current_comment.reviewed_sha
|
|
137
|
+
and sha_matches_reviewed_head(
|
|
138
|
+
current_comment.reviewed_sha,
|
|
139
|
+
current_head_sha,
|
|
140
|
+
)
|
|
141
|
+
):
|
|
142
|
+
expected_label = _status_label(config, current_comment.status)
|
|
143
|
+
wrong_terminal_labels = tuple(sorted(present_terminal - {expected_label}))
|
|
144
|
+
if expected_label in label_set and not wrong_terminal_labels:
|
|
145
|
+
return StaleClearResult(
|
|
146
|
+
None,
|
|
147
|
+
(
|
|
148
|
+
f"terminal label is current: {current_comment.status} by "
|
|
149
|
+
f"{current_comment.author} for {current_comment.reviewed_sha}"
|
|
150
|
+
),
|
|
151
|
+
)
|
|
152
|
+
return StaleClearResult(
|
|
153
|
+
LabelDecision(
|
|
154
|
+
issue_number=issue_number,
|
|
155
|
+
add_label=expected_label,
|
|
156
|
+
remove_labels=(config.needs_label, *wrong_terminal_labels),
|
|
157
|
+
reviewed_sha=current_comment.reviewed_sha,
|
|
158
|
+
reason=(
|
|
159
|
+
f"repaired terminal label mismatch for current head "
|
|
160
|
+
f"{current_head_sha}: expected {expected_label}"
|
|
161
|
+
),
|
|
162
|
+
),
|
|
163
|
+
(
|
|
164
|
+
"terminal audit label mismatch for current trusted "
|
|
165
|
+
f"{current_comment.status} result"
|
|
166
|
+
),
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
decision = LabelDecision(
|
|
170
|
+
issue_number=issue_number,
|
|
171
|
+
add_label=config.needs_label,
|
|
172
|
+
remove_labels=(config.done_label, config.blocked_label),
|
|
173
|
+
reviewed_sha=current_comment.reviewed_sha if current_comment else None,
|
|
174
|
+
reason=(
|
|
175
|
+
f"cleared stale terminal labels after head moved to {current_head_sha}"
|
|
176
|
+
),
|
|
177
|
+
)
|
|
178
|
+
return StaleClearResult(
|
|
179
|
+
decision,
|
|
180
|
+
"terminal audit label is stale or lacks a current trusted Head SHA",
|
|
181
|
+
requeue_added=config.needs_label not in label_set,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _event_pr_context(event: dict[str, Any]) -> tuple[int, Optional[str], list[str]]:
|
|
186
|
+
pull_request = event.get("pull_request") or {}
|
|
187
|
+
issue_number = int(pull_request.get("number") or event.get("number") or 0)
|
|
188
|
+
head_sha = ((pull_request.get("head") or {}).get("sha") or None)
|
|
189
|
+
labels = [
|
|
190
|
+
str(label.get("name") or "")
|
|
191
|
+
for label in pull_request.get("labels") or []
|
|
192
|
+
if str(label.get("name") or "")
|
|
193
|
+
]
|
|
194
|
+
return issue_number, head_sha, labels
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
def _render_dispatch_input(template: str, *, issue_number: int, head_sha: str, lane: str) -> str:
|
|
198
|
+
return (
|
|
199
|
+
template.replace("{pr}", str(issue_number))
|
|
200
|
+
.replace("{pr_number}", str(issue_number))
|
|
201
|
+
.replace("{head}", head_sha)
|
|
202
|
+
.replace("{head_sha}", head_sha)
|
|
203
|
+
.replace("{lane}", lane)
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _parse_dispatch_inputs(
|
|
208
|
+
entries: Sequence[str],
|
|
209
|
+
*,
|
|
210
|
+
issue_number: int,
|
|
211
|
+
head_sha: str,
|
|
212
|
+
lane: str,
|
|
213
|
+
) -> dict[str, str]:
|
|
214
|
+
inputs: dict[str, str] = {}
|
|
215
|
+
for entry in entries:
|
|
216
|
+
if "=" not in entry:
|
|
217
|
+
raise ValueError(f"dispatch input must be name=value: {entry}")
|
|
218
|
+
name, value = entry.split("=", 1)
|
|
219
|
+
name = name.strip()
|
|
220
|
+
if not name:
|
|
221
|
+
raise ValueError(f"dispatch input has an empty name: {entry}")
|
|
222
|
+
inputs[name] = _render_dispatch_input(
|
|
223
|
+
value,
|
|
224
|
+
issue_number=issue_number,
|
|
225
|
+
head_sha=head_sha,
|
|
226
|
+
lane=lane,
|
|
227
|
+
)
|
|
228
|
+
return inputs
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def dispatch_workflow(
|
|
232
|
+
repo: str,
|
|
233
|
+
workflow: str,
|
|
234
|
+
*,
|
|
235
|
+
ref: str,
|
|
236
|
+
inputs: dict[str, str],
|
|
237
|
+
tokens: Sequence[GitHubToken],
|
|
238
|
+
) -> None:
|
|
239
|
+
github_request_with_fallback(
|
|
240
|
+
"POST",
|
|
241
|
+
f"/repos/{repo}/actions/workflows/{workflow}/dispatches",
|
|
242
|
+
tokens=tokens,
|
|
243
|
+
body={"ref": ref, "inputs": inputs},
|
|
244
|
+
)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _default_dispatch_ref(event: dict[str, Any]) -> str:
|
|
248
|
+
pull_request = event.get("pull_request") or {}
|
|
249
|
+
base = pull_request.get("base") or {}
|
|
250
|
+
repo = event.get("repository") or {}
|
|
251
|
+
return str(base.get("ref") or repo.get("default_branch") or os.environ.get("GITHUB_REF_NAME") or "main")
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def main(argv: Optional[Sequence[str]] = None) -> int:
|
|
255
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
256
|
+
parser.add_argument("--lane", required=True, help="Trailer audit lane from lane_configs/.")
|
|
257
|
+
parser.add_argument("--repo", default=os.environ.get("GITHUB_REPOSITORY", ""))
|
|
258
|
+
parser.add_argument("--pr", type=int, default=0)
|
|
259
|
+
parser.add_argument("--head-sha", default="")
|
|
260
|
+
parser.add_argument("--event-path", default=os.environ.get("GITHUB_EVENT_PATH", ""))
|
|
261
|
+
parser.add_argument("--page-cap", type=int, default=5)
|
|
262
|
+
parser.add_argument("--dry-run", action="store_true")
|
|
263
|
+
parser.add_argument("--json", action="store_true")
|
|
264
|
+
parser.add_argument(
|
|
265
|
+
"--dispatch-workflow",
|
|
266
|
+
default="",
|
|
267
|
+
help="optional workflow filename/id to workflow_dispatch after a new stale requeue",
|
|
268
|
+
)
|
|
269
|
+
parser.add_argument("--dispatch-ref", default="")
|
|
270
|
+
parser.add_argument(
|
|
271
|
+
"--dispatch-input",
|
|
272
|
+
action="append",
|
|
273
|
+
default=[],
|
|
274
|
+
help="workflow_dispatch input as name=value; supports {pr}, {head}, and {lane}",
|
|
275
|
+
)
|
|
276
|
+
args = parser.parse_args(argv)
|
|
277
|
+
|
|
278
|
+
config = load_lane_config(args.lane)
|
|
279
|
+
tokens = config.github_tokens_from_env()
|
|
280
|
+
if not args.dry_run and not tokens:
|
|
281
|
+
token_names = " or ".join(config.token_env_vars)
|
|
282
|
+
print(f"error: {token_names} is required", file=sys.stderr)
|
|
283
|
+
return 1
|
|
284
|
+
|
|
285
|
+
event = load_json(Path(args.event_path)) if args.event_path else {}
|
|
286
|
+
repo = args.repo or str((event.get("repository") or {}).get("full_name") or "")
|
|
287
|
+
if not repo:
|
|
288
|
+
print("error: --repo or GITHUB_REPOSITORY is required", file=sys.stderr)
|
|
289
|
+
return 1
|
|
290
|
+
|
|
291
|
+
event_issue_number, event_head_sha, event_labels = _event_pr_context(event)
|
|
292
|
+
issue_number = args.pr or event_issue_number
|
|
293
|
+
current_head_sha = args.head_sha or event_head_sha or ""
|
|
294
|
+
labels = event_labels
|
|
295
|
+
if not args.dry_run:
|
|
296
|
+
if not issue_number:
|
|
297
|
+
print("error: --pr or pull_request event is required", file=sys.stderr)
|
|
298
|
+
return 1
|
|
299
|
+
pull_request = fetch_pull_request(repo, issue_number, tokens=tokens)
|
|
300
|
+
current_head_sha = str((pull_request.get("head") or {}).get("sha") or current_head_sha)
|
|
301
|
+
labels = fetch_issue_labels(repo, issue_number, tokens=tokens)
|
|
302
|
+
try:
|
|
303
|
+
comments = fetch_issue_comments(
|
|
304
|
+
repo,
|
|
305
|
+
issue_number,
|
|
306
|
+
tokens=tokens,
|
|
307
|
+
page_cap=args.page_cap,
|
|
308
|
+
)
|
|
309
|
+
except IssueCommentPaginationLimitExceeded as exc:
|
|
310
|
+
# Conservative for merge-authority lanes: if we cannot inspect enough
|
|
311
|
+
# comments to prove a terminal label is current, clear and requeue
|
|
312
|
+
# rather than leaving stale approval authoritative.
|
|
313
|
+
print(f"warning: {exc}; using conservative stale requeue", file=sys.stderr)
|
|
314
|
+
comments = []
|
|
315
|
+
else:
|
|
316
|
+
comments = list((event.get("comments") or []))
|
|
317
|
+
|
|
318
|
+
if not issue_number or not current_head_sha:
|
|
319
|
+
print("error: PR number and head SHA are required", file=sys.stderr)
|
|
320
|
+
return 1
|
|
321
|
+
|
|
322
|
+
result = resolve_stale_clear_decision(
|
|
323
|
+
issue_number=issue_number,
|
|
324
|
+
current_head_sha=current_head_sha,
|
|
325
|
+
labels=labels,
|
|
326
|
+
comments=comments,
|
|
327
|
+
config=config,
|
|
328
|
+
)
|
|
329
|
+
|
|
330
|
+
dispatch_requested = False
|
|
331
|
+
if result.decision is not None and not args.dry_run:
|
|
332
|
+
apply_label_decision(repo, result.decision, tokens=tokens)
|
|
333
|
+
if args.dispatch_workflow and result.requeue_added:
|
|
334
|
+
dispatch_ref = args.dispatch_ref or _default_dispatch_ref(event)
|
|
335
|
+
dispatch_inputs = _parse_dispatch_inputs(
|
|
336
|
+
args.dispatch_input,
|
|
337
|
+
issue_number=issue_number,
|
|
338
|
+
head_sha=current_head_sha,
|
|
339
|
+
lane=config.name,
|
|
340
|
+
)
|
|
341
|
+
dispatch_workflow(
|
|
342
|
+
repo,
|
|
343
|
+
args.dispatch_workflow,
|
|
344
|
+
ref=dispatch_ref,
|
|
345
|
+
inputs=dispatch_inputs,
|
|
346
|
+
tokens=tokens,
|
|
347
|
+
)
|
|
348
|
+
dispatch_requested = True
|
|
349
|
+
|
|
350
|
+
output = {
|
|
351
|
+
"lane": config.name,
|
|
352
|
+
"repo": repo,
|
|
353
|
+
"pr": issue_number,
|
|
354
|
+
"head_sha": current_head_sha,
|
|
355
|
+
"reason": result.reason,
|
|
356
|
+
"decision": result.decision.__dict__ if result.decision else None,
|
|
357
|
+
"requeue_added": result.requeue_added,
|
|
358
|
+
"dispatch_requested": dispatch_requested,
|
|
359
|
+
}
|
|
360
|
+
if args.json or args.dry_run:
|
|
361
|
+
print(json.dumps(output, indent=2, sort_keys=True))
|
|
362
|
+
elif result.decision:
|
|
363
|
+
print(
|
|
364
|
+
f"applied: add {result.decision.add_label}; remove "
|
|
365
|
+
f"{', '.join(result.decision.remove_labels)} on {repo}#{issue_number} "
|
|
366
|
+
f"({result.reason})"
|
|
367
|
+
)
|
|
368
|
+
else:
|
|
369
|
+
print(f"skip: {result.reason}")
|
|
370
|
+
return 0
|
|
371
|
+
|
|
372
|
+
|
|
373
|
+
if __name__ == "__main__":
|
|
374
|
+
raise SystemExit(main())
|