@matt82198/aesop 0.1.0 → 0.3.1
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.
- package/CHANGELOG.md +117 -2
- package/README.md +59 -218
- package/bin/cli.js +168 -41
- package/daemons/run-watchdog.sh +16 -4
- package/daemons/selfheal.sh +231 -0
- package/docs/ANY-REPO.md +427 -0
- package/docs/CONTRIBUTING.md +72 -0
- package/docs/DEMO.md +334 -0
- package/docs/HOOK-INSTALL.md +15 -56
- package/docs/INSTALL.md +74 -4
- package/docs/PORTING.md +166 -0
- package/docs/README.md +33 -3
- package/docs/TEAM-STATE.md +372 -0
- package/docs/reproduce.md +33 -3
- package/driver/CLAUDE.md +150 -0
- package/driver/README.md +383 -0
- package/driver/aesop.config.example.json +80 -0
- package/driver/agent_driver.py +355 -0
- package/driver/backend_config.py +253 -0
- package/driver/claude_code_driver.py +198 -0
- package/driver/codex_driver.py +673 -0
- package/driver/openai_compatible_driver.py +249 -0
- package/driver/openai_transport.py +179 -0
- package/driver/verification_policy.py +75 -0
- package/driver/wave_bridge.py +254 -0
- package/driver/wave_loop.py +1408 -0
- package/driver/wave_scheduler.py +890 -0
- package/hooks/pre-push-policy.sh +131 -33
- package/mcp/server.mjs +320 -4
- package/monitor/collect-signals.mjs +69 -0
- package/package.json +22 -14
- package/skills/CLAUDE.md +132 -2
- package/skills/buildsystem/SKILL.md +330 -0
- package/skills/buildsystem/wave-flat-dispatch.template.mjs +658 -0
- package/skills/fleet/SKILL.md +113 -0
- package/skills/power/SKILL.md +246 -131
- package/state_store/__init__.py +4 -2
- package/state_store/api.py +19 -4
- package/state_store/coordination.py +209 -0
- package/state_store/identity.py +51 -0
- package/state_store/projections.py +63 -0
- package/state_store/read_api.py +156 -0
- package/state_store/store.py +185 -73
- package/state_store/write_api.py +462 -0
- package/templates/wave-presets/data.json +65 -0
- package/templates/wave-presets/library.json +65 -0
- package/templates/wave-presets/saas.json +64 -0
- package/tools/audit_report.py +388 -0
- package/tools/bench_runner.py +100 -3
- package/tools/ci_merge_wait.py +256 -35
- package/tools/ci_workflow_lint.py +430 -0
- package/tools/claudemd_drift.py +394 -0
- package/tools/claudemd_lint.py +359 -0
- package/tools/common.py +39 -3
- package/tools/cost_ceiling.py +166 -43
- package/tools/cost_econ.py +480 -0
- package/tools/cost_projection.py +559 -0
- package/tools/crossos_drift.py +394 -0
- package/tools/defect_escape.py +252 -0
- package/tools/doctor.js +1 -1
- package/tools/eod_sweep.py +188 -26
- package/tools/fleet.js +260 -0
- package/tools/fleet_ledger.py +209 -7
- package/tools/git_identity_check.py +315 -0
- package/tools/health-score.js +40 -0
- package/tools/health_score.py +361 -0
- package/tools/metrics_gate.py +13 -4
- package/tools/mutation_test.py +523 -0
- package/tools/portability_check.py +206 -0
- package/tools/proposals.mjs +47 -2
- package/tools/reconcile.py +7 -4
- package/tools/reproduce.js +405 -0
- package/tools/secret_scan.py +207 -65
- package/tools/self_stats.py +20 -0
- package/tools/stall_check.py +247 -16
- package/tools/stateapi_lint.py +325 -0
- package/tools/test_battery.py +173 -0
- package/tools/transcript_digest.py +380 -0
- package/tools/verify_activity_filter.py +437 -0
- package/tools/verify_agent_inspector.py +2 -0
- package/tools/verify_cost_panel.py +345 -0
- package/tools/verify_dash.py +2 -0
- package/tools/verify_dispatch_panel.py +301 -0
- package/tools/verify_failure_drilldown.py +188 -0
- package/tools/verify_prboard.py +2 -0
- package/tools/verify_scorecards.py +281 -0
- package/tools/verify_submit_encoding.py +2 -0
- package/tools/verify_ui_trio.py +409 -0
- package/tools/verify_wave_telemetry.py +268 -0
- package/tools/wave_backlog_analyzer.py +490 -0
- package/tools/wave_ledger_hook.py +150 -0
- package/tools/wave_preflight.py +779 -0
- package/tools/wave_resume.py +215 -0
- package/tools/wave_templates.py +340 -0
- package/ui/agents.py +68 -14
- package/ui/collectors.py +81 -55
- package/ui/config.py +7 -2
- package/ui/cost.py +231 -12
- package/ui/handler.py +383 -55
- package/ui/quality_scorecard.py +232 -0
- package/ui/sse.py +3 -3
- package/ui/wave_audit_tail.py +213 -0
- package/ui/wave_dispatch.py +280 -0
- package/ui/wave_failure.py +288 -0
- package/ui/wave_gantt.py +152 -0
- package/ui/wave_reasoning_tail.py +176 -0
- package/ui/wave_telemetry.py +383 -0
- package/ui/web/dist/assets/index-CNQxaiOW.css +1 -0
- package/ui/web/dist/assets/index-CP68RIh3.js +9 -0
- package/ui/web/dist/index.html +2 -2
- package/bin/CLAUDE.md +0 -76
- package/daemons/CLAUDE.md +0 -36
- package/dash/CLAUDE.md +0 -32
- package/docs/archive/README.md +0 -3
- package/docs/archive/spikes/tiered-cognition/ACTIVATION.md +0 -125
- package/docs/archive/spikes/tiered-cognition/DESIGN.md +0 -287
- package/docs/archive/spikes/tiered-cognition/FINDINGS.md +0 -113
- package/docs/archive/spikes/tiered-cognition/README.md +0 -27
- package/docs/archive/spikes/tiered-cognition/aesop-cognition.example.md +0 -32
- package/docs/archive/spikes/tiered-cognition/force-model-policy.merged.mjs +0 -673
- package/docs/archive/spikes/tiered-cognition/strip-tools-hook.mjs +0 -434
- package/hooks/CLAUDE.md +0 -89
- package/mcp/CLAUDE.md +0 -213
- package/monitor/CLAUDE.md +0 -40
- package/scan/CLAUDE.md +0 -30
- package/state_store/CLAUDE.md +0 -39
- package/tools/CLAUDE.md +0 -79
- package/ui/CLAUDE.md +0 -127
- package/ui/web/dist/assets/index-0qQYnvMC.js +0 -9
- package/ui/web/dist/assets/index-BdIlFieV.css +0 -1
package/tools/ci_merge_wait.py
CHANGED
|
@@ -1,21 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
"""
|
|
3
|
-
ci_merge_wait.py
|
|
3
|
+
ci_merge_wait.py - CI-gated merge helper: wait for PR checks to conclude, then merge.
|
|
4
4
|
|
|
5
5
|
Polls gh pr view until all status checks conclude (SUCCESS/FAILURE), then merges ONLY
|
|
6
6
|
if all checks are SUCCESS. The gh pr merge call is STRUCTURALLY UNREACHABLE unless the
|
|
7
|
-
status is SUCCESS
|
|
7
|
+
status is SUCCESS - this is the whole point (prevents merge-on-CI-failure edge cases).
|
|
8
|
+
|
|
9
|
+
Fail-closed semantics: empty rollup -> PENDING by default; use --allow-no-checks to treat
|
|
10
|
+
empty rollups as SUCCESS. Use --expect-checks to require specific named checks to be present
|
|
11
|
+
and successful before merging.
|
|
8
12
|
|
|
9
13
|
Usage:
|
|
10
14
|
python ci_merge_wait.py <PR-number> [--timeout SECONDS] [--poll SECONDS] [--merge-method merge|squash|rebase]
|
|
11
|
-
[--dry-run] [--self-test]
|
|
15
|
+
[--dry-run] [--allow-no-checks] [--expect-checks NAME1,NAME2,...] [--self-test]
|
|
12
16
|
|
|
13
17
|
Options:
|
|
14
|
-
--timeout SECONDS
|
|
15
|
-
--poll SECONDS
|
|
16
|
-
--merge-method METHOD
|
|
17
|
-
--dry-run
|
|
18
|
-
--
|
|
18
|
+
--timeout SECONDS Max seconds to wait for CI (default: 3600)
|
|
19
|
+
--poll SECONDS Poll interval in seconds (default: 10)
|
|
20
|
+
--merge-method METHOD Merge strategy: merge, squash, rebase (default: merge)
|
|
21
|
+
--dry-run Skip actual merge, just verify CI status and report what would happen
|
|
22
|
+
--allow-no-checks Allow merge when no CI checks present (repos without CI)
|
|
23
|
+
--expect-checks NAME1,NAME2 Comma-separated list of checks that MUST be present and successful
|
|
24
|
+
--self-test Run offline self-test of polling/decision logic (no network, no PR required)
|
|
19
25
|
|
|
20
26
|
Exit codes:
|
|
21
27
|
0 = PR merged successfully, dry-run verified, or self-test passed
|
|
@@ -73,7 +79,7 @@ def get_pr_status(pr_number):
|
|
|
73
79
|
return data
|
|
74
80
|
|
|
75
81
|
|
|
76
|
-
def check_ci_status(status_rollup):
|
|
82
|
+
def check_ci_status(status_rollup, allow_no_checks=False, expected_checks=None):
|
|
77
83
|
"""
|
|
78
84
|
Analyze status check rollup from gh pr view --json statusCheckRollup.
|
|
79
85
|
Handles both CheckRun (status + conclusion) and StatusContext (state) entries.
|
|
@@ -95,15 +101,35 @@ def check_ci_status(status_rollup):
|
|
|
95
101
|
|
|
96
102
|
GitHub semantics: NEUTRAL and SKIPPED conclusions/states are non-blocking and do not prevent merge.
|
|
97
103
|
|
|
104
|
+
Empty rollup (fail-closed):
|
|
105
|
+
- If allow_no_checks=True: ("success", None)
|
|
106
|
+
- If allow_no_checks=False: ("pending", None) [fail-closed default]
|
|
107
|
+
|
|
108
|
+
Expected checks (if provided):
|
|
109
|
+
- ALL named checks must be present in rollup
|
|
110
|
+
- ALL named checks must be in SUCCESS state
|
|
111
|
+
- If any expected check is missing: ("pending", None)
|
|
112
|
+
- If any expected check fails: ("failure", check_name)
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
status_rollup: list of check dicts from gh pr view
|
|
116
|
+
allow_no_checks: if True, empty rollup → success; if False (default), empty rollup → pending
|
|
117
|
+
expected_checks: set of check names that MUST be present and successful
|
|
118
|
+
|
|
98
119
|
Returns: ("pending", None), ("success", None), or ("failure", check_name)
|
|
99
120
|
"""
|
|
121
|
+
# Fail-closed: empty rollup defaults to PENDING unless explicitly allowed
|
|
100
122
|
if not status_rollup:
|
|
101
|
-
|
|
102
|
-
|
|
123
|
+
if allow_no_checks:
|
|
124
|
+
return ("success", None)
|
|
125
|
+
else:
|
|
126
|
+
# Empty rollup: fail-closed to PENDING (window where checks vanished/haven't registered yet)
|
|
127
|
+
return ("pending", None)
|
|
103
128
|
|
|
104
129
|
# Collect statuses
|
|
105
130
|
pending_checks = []
|
|
106
131
|
failed_checks = []
|
|
132
|
+
found_checks = {} # Map of check name → status
|
|
107
133
|
|
|
108
134
|
for check in status_rollup:
|
|
109
135
|
check_name = check.get("name", "unknown")
|
|
@@ -117,10 +143,16 @@ def check_ci_status(status_rollup):
|
|
|
117
143
|
|
|
118
144
|
if status == "COMPLETED":
|
|
119
145
|
# Check conclusion for failure indicators
|
|
120
|
-
if conclusion and conclusion.upper() in ("FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE"):
|
|
146
|
+
if conclusion and conclusion.upper() in ("FAILURE", "CANCELLED", "TIMED_OUT", "ACTION_REQUIRED", "STARTUP_FAILURE", "STALE"):
|
|
121
147
|
check_status = "failure"
|
|
148
|
+
elif not conclusion or conclusion == "":
|
|
149
|
+
# COMPLETED with null/empty conclusion = fail-closed to PENDING (API anomaly)
|
|
150
|
+
check_status = "pending"
|
|
151
|
+
elif conclusion.upper() in ("NEUTRAL", "SKIPPED"):
|
|
152
|
+
# Non-blocking advisory or skipped checks
|
|
153
|
+
check_status = "success"
|
|
122
154
|
else:
|
|
123
|
-
#
|
|
155
|
+
# Other conclusion values (unknown) = success (GitHub default)
|
|
124
156
|
check_status = "success"
|
|
125
157
|
elif status in ("QUEUED", "IN_PROGRESS"):
|
|
126
158
|
check_status = "pending"
|
|
@@ -148,13 +180,44 @@ def check_ci_status(status_rollup):
|
|
|
148
180
|
# Unrecognized shape (no status or state field) - fail-closed
|
|
149
181
|
check_status = "pending"
|
|
150
182
|
|
|
183
|
+
# Record this check's status
|
|
184
|
+
found_checks[check_name] = check_status
|
|
185
|
+
|
|
151
186
|
# Collect check status
|
|
152
187
|
if check_status == "failure":
|
|
153
188
|
failed_checks.append(check_name)
|
|
154
189
|
elif check_status == "pending":
|
|
155
190
|
pending_checks.append(check_name)
|
|
156
191
|
|
|
157
|
-
#
|
|
192
|
+
# If expected checks provided, verify all are present and successful
|
|
193
|
+
if expected_checks:
|
|
194
|
+
for expected_name in expected_checks:
|
|
195
|
+
if expected_name not in found_checks:
|
|
196
|
+
# Expected check not found (missing in the window)
|
|
197
|
+
return ("pending", None)
|
|
198
|
+
if found_checks[expected_name] == "failure":
|
|
199
|
+
# Expected check failed
|
|
200
|
+
return ("failure", expected_name)
|
|
201
|
+
if found_checks[expected_name] == "pending":
|
|
202
|
+
# Expected check still pending
|
|
203
|
+
return ("pending", None)
|
|
204
|
+
|
|
205
|
+
# All expected checks passed - warn about any non-expected check failures but don't block
|
|
206
|
+
# Non-expected checks are informational/flaky third-party checks; they should not gate merge
|
|
207
|
+
non_expected_failures = []
|
|
208
|
+
for check_name, check_status in found_checks.items():
|
|
209
|
+
if check_name not in expected_checks and check_status == "failure":
|
|
210
|
+
non_expected_failures.append(check_name)
|
|
211
|
+
|
|
212
|
+
if non_expected_failures:
|
|
213
|
+
# Log the failures loudly but don't block merge
|
|
214
|
+
print(f"WARNING: Non-expected check(s) failed (informational, does not block merge): {', '.join(non_expected_failures)}")
|
|
215
|
+
|
|
216
|
+
# All expected checks passed - merge is allowed even if non-expected checks are failing
|
|
217
|
+
# (pending non-expected checks are also acceptable)
|
|
218
|
+
return ("success", None)
|
|
219
|
+
|
|
220
|
+
# Determine overall status (when expected_checks is not specified)
|
|
158
221
|
if failed_checks:
|
|
159
222
|
return ("failure", failed_checks[0])
|
|
160
223
|
elif pending_checks:
|
|
@@ -191,17 +254,17 @@ def run_self_test():
|
|
|
191
254
|
"""
|
|
192
255
|
print("Running self-test with real GitHub API payloads...")
|
|
193
256
|
|
|
194
|
-
# Test 1: CheckRun success case (COMPLETED with
|
|
257
|
+
# Test 1: CheckRun success case (COMPLETED with explicit success conclusion)
|
|
195
258
|
checkrun_success = [
|
|
196
|
-
{"name": "test-unit", "status": "COMPLETED", "conclusion":
|
|
197
|
-
{"name": "test-integration", "status": "COMPLETED", "conclusion": ""},
|
|
198
|
-
{"name": "lint", "status": "COMPLETED", "conclusion": None},
|
|
259
|
+
{"name": "test-unit", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
260
|
+
{"name": "test-integration", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
261
|
+
{"name": "lint", "status": "COMPLETED", "conclusion": None}, # null conclusion = fail-closed to PENDING
|
|
199
262
|
]
|
|
200
263
|
ci_status, failed_check = check_ci_status(checkrun_success)
|
|
201
|
-
if ci_status != "
|
|
202
|
-
print(f"FAIL: Expected '
|
|
264
|
+
if ci_status != "pending":
|
|
265
|
+
print(f"FAIL: Expected 'pending' for COMPLETED with null conclusion (fail-closed), got '{ci_status}'")
|
|
203
266
|
return False
|
|
204
|
-
print("[OK] CheckRun
|
|
267
|
+
print("[OK] CheckRun with null conclusion fails-closed to PENDING")
|
|
205
268
|
|
|
206
269
|
# Test 2: CheckRun in-progress case (should be pending)
|
|
207
270
|
checkrun_pending = [
|
|
@@ -257,7 +320,7 @@ def run_self_test():
|
|
|
257
320
|
|
|
258
321
|
# Test 7: Mixed CheckRun and StatusContext (all success)
|
|
259
322
|
mixed_success = [
|
|
260
|
-
{"name": "test-unit", "status": "COMPLETED", "conclusion":
|
|
323
|
+
{"name": "test-unit", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
261
324
|
{"name": "travis-ci", "state": "success"},
|
|
262
325
|
]
|
|
263
326
|
ci_status, _ = check_ci_status(mixed_success)
|
|
@@ -268,7 +331,7 @@ def run_self_test():
|
|
|
268
331
|
|
|
269
332
|
# Test 8: Mixed payloads with pending (should block merge)
|
|
270
333
|
mixed_pending = [
|
|
271
|
-
{"name": "test-unit", "status": "COMPLETED", "conclusion":
|
|
334
|
+
{"name": "test-unit", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
272
335
|
{"name": "lint", "status": "QUEUED", "conclusion": None},
|
|
273
336
|
]
|
|
274
337
|
ci_status, _ = check_ci_status(mixed_pending)
|
|
@@ -277,14 +340,21 @@ def run_self_test():
|
|
|
277
340
|
return False
|
|
278
341
|
print("[OK] Mixed payloads with QUEUED (pending)")
|
|
279
342
|
|
|
280
|
-
# Test 9:
|
|
343
|
+
# Test 9: Empty checks array (fail-closed: should be PENDING by default)
|
|
281
344
|
ci_status, _ = check_ci_status([])
|
|
345
|
+
if ci_status != "pending":
|
|
346
|
+
print(f"FAIL: Expected 'pending' for empty rollup (fail-closed), got '{ci_status}'")
|
|
347
|
+
return False
|
|
348
|
+
print("[OK] Empty rollup case: treated as PENDING (fail-closed)")
|
|
349
|
+
|
|
350
|
+
# Test 10: Empty checks array with --allow-no-checks flag (should be SUCCESS)
|
|
351
|
+
ci_status, _ = check_ci_status([], allow_no_checks=True)
|
|
282
352
|
if ci_status != "success":
|
|
283
|
-
print(f"FAIL: Expected 'success' for
|
|
353
|
+
print(f"FAIL: Expected 'success' for empty rollup with allow_no_checks=True, got '{ci_status}'")
|
|
284
354
|
return False
|
|
285
|
-
print("[OK]
|
|
355
|
+
print("[OK] Empty rollup with allow_no_checks=True: treated as SUCCESS")
|
|
286
356
|
|
|
287
|
-
# Test
|
|
357
|
+
# Test 11: Unrecognized shape (fail-closed: should not succeed)
|
|
288
358
|
unrecognized = [
|
|
289
359
|
{"name": "mystery-check"},
|
|
290
360
|
]
|
|
@@ -294,7 +364,7 @@ def run_self_test():
|
|
|
294
364
|
return False
|
|
295
365
|
print("[OK] Unrecognized check shape (fail-closed)")
|
|
296
366
|
|
|
297
|
-
# Test
|
|
367
|
+
# Test 12: CheckRun cancelled (counts as failure)
|
|
298
368
|
cancelled_check = [
|
|
299
369
|
{"name": "test-unit", "status": "COMPLETED", "conclusion": "CANCELLED"},
|
|
300
370
|
]
|
|
@@ -304,7 +374,7 @@ def run_self_test():
|
|
|
304
374
|
return False
|
|
305
375
|
print("[OK] CheckRun CANCELLED (failure)")
|
|
306
376
|
|
|
307
|
-
# Test
|
|
377
|
+
# Test 13: CheckRun neutral conclusion (non-blocking advisory, should be success)
|
|
308
378
|
neutral_check = [
|
|
309
379
|
{"name": "advisory-lint", "status": "COMPLETED", "conclusion": "NEUTRAL"},
|
|
310
380
|
]
|
|
@@ -314,7 +384,7 @@ def run_self_test():
|
|
|
314
384
|
return False
|
|
315
385
|
print("[OK] CheckRun NEUTRAL conclusion (success, non-blocking)")
|
|
316
386
|
|
|
317
|
-
# Test
|
|
387
|
+
# Test 14: CheckRun skipped conclusion (non-blocking, should be success)
|
|
318
388
|
skipped_check = [
|
|
319
389
|
{"name": "optional-test", "status": "COMPLETED", "conclusion": "SKIPPED"},
|
|
320
390
|
]
|
|
@@ -324,7 +394,7 @@ def run_self_test():
|
|
|
324
394
|
return False
|
|
325
395
|
print("[OK] CheckRun SKIPPED conclusion (success, non-blocking)")
|
|
326
396
|
|
|
327
|
-
# Test
|
|
397
|
+
# Test 15: StatusContext neutral state (non-blocking advisory, should be success)
|
|
328
398
|
neutral_state = [
|
|
329
399
|
{"name": "advisory-check", "state": "neutral"},
|
|
330
400
|
]
|
|
@@ -334,7 +404,7 @@ def run_self_test():
|
|
|
334
404
|
return False
|
|
335
405
|
print("[OK] StatusContext neutral state (success, non-blocking)")
|
|
336
406
|
|
|
337
|
-
# Test
|
|
407
|
+
# Test 16: StatusContext skipped state (non-blocking, should be success)
|
|
338
408
|
skipped_state = [
|
|
339
409
|
{"name": "optional-check", "state": "skipped"},
|
|
340
410
|
]
|
|
@@ -344,7 +414,7 @@ def run_self_test():
|
|
|
344
414
|
return False
|
|
345
415
|
print("[OK] StatusContext skipped state (success, non-blocking)")
|
|
346
416
|
|
|
347
|
-
# Test
|
|
417
|
+
# Test 17: Fabricated unknown state (fail-closed to pending)
|
|
348
418
|
unknown_state = [
|
|
349
419
|
{"name": "mystery-state", "state": "fabricated_unknown_state"},
|
|
350
420
|
]
|
|
@@ -354,6 +424,133 @@ def run_self_test():
|
|
|
354
424
|
return False
|
|
355
425
|
print("[OK] Fabricated unknown state (pending, fail-closed)")
|
|
356
426
|
|
|
427
|
+
# Test 18: Expected checks - all present and successful
|
|
428
|
+
rollup_with_expected = [
|
|
429
|
+
{"name": "unit-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
430
|
+
{"name": "integration-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
431
|
+
{"name": "lint", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
432
|
+
]
|
|
433
|
+
ci_status, _ = check_ci_status(
|
|
434
|
+
rollup_with_expected,
|
|
435
|
+
expected_checks={"unit-tests", "integration-tests"}
|
|
436
|
+
)
|
|
437
|
+
if ci_status != "success":
|
|
438
|
+
print(f"FAIL: Expected 'success' with expected checks present, got '{ci_status}'")
|
|
439
|
+
return False
|
|
440
|
+
print("[OK] Expected checks all present and successful")
|
|
441
|
+
|
|
442
|
+
# Test 19: Expected checks - one missing (should be PENDING)
|
|
443
|
+
rollup_missing_expected = [
|
|
444
|
+
{"name": "unit-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
445
|
+
{"name": "lint", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
446
|
+
]
|
|
447
|
+
ci_status, _ = check_ci_status(
|
|
448
|
+
rollup_missing_expected,
|
|
449
|
+
expected_checks={"unit-tests", "integration-tests"}
|
|
450
|
+
)
|
|
451
|
+
if ci_status != "pending":
|
|
452
|
+
print(f"FAIL: Expected 'pending' with missing expected check, got '{ci_status}'")
|
|
453
|
+
return False
|
|
454
|
+
print("[OK] Missing expected check (window transition): returns PENDING")
|
|
455
|
+
|
|
456
|
+
# Test 20: Expected checks - one failed (should be FAILURE)
|
|
457
|
+
rollup_failed_expected = [
|
|
458
|
+
{"name": "unit-tests", "status": "COMPLETED", "conclusion": "FAILURE"},
|
|
459
|
+
{"name": "integration-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
460
|
+
]
|
|
461
|
+
ci_status, failed_check = check_ci_status(
|
|
462
|
+
rollup_failed_expected,
|
|
463
|
+
expected_checks={"unit-tests", "integration-tests"}
|
|
464
|
+
)
|
|
465
|
+
if ci_status != "failure" or failed_check != "unit-tests":
|
|
466
|
+
print(f"FAIL: Expected 'failure' with expected check failed, got '{ci_status}' / '{failed_check}'")
|
|
467
|
+
return False
|
|
468
|
+
print("[OK] Expected check failed: returns FAILURE")
|
|
469
|
+
|
|
470
|
+
# Test 20b: Expected checks all pass, but non-expected check FAILED (P2 audit bug fix)
|
|
471
|
+
# FIXED: When --expect-checks is given, non-expected failures should NOT block merge.
|
|
472
|
+
# Non-expected checks are informational/flaky third-party checks; only expected checks gate merge.
|
|
473
|
+
# The failure is logged loudly as a warning, but merge is allowed.
|
|
474
|
+
rollup_expected_pass_noncxpected_fail = [
|
|
475
|
+
{"name": "unit-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
476
|
+
{"name": "integration-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
477
|
+
{"name": "lint", "status": "COMPLETED", "conclusion": "FAILURE"},
|
|
478
|
+
]
|
|
479
|
+
ci_status, failed_check = check_ci_status(
|
|
480
|
+
rollup_expected_pass_noncxpected_fail,
|
|
481
|
+
expected_checks={"unit-tests", "integration-tests"}
|
|
482
|
+
)
|
|
483
|
+
if ci_status != "success":
|
|
484
|
+
print(f"FAIL: Expected 'success' with non-expected check failed (audit P2 fix), got '{ci_status}'")
|
|
485
|
+
return False
|
|
486
|
+
print("[OK] Non-expected check failed while expected pass: returns SUCCESS with warning (P2 audit fix)")
|
|
487
|
+
|
|
488
|
+
# Test 20c: Expected checks all pass, non-expected pending is OK (does not block)
|
|
489
|
+
rollup_expected_pass_noncexpected_pending = [
|
|
490
|
+
{"name": "unit-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
491
|
+
{"name": "integration-tests", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
492
|
+
{"name": "optional-scan", "status": "IN_PROGRESS", "conclusion": None},
|
|
493
|
+
]
|
|
494
|
+
ci_status, _ = check_ci_status(
|
|
495
|
+
rollup_expected_pass_noncexpected_pending,
|
|
496
|
+
expected_checks={"unit-tests", "integration-tests"}
|
|
497
|
+
)
|
|
498
|
+
if ci_status != "success":
|
|
499
|
+
print(f"FAIL: Expected 'success' when all expected pass + non-expected pending, got '{ci_status}'")
|
|
500
|
+
return False
|
|
501
|
+
print("[OK] Expected all pass, non-expected pending: returns SUCCESS (pending non-expected is OK)")
|
|
502
|
+
|
|
503
|
+
# Test 22: CheckRun STALE conclusion (invalidated check, counts as failure)
|
|
504
|
+
stale_check = [
|
|
505
|
+
{"name": "ci-run", "status": "COMPLETED", "conclusion": "STALE"},
|
|
506
|
+
]
|
|
507
|
+
ci_status, _ = check_ci_status(stale_check)
|
|
508
|
+
if ci_status != "failure":
|
|
509
|
+
print(f"FAIL: Expected 'failure' for STALE conclusion, got '{ci_status}'")
|
|
510
|
+
return False
|
|
511
|
+
print("[OK] CheckRun STALE conclusion (failure, invalidated by force-push)")
|
|
512
|
+
|
|
513
|
+
# Test 23: CheckRun COMPLETED with null conclusion (API anomaly, fail-closed)
|
|
514
|
+
null_conclusion_check = [
|
|
515
|
+
{"name": "test-suite", "status": "COMPLETED", "conclusion": None},
|
|
516
|
+
]
|
|
517
|
+
ci_status, _ = check_ci_status(null_conclusion_check)
|
|
518
|
+
if ci_status != "pending":
|
|
519
|
+
print(f"FAIL: Expected 'pending' for COMPLETED + null conclusion (fail-closed), got '{ci_status}'")
|
|
520
|
+
return False
|
|
521
|
+
print("[OK] CheckRun COMPLETED + null conclusion fails-closed to PENDING")
|
|
522
|
+
|
|
523
|
+
# Test 21: Superseded-run window simulation (old checks vanish, new pending appear)
|
|
524
|
+
# First: old run had checks that completed successfully
|
|
525
|
+
old_run_checks = [
|
|
526
|
+
{"name": "build", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
527
|
+
{"name": "test", "status": "COMPLETED", "conclusion": "SUCCESS"},
|
|
528
|
+
]
|
|
529
|
+
ci_status, _ = check_ci_status(old_run_checks)
|
|
530
|
+
if ci_status != "success":
|
|
531
|
+
print(f"FAIL: Old run checks should be success, got '{ci_status}'")
|
|
532
|
+
return False
|
|
533
|
+
|
|
534
|
+
# Second: transition window where old checks vanished and new run hasn't registered yet
|
|
535
|
+
# Empty rollup should return PENDING (fail-closed)
|
|
536
|
+
empty_window = []
|
|
537
|
+
ci_status, _ = check_ci_status(empty_window)
|
|
538
|
+
if ci_status != "pending":
|
|
539
|
+
print(f"FAIL: Transition window (empty rollup) should be PENDING (fail-closed), got '{ci_status}'")
|
|
540
|
+
return False
|
|
541
|
+
|
|
542
|
+
# Third: new run appears with pending checks
|
|
543
|
+
new_run_checks = [
|
|
544
|
+
{"name": "build", "status": "IN_PROGRESS", "conclusion": None},
|
|
545
|
+
{"name": "test", "status": "QUEUED", "conclusion": None},
|
|
546
|
+
]
|
|
547
|
+
ci_status, _ = check_ci_status(new_run_checks)
|
|
548
|
+
if ci_status != "pending":
|
|
549
|
+
print(f"FAIL: New run pending checks should be PENDING, got '{ci_status}'")
|
|
550
|
+
return False
|
|
551
|
+
|
|
552
|
+
print("[OK] Superseded-run window simulation: transitions correctly")
|
|
553
|
+
|
|
357
554
|
print("\nAll self-tests passed!")
|
|
358
555
|
return True
|
|
359
556
|
|
|
@@ -393,6 +590,17 @@ def main():
|
|
|
393
590
|
action="store_true",
|
|
394
591
|
help="Skip actual merge, just verify CI status and report what would happen"
|
|
395
592
|
)
|
|
593
|
+
parser.add_argument(
|
|
594
|
+
"--allow-no-checks",
|
|
595
|
+
action="store_true",
|
|
596
|
+
help="Allow merge when no CI checks present (repos without CI)"
|
|
597
|
+
)
|
|
598
|
+
parser.add_argument(
|
|
599
|
+
"--expect-checks",
|
|
600
|
+
type=str,
|
|
601
|
+
default=None,
|
|
602
|
+
help="Comma-separated list of checks that MUST be present and successful"
|
|
603
|
+
)
|
|
396
604
|
parser.add_argument(
|
|
397
605
|
"--self-test",
|
|
398
606
|
action="store_true",
|
|
@@ -422,6 +630,11 @@ def main():
|
|
|
422
630
|
print("ERROR: timeout and poll must be positive")
|
|
423
631
|
sys.exit(1)
|
|
424
632
|
|
|
633
|
+
# Parse expected checks
|
|
634
|
+
expected_checks = None
|
|
635
|
+
if args.expect_checks:
|
|
636
|
+
expected_checks = set(c.strip() for c in args.expect_checks.split(",") if c.strip())
|
|
637
|
+
|
|
425
638
|
# Fetch initial PR status
|
|
426
639
|
print(f"Checking PR #{args.pr_number} status...")
|
|
427
640
|
status = get_pr_status(args.pr_number)
|
|
@@ -452,7 +665,11 @@ def main():
|
|
|
452
665
|
sys.exit(1)
|
|
453
666
|
|
|
454
667
|
status_rollup = status.get("statusCheckRollup", [])
|
|
455
|
-
ci_status, failed_check = check_ci_status(
|
|
668
|
+
ci_status, failed_check = check_ci_status(
|
|
669
|
+
status_rollup,
|
|
670
|
+
allow_no_checks=args.allow_no_checks,
|
|
671
|
+
expected_checks=expected_checks
|
|
672
|
+
)
|
|
456
673
|
|
|
457
674
|
if ci_status == "failure":
|
|
458
675
|
print(f"CI FAILED: {failed_check}")
|
|
@@ -466,7 +683,11 @@ def main():
|
|
|
466
683
|
sys.exit(1)
|
|
467
684
|
|
|
468
685
|
final_rollup = final_check.get("statusCheckRollup", [])
|
|
469
|
-
final_ci_status, final_failed = check_ci_status(
|
|
686
|
+
final_ci_status, final_failed = check_ci_status(
|
|
687
|
+
final_rollup,
|
|
688
|
+
allow_no_checks=args.allow_no_checks,
|
|
689
|
+
expected_checks=expected_checks
|
|
690
|
+
)
|
|
470
691
|
|
|
471
692
|
if final_ci_status != "success":
|
|
472
693
|
print(f"CI STATUS CHANGED: {final_failed}")
|