- deploy.yml sleep-30 polling loop REPLACED: registers a gitea-identity PROCESS_EVENT wait, yields in select() on the FIFO, resumes on the ci.yml notify-event-bridge WAKE, then re-reads the canonical Gitea verdict (deploy_qualify.py). Event payload is never completion truth. - ci.yml gains notify-event-bridge (always(), best-effort): a red CI wakes the waiter as loudly as a green one; bridge unreachable -> watchdog recovery covers the lost wake. - event_wait.py: register_process_wait gains provider/event_type/sha identity (16E: ONE bridge primitive; builder + gitea are adapters); open_fifo_reader + caller-owned fifo_fd close the 16Y#10 race (event before waiter in select()); await_resume_event returns eof instead of busy-looping on a caller-owned fd. - ci_event_wait.py watchdog: failure DETECTOR only — liveness-gated re-arm, one-shot authority recovery on lost events, truthful failure when no verdict exists; timestamps emitted for the 16D latency proof. - tests: 8 held-outs on the REAL daemon (shared wr_daemon fixture in conftest — no duplicated helpers). Daemon-side: /process-event passes through provider/event_type/sha (defaults preserve T15); lease_path sanitizes slash-bearing session ids (resume delivery was impossible for them — found live, fixed in daemon snapshot 79434e0). Suite 1861 green; ratchets PASS; Henxels rc=0.
60 lines
2.2 KiB
Python
60 lines
2.2 KiB
Python
#!/usr/bin/env python3
|
|
"""notify_event_bridge.py — T16 §16C: ci.yml's final job posts the CI
|
|
completion WAKE to the local W/R event bridge (daemon /process-event).
|
|
|
|
The event is a WAKE, never completion truth: the waiting controller
|
|
(ci_event_wait.py) re-reads the canonical Gitea verdict via
|
|
deploy_qualify.py after resuming. Best-effort by design — if the bridge
|
|
is unreachable the waiter's watchdog still recovers truthfully (that is
|
|
the §16D lost-event contract), so a notify failure must NEVER fail CI.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.request
|
|
|
|
BRIDGE_URL = os.environ.get("RUNE_EVENT_BRIDGE_URL",
|
|
"http://127.0.0.1:8791")
|
|
|
|
|
|
def main() -> int:
|
|
sha = os.environ.get("CI_SHA", "")
|
|
result = os.environ.get("CI_RESULT", "") # needs.<job>.result
|
|
if not sha:
|
|
print("notify-event-bridge: no CI_SHA — nothing to wake", flush=True)
|
|
return 0
|
|
payload = {
|
|
"operation_id": f"ci-{sha}",
|
|
"provider": "gitea",
|
|
"event_type": "ci_completed",
|
|
"sha": sha,
|
|
# exit_code is informational only; the wake is not the verdict
|
|
"exit_code": 0 if result == "success" else 1,
|
|
"completed_at": time.time(),
|
|
}
|
|
for attempt in range(2):
|
|
try:
|
|
req = urllib.request.Request(
|
|
BRIDGE_URL + "/process-event",
|
|
data=json.dumps(payload).encode(),
|
|
headers={"Content-Type": "application/json"})
|
|
with urllib.request.urlopen(req, timeout=5) as r:
|
|
resp = json.loads(r.read())
|
|
print(f"notify-event-bridge: woke bridge "
|
|
f"(matched={resp.get('matched')})", flush=True)
|
|
return 0
|
|
except Exception as exc: # noqa: BLE001 — wake is best-effort by design; failure is logged and the waiter's watchdog recovery covers a lost wake (§16D contract)
|
|
if attempt == 0:
|
|
time.sleep(2)
|
|
else:
|
|
print(f"notify-event-bridge: bridge unreachable ({exc}); "
|
|
f"watchdog recovery covers a lost wake", flush=True)
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|