CI Pipeline / best-practices (push) Successful in 8s
Auto-version and Release / release (push) Failing after 13s
CI Pipeline / documentation (push) Successful in 5s
CI Pipeline / canary (push) Successful in 33s
CI Pipeline / lint (push) Failing after 41s
CI Pipeline / type-check (push) Successful in 27s
CI Pipeline / gitea-offline (push) Successful in 14s
CI Pipeline / unit-tests (push) Successful in 59s
CI Pipeline / required-checks (push) Successful in 1s
deploy / deploy (push) Successful in 37s
- scripts/gitea_auth.py: single credential ladder (env -> 0600 token file -> git credential fill); 6 consumers migrated off direct os.environ reads - rune_operator: consolidate 5 duplicated failure blocks into _terminalize_execution_failure; fix 3 fault-injection gaps: (1) dual-module PreparedTool identity split crashed approval-path saves, (2) fallback-model failure escaped send_task (created/pending orphan), (3) running-transition save uncaught (running-with-no-owner) - envelope task.status now agrees with canonical ledger (awaiting_approval) - orphan task_ac35f4e287c8 terminalized failed with durable evidence - tests: gitea auth ladder (6), lifecycle fault matrix (9 boundaries + restart/reload + helper contract); 1280 green in ci-venv; ratchet 352/80
117 lines
4.5 KiB
Python
117 lines
4.5 KiB
Python
#!/usr/bin/env python3
|
|
"""publish_source_ledger.py — idempotent Gitea publication for the Source Ledger
|
|
(PROV-1, #282).
|
|
|
|
Pure projection publisher: renders the ledger from the accepted OUTWARD
|
|
artifact and posts/updates a single OUTWARD SOURCE LEDGER comment on the
|
|
issue. Idempotency: the comment is identified by a fixed marker containing
|
|
the artifact hash — re-running with the same hash UPDATES the existing
|
|
comment instead of creating duplicates.
|
|
|
|
Publication failure raises visibly (per #282 constraint: provenance
|
|
publication fails visibly, must not silently falsify the ledger). It does
|
|
NOT alter research cognition or stage outcomes — callers invoke it after
|
|
gate acceptance, and its failure does not invalidate the accepted artifact.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
import os
|
|
import urllib.request
|
|
|
|
from source_ledger import build_records, render_markdown
|
|
import sys as _sys
|
|
_SCRIPT_DIR = str(__import__('pathlib').Path(__file__).resolve().parent)
|
|
if _SCRIPT_DIR not in _sys.path:
|
|
_sys.path.insert(0, _SCRIPT_DIR)
|
|
import gitea_auth
|
|
|
|
MARKER_PREFIX = "<!-- source-ledger"
|
|
|
|
|
|
def _api(method: str, path: str, payload: dict | None = None) -> tuple[int, dict | str]:
|
|
token = gitea_auth.get_token() if gitea_auth.has_token() else ""
|
|
server = os.environ.get("GITEA_SERVER", "https://gitea.thingscouldgetdicey.online")
|
|
req = urllib.request.Request(
|
|
server + "/api/v1" + path,
|
|
method=method,
|
|
data=json.dumps(payload).encode() if payload else None,
|
|
headers={"Authorization": f"token {token}", "Content-Type": "application/json"},
|
|
)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=60) as r:
|
|
body = r.read().decode()
|
|
return r.status, (json.loads(body) if body.strip() else "")
|
|
except urllib.error.HTTPError as e:
|
|
return e.code, e.read().decode()[:500]
|
|
|
|
|
|
def _marker(artifact_hash: str) -> str:
|
|
return f"{MARKER_PREFIX} hash={artifact_hash} -->"
|
|
|
|
|
|
def publish(
|
|
issue: int, artifact_dir: str, artifact_hash: str, runner: dict | None = None, model: str | None = None
|
|
) -> dict:
|
|
"""Render + post/update the ledger comment. Returns publication result."""
|
|
path = os.path.join(artifact_dir, "outward.json")
|
|
with open(path) as f:
|
|
outward = json.load(f)
|
|
|
|
# runner/model: persisted factual fields from the artifact; UNKNOWN if absent
|
|
runner = runner or outward.get("runner")
|
|
model = model or outward.get("model", "UNKNOWN")
|
|
|
|
records = build_records(outward)
|
|
legacy = not any(
|
|
c.get("contradicts_source_ids")
|
|
for c in outward.get("contradictory_evidence", outward.get("contradictions", []))
|
|
if isinstance(c, dict)
|
|
)
|
|
body = (
|
|
_marker(artifact_hash)
|
|
+ "\n"
|
|
+ render_markdown(records, outward, runner, model, artifact_hash, legacy_claims=legacy)
|
|
)
|
|
|
|
# Find existing ledger comment for this artifact hash (idempotency)
|
|
status, comments = _api("GET", f"/repos/conductor/rune/issues/{issue}/comments?limit=50")
|
|
if status != 200 or not isinstance(comments, list):
|
|
raise RuntimeError(f"SourceLedger publish: cannot list comments (HTTP {status})")
|
|
existing = None
|
|
for c in comments:
|
|
if isinstance(c, dict) and c.get("body", "").startswith(_marker(artifact_hash)):
|
|
existing = c
|
|
break
|
|
|
|
if existing:
|
|
status, _ = _api("PATCH", f"/repos/conductor/rune/issues/comments/{existing['id']}", {"body": body})
|
|
if status not in (200, 201):
|
|
raise RuntimeError(f"SourceLedger publish: update failed (HTTP {status})")
|
|
return {"action": "updated", "comment_id": existing["id"], "artifact_hash": artifact_hash}
|
|
|
|
status, created = _api("POST", f"/repos/conductor/rune/issues/{issue}/comments", {"body": body})
|
|
if status not in (200, 201):
|
|
raise RuntimeError(f"SourceLedger publish: create failed (HTTP {status}): {created}")
|
|
return {"action": "created", "comment_id": created.get("id"), "artifact_hash": artifact_hash}
|
|
|
|
|
|
def main_with(argv: list[str]) -> int:
|
|
import argparse
|
|
|
|
parser = argparse.ArgumentParser(description="Publish the OUTWARD Source Ledger")
|
|
parser.add_argument("--issue", type=int, required=True)
|
|
parser.add_argument("--artifact-dir", required=True)
|
|
parser.add_argument("--artifact-hash", required=True)
|
|
args = parser.parse_args(argv[1:])
|
|
result = publish(args.issue, args.artifact_dir, args.artifact_hash)
|
|
print(json.dumps(result))
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import sys
|
|
|
|
raise SystemExit(main_with(sys.argv))
|