Merge remote-tracking branch 'origin/master' into fix/1784-managed-repository-materialization
This commit is contained in:
@@ -13,6 +13,7 @@ metadata:
|
||||
- 1760
|
||||
- 1769
|
||||
- 1802
|
||||
- 1811
|
||||
defaults:
|
||||
targetId: NC01
|
||||
consumerId: hwlab-nc01-v03
|
||||
@@ -26,7 +27,7 @@ release:
|
||||
controllerServicePort: 8080
|
||||
waitTimeoutSeconds: 55
|
||||
deliveryProvenance:
|
||||
version: admission-pac-v1
|
||||
version: admission-pac-v2
|
||||
markerAnnotation: unidesk.ai/pac-admission-provenance
|
||||
child:
|
||||
enabled: false
|
||||
@@ -35,9 +36,19 @@ deliveryProvenance:
|
||||
controller:
|
||||
namespace: pipelines-as-code
|
||||
serviceAccountName: pipelines-as-code-controller
|
||||
queueTransition:
|
||||
controller:
|
||||
namespace: pipelines-as-code
|
||||
serviceAccountName: pipelines-as-code-watcher
|
||||
stateLabel: pipelinesascode.tekton.dev/state
|
||||
allowed:
|
||||
- fromSpecStatus: PipelineRunPending
|
||||
toSpecStatus: absent
|
||||
fromState: queued
|
||||
toState: started
|
||||
admission:
|
||||
policyName: unidesk-pac-delivery-provenance-v1
|
||||
bindingName: unidesk-pac-delivery-provenance-v1
|
||||
policyName: unidesk-pac-delivery-provenance-v2
|
||||
bindingName: unidesk-pac-delivery-provenance-v2
|
||||
failurePolicy: Fail
|
||||
validationActions:
|
||||
- Deny
|
||||
@@ -130,7 +141,7 @@ consumers:
|
||||
LANE: v02
|
||||
deliveryProvenance:
|
||||
required: true
|
||||
markerValue: admission-pac-v1:agentrun-nc01-v02
|
||||
markerValue: admission-pac-v2:agentrun-nc01-v02
|
||||
executionServiceAccountName: agentrun-nc01-v02-tekton-runner
|
||||
gitOps:
|
||||
repoUrl: http://git-mirror-http.devops-infra.svc.cluster.local:8080/pikasTech/agentrun.git
|
||||
|
||||
@@ -76,6 +76,16 @@ function normalizeAdmissionPolicySpec(value) {
|
||||
if (Object.prototype.hasOwnProperty.call(spec, "matchConstraints")) {
|
||||
const matchConstraints = { ...record(spec.matchConstraints) };
|
||||
if (!Object.prototype.hasOwnProperty.call(matchConstraints, "matchPolicy")) matchConstraints.matchPolicy = "Equivalent";
|
||||
for (const selector of ["namespaceSelector", "objectSelector"]) {
|
||||
if (isEmptyRecord(matchConstraints[selector])) delete matchConstraints[selector];
|
||||
}
|
||||
if (Array.isArray(matchConstraints.resourceRules)) {
|
||||
matchConstraints.resourceRules = matchConstraints.resourceRules.map((value) => {
|
||||
const rule = { ...record(value) };
|
||||
if (rule.scope === "*") delete rule.scope;
|
||||
return rule;
|
||||
});
|
||||
}
|
||||
spec.matchConstraints = matchConstraints;
|
||||
}
|
||||
return spec;
|
||||
@@ -91,6 +101,10 @@ function normalizeAdmissionBindingSpec(value) {
|
||||
return spec;
|
||||
}
|
||||
|
||||
function isEmptyRecord(value) {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value) && Object.keys(value).length === 0;
|
||||
}
|
||||
|
||||
function evaluatePacAdmissionState(inputValue) {
|
||||
const input = record(inputValue);
|
||||
const policy = record(input.policy);
|
||||
|
||||
@@ -632,7 +632,7 @@ PY
|
||||
python3 - "$status_rc" "$tmp/repo-meta.json" "$tmp" <<'PY'
|
||||
import json, os, sys
|
||||
sys.path.insert(0, sys.argv[3])
|
||||
from platform_infra_gitea_status_evaluator import select_snapshot
|
||||
from platform_infra_gitea_status_evaluator import parse_ls_remote_lines, select_snapshot
|
||||
status_rc = int(sys.argv[1])
|
||||
meta = json.load(open(sys.argv[2], encoding="utf-8"))
|
||||
tmp = sys.argv[3]
|
||||
@@ -643,12 +643,11 @@ def text(path, limit=1200):
|
||||
return ""
|
||||
repos = []
|
||||
for repo in meta:
|
||||
out = text(os.path.join(tmp, f"{repo['key']}.ls.out"), 20000)
|
||||
refs = {}
|
||||
for line in out.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 2:
|
||||
refs[parts[1]] = parts[0]
|
||||
try:
|
||||
with open(os.path.join(tmp, f"{repo['key']}.ls.out"), encoding="utf-8", errors="replace") as handle:
|
||||
refs = parse_ls_remote_lines(handle)
|
||||
except FileNotFoundError:
|
||||
refs = {}
|
||||
branch_ref = f"refs/heads/{repo['branch']}"
|
||||
branch_commit = refs.get(branch_ref)
|
||||
selected_snapshot = select_snapshot(branch_commit, repo["snapshotPrefix"], refs)
|
||||
@@ -755,7 +754,7 @@ NODE
|
||||
python3 - "$tmp/repos.json" "$bridge_ready" "$service_exists" "$mirror_status_rc" "$bridge_logs_rc" "$inbox_status_rc" "$tmp/mirror-status.json" "$tmp/mirror-status.err" "$tmp/bridge.log" "$tmp/bridge.err" "$tmp/inbox-status.json" "$tmp/inbox-status.err" <<'PY'
|
||||
import json, os, sys, urllib.error, urllib.parse, urllib.request
|
||||
sys.path.insert(0, os.environ["tmp"])
|
||||
from platform_infra_gitea_status_evaluator import evaluate_repository
|
||||
from platform_infra_gitea_status_evaluator import evaluate_repository, select_committed_head_record, select_target_delivery
|
||||
repos = json.load(open(sys.argv[1], encoding="utf-8"))
|
||||
bridge_ready = sys.argv[2]
|
||||
service_exists = bool(sys.argv[3])
|
||||
@@ -782,9 +781,9 @@ def request(api_path):
|
||||
req.add_header("X-GitHub-Api-Version", "2022-11-28")
|
||||
try:
|
||||
with urllib.request.urlopen(req, timeout=30) as resp:
|
||||
return {"ok": True, "status": resp.status, "body": resp.read().decode("utf-8", errors="replace")}
|
||||
return {"ok": True, "status": resp.status, "body": resp.read().decode("utf-8", errors="replace"), "link": resp.headers.get("Link")}
|
||||
except urllib.error.HTTPError as exc:
|
||||
return {"ok": False, "status": exc.code, "body": exc.read().decode("utf-8", errors="replace")[:1000]}
|
||||
return {"ok": False, "status": exc.code, "body": exc.read().decode("utf-8", errors="replace")[:1000], "link": exc.headers.get("Link") if exc.headers else None}
|
||||
def github_head(repository, branch):
|
||||
result = request(f"/repos/{repository}/git/ref/heads/{urllib.parse.quote(branch, safe='')}")
|
||||
try:
|
||||
@@ -792,14 +791,42 @@ def github_head(repository, branch):
|
||||
except Exception:
|
||||
payload = {}
|
||||
return {"ok": result.get("ok"), "status": result.get("status"), "sha": payload.get("object", {}).get("sha"), "error": None if result.get("ok") else result.get("body", "")[:300]}
|
||||
def hook_deliveries(repository, hook_id):
|
||||
if not hook_id:
|
||||
return {"ok": False, "status": None, "latest": None, "latestPush": None, "error": "hook not found"}
|
||||
result = request(f"/repos/{repository}/hooks/{hook_id}/deliveries?per_page=8")
|
||||
def hook_delivery_detail(repository, hook_id, delivery):
|
||||
delivery_id = delivery.get("id") if isinstance(delivery, dict) else None
|
||||
if not delivery_id:
|
||||
return {"ok": False, "error": "delivery id missing"}
|
||||
result = request(f"/repos/{repository}/hooks/{hook_id}/deliveries/{delivery_id}")
|
||||
try:
|
||||
rows = json.loads(result.get("body") or "[]")
|
||||
body = json.loads(result.get("body") or "{}")
|
||||
except Exception:
|
||||
rows = []
|
||||
body = {}
|
||||
request_payload = body.get("request", {}).get("payload", {}) if isinstance(body.get("request"), dict) else {}
|
||||
payload_repository = request_payload.get("repository", {}) if isinstance(request_payload.get("repository"), dict) else {}
|
||||
return {
|
||||
"ok": result.get("ok"),
|
||||
"repository": payload_repository.get("full_name"),
|
||||
"ref": request_payload.get("ref"),
|
||||
"requestedCommit": request_payload.get("after"),
|
||||
"error": None if result.get("ok") else result.get("body", "")[:300],
|
||||
}
|
||||
def next_delivery_cursor(link):
|
||||
if not isinstance(link, str):
|
||||
return None
|
||||
for part in link.split(","):
|
||||
if 'rel="next"' not in part:
|
||||
continue
|
||||
start = part.find("<")
|
||||
end = part.find(">", start + 1)
|
||||
if start < 0 or end < 0:
|
||||
continue
|
||||
query = urllib.parse.parse_qs(urllib.parse.urlparse(part[start + 1:end]).query)
|
||||
values = query.get("cursor") or []
|
||||
if values:
|
||||
return values[0]
|
||||
return None
|
||||
def hook_deliveries(repository, expected_ref, github_head, head_delivery_id, hook_id):
|
||||
if not hook_id:
|
||||
return {"ok": False, "status": None, "latest": None, "latestPush": None, "targetPush": None, "rows": [], "error": "hook not found"}
|
||||
def compact(item):
|
||||
if not isinstance(item, dict):
|
||||
return None
|
||||
@@ -813,9 +840,71 @@ def hook_deliveries(repository, hook_id):
|
||||
"deliveredAt": item.get("delivered_at"),
|
||||
"duration": item.get("duration"),
|
||||
}
|
||||
latest = compact(rows[0]) if rows else None
|
||||
latest_push = next((compact(item) for item in rows if isinstance(item, dict) and item.get("event") == "push"), None)
|
||||
return {"ok": result.get("ok"), "status": result.get("status"), "latest": latest, "latestPush": latest_push, "error": None if result.get("ok") else result.get("body", "")[:300]}
|
||||
compact_rows = []
|
||||
cursor = None
|
||||
list_error = None
|
||||
last_status = None
|
||||
history_has_more = False
|
||||
max_pages = 10 if head_delivery_id else 1
|
||||
for _ in range(max_pages):
|
||||
query = "?per_page=100" + ("&cursor=" + urllib.parse.quote(cursor, safe="") if cursor else "")
|
||||
result = request(f"/repos/{repository}/hooks/{hook_id}/deliveries{query}")
|
||||
last_status = result.get("status")
|
||||
if not result.get("ok"):
|
||||
list_error = result.get("body", "")[:300]
|
||||
break
|
||||
try:
|
||||
page_rows = json.loads(result.get("body") or "[]")
|
||||
except Exception:
|
||||
page_rows = []
|
||||
compact_rows.extend(item for item in (compact(value) for value in page_rows if isinstance(value, dict)) if item is not None)
|
||||
if head_delivery_id and any(str(item.get("deliveryId")) == str(head_delivery_id) for item in compact_rows):
|
||||
break
|
||||
cursor = next_delivery_cursor(result.get("link"))
|
||||
history_has_more = cursor is not None
|
||||
if cursor is None:
|
||||
break
|
||||
latest = compact_rows[0] if compact_rows else None
|
||||
latest_push = next((item for item in compact_rows if item.get("event") == "push"), None)
|
||||
target_push = None
|
||||
detail_error = None
|
||||
if head_delivery_id:
|
||||
delivery_candidates = [item for item in compact_rows if str(item.get("deliveryId")) == str(head_delivery_id)]
|
||||
if not delivery_candidates:
|
||||
detail_error = "committed head delivery guid not found in bounded github history"
|
||||
else:
|
||||
delivery_candidates = [item for item in compact_rows if item.get("event") == "push"][:100]
|
||||
detailed_candidates = []
|
||||
for delivery in delivery_candidates:
|
||||
detail = hook_delivery_detail(repository, hook_id, delivery)
|
||||
if not detail.get("ok"):
|
||||
detail_error = detail.get("error") or "delivery detail unavailable"
|
||||
break
|
||||
detailed = {**delivery, **{key: detail.get(key) for key in ("repository", "ref", "requestedCommit")}}
|
||||
detailed_candidates.append(detailed)
|
||||
target_push = select_target_delivery(
|
||||
detailed_candidates,
|
||||
repository,
|
||||
expected_ref,
|
||||
github_head if head_delivery_id else None,
|
||||
head_delivery_id,
|
||||
)
|
||||
if target_push is not None:
|
||||
break
|
||||
if head_delivery_id is None and target_push is None and detail_error is None:
|
||||
detail_error = "target branch delivery not found in bounded github history"
|
||||
return {
|
||||
"ok": list_error is None and detail_error is None,
|
||||
"status": last_status,
|
||||
"latest": latest,
|
||||
"latestPush": latest_push,
|
||||
"targetPush": target_push,
|
||||
"rows": compact_rows,
|
||||
"historyHasMore": history_has_more,
|
||||
"error": detail_error or list_error,
|
||||
}
|
||||
def newest_delivery(rows):
|
||||
return max(rows, key=lambda item: (str(item.get("deliveredAt") or ""), str(item.get("id") or "")), default=None)
|
||||
try:
|
||||
mirror_status = json.load(open(mirror_status_path, encoding="utf-8"))
|
||||
except Exception:
|
||||
@@ -861,22 +950,55 @@ for repo in repos:
|
||||
hooks = json.loads(result.get("body") or "[]")
|
||||
except Exception:
|
||||
hooks = []
|
||||
match = next((item for item in hooks if (item.get("config") or {}).get("url") == url), None)
|
||||
hook_id = match.get("id") if match else None
|
||||
matches = [item for item in hooks if (item.get("config") or {}).get("url") == url]
|
||||
head = github_head(repository, branch)
|
||||
delivery = hook_deliveries(repository, hook_id)
|
||||
head_record = select_committed_head_record(repo["key"], head.get("sha"), inbox_records)
|
||||
head_delivery_id = head_record.get("deliveryId") if isinstance(head_record, dict) else None
|
||||
observations = []
|
||||
for match in matches:
|
||||
hook_id = match.get("id")
|
||||
delivery = hook_deliveries(repository, f"refs/heads/{branch}", head.get("sha"), head_delivery_id, hook_id)
|
||||
observations.append({"hook": match, "delivery": delivery})
|
||||
all_deliveries = [
|
||||
{**item, "hookId": observation["hook"].get("id")}
|
||||
for observation in observations
|
||||
for item in observation["delivery"].get("rows", [])
|
||||
]
|
||||
push_deliveries = [item for item in all_deliveries if item.get("event") == "push"]
|
||||
latest_delivery = newest_delivery(all_deliveries)
|
||||
latest_push_delivery = newest_delivery(push_deliveries)
|
||||
target_push_deliveries = [
|
||||
{**observation["delivery"]["targetPush"], "hookId": observation["hook"].get("id")}
|
||||
for observation in observations
|
||||
if observation["delivery"].get("targetPush") is not None
|
||||
]
|
||||
selected_push_delivery = newest_delivery(target_push_deliveries)
|
||||
selected_hook_id = selected_push_delivery.get("hookId") if selected_push_delivery else (matches[0].get("id") if matches else None)
|
||||
selected_match = next((item for item in matches if item.get("id") == selected_hook_id), None)
|
||||
topology_ready = len(matches) == 1 and matches[0].get("active") is True
|
||||
topology_reason = None if topology_ready else (
|
||||
"github-hook-missing" if len(matches) == 0
|
||||
else "github-hook-duplicate-exact-url" if len(matches) > 1
|
||||
else "github-hook-inactive"
|
||||
)
|
||||
delivery_errors = [observation["delivery"].get("error") for observation in observations if observation["delivery"].get("error")]
|
||||
mirror = mirror_by_key.get(repo["key"], {})
|
||||
latest_push_delivery = delivery.get("latestPush")
|
||||
hook = {
|
||||
"hookId": hook_id,
|
||||
"hookReady": bool(match and match.get("active") is True),
|
||||
"active": match.get("active") if match else None,
|
||||
"hookId": selected_hook_id,
|
||||
"hookReady": topology_ready,
|
||||
"active": selected_match.get("active") if selected_match else None,
|
||||
"status": result.get("status"),
|
||||
"latestDelivery": delivery.get("latest"),
|
||||
"latestDelivery": latest_delivery,
|
||||
"latestPushDelivery": latest_push_delivery,
|
||||
"latest": delivery.get("latest"),
|
||||
"latestPush": delivery.get("latestPush"),
|
||||
"error": result.get("body", "")[:500] if not result.get("ok") else delivery.get("error"),
|
||||
"selectedPushDelivery": selected_push_delivery,
|
||||
"deliverySelectionReason": ("committed-head-guid-repository-branch-after" if head_delivery_id else "latest-repository-branch-push-delivery") if selected_push_delivery else "target-branch-push-delivery-unavailable",
|
||||
"matchingHookCount": len(matches),
|
||||
"matchingHookIds": [item.get("id") for item in matches],
|
||||
"hookTopologyReason": topology_reason,
|
||||
"latest": latest_delivery,
|
||||
"latestPush": latest_push_delivery,
|
||||
"selectedPush": selected_push_delivery,
|
||||
"error": result.get("body", "")[:500] if not result.get("ok") else (delivery_errors[0] if delivery_errors else topology_reason),
|
||||
}
|
||||
rows.append(evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records, inbox_status.get("journal")))
|
||||
bridge = {
|
||||
|
||||
@@ -5,6 +5,76 @@ import { resolve } from "node:path";
|
||||
const evaluator = resolve(import.meta.dir, "platform_infra_gitea_status_evaluator.py");
|
||||
|
||||
describe("Gitea source authority status evaluator", () => {
|
||||
test("parses branch and exact snapshot from ls-remote output larger than the display tail budget", () => {
|
||||
const head = "2".repeat(40);
|
||||
const prefix = "refs/unidesk/snapshots/gitea-actions/unidesk-master-nc01";
|
||||
const historical = Array.from({ length: 420 }, (_, index) => {
|
||||
const sha = index.toString(16).padStart(40, "0");
|
||||
return `${sha}\t${prefix}/${sha}`;
|
||||
});
|
||||
const output = [
|
||||
`${head}\trefs/heads/master`,
|
||||
`${head}\t${prefix}/${head}`,
|
||||
...historical,
|
||||
].join("\n");
|
||||
expect(Buffer.byteLength(output)).toBeGreaterThan(20_000);
|
||||
const refs = evaluate({ action: "parse-ls-remote", output });
|
||||
expect(refs["refs/heads/master"]).toBe(head);
|
||||
expect(refs[`${prefix}/${head}`]).toBe(head);
|
||||
});
|
||||
|
||||
test("anchors the current committed head beyond more than fifty unrelated feature pushes", () => {
|
||||
const head = "7".repeat(40);
|
||||
const committed = committedInbox("master-durable-guid", head);
|
||||
const headRecord = evaluate({
|
||||
action: "select-committed-head-record",
|
||||
repoKey: "fixture",
|
||||
githubHead: head,
|
||||
records: [committedInbox("old-guid", "6".repeat(40)), committed],
|
||||
});
|
||||
expect(headRecord.deliveryId).toBe("master-durable-guid");
|
||||
expect(evaluate({
|
||||
action: "select-committed-head-record",
|
||||
repoKey: "fixture",
|
||||
githubHead: "8".repeat(40),
|
||||
records: [committed],
|
||||
})).toBeNull();
|
||||
const featureDeliveries = Array.from({ length: 75 }, (_, index) => ({
|
||||
id: String(10_000 - index),
|
||||
deliveryId: `feature-${index}`,
|
||||
deliveredAt: `2026-07-11T23:${String(index % 60).padStart(2, "0")}:00Z`,
|
||||
repository: "pikasTech/fixture",
|
||||
ref: `refs/heads/feature-${index}`,
|
||||
requestedCommit: index.toString(16).padStart(40, "0"),
|
||||
}));
|
||||
const masterDelivery = {
|
||||
id: "42",
|
||||
deliveryId: "master-durable-guid",
|
||||
deliveredAt: "2026-07-11T22:00:00Z",
|
||||
repository: "pikasTech/fixture",
|
||||
ref: "refs/heads/main",
|
||||
requestedCommit: head,
|
||||
};
|
||||
const failedRedelivery = {
|
||||
id: "43",
|
||||
deliveryId: "failed-redelivery-same-head",
|
||||
deliveredAt: "2026-07-11T23:59:59Z",
|
||||
statusCode: 502,
|
||||
repository: "pikasTech/fixture",
|
||||
ref: "refs/heads/main",
|
||||
requestedCommit: head,
|
||||
};
|
||||
const selected = evaluate({
|
||||
action: "select-target-delivery",
|
||||
deliveries: [...featureDeliveries, failedRedelivery, masterDelivery],
|
||||
repository: "pikasTech/fixture",
|
||||
ref: "refs/heads/main",
|
||||
requestedCommit: head,
|
||||
deliveryId: headRecord.deliveryId,
|
||||
});
|
||||
expect(selected).toEqual(masterDelivery);
|
||||
});
|
||||
|
||||
test("selects only the exact branch-derived immutable snapshot", () => {
|
||||
const head = "b".repeat(40);
|
||||
const result = evaluate({
|
||||
@@ -48,7 +118,48 @@ describe("Gitea source authority status evaluator", () => {
|
||||
expect(row.deliveryAccepted).toBe(false);
|
||||
expect(row.correlatedBridgeEvent).toBeNull();
|
||||
expect(row.bridgeEventStale).toBe(true);
|
||||
expect(row.staleReason).toBe("latest-push-delivery-failed-or-missing");
|
||||
expect(row.staleReason).toBe("target-branch-delivery-failed");
|
||||
});
|
||||
|
||||
test("selects the exact durable proof across duplicate exact-url hooks without hiding topology drift", () => {
|
||||
const head = "8".repeat(40);
|
||||
const row = evaluate({
|
||||
action: "evaluate-repository",
|
||||
repo: {
|
||||
key: "fixture",
|
||||
upstream: { repository: "pikasTech/fixture", branch: "main" },
|
||||
snapshot: { prefix: "refs/snapshots/source" },
|
||||
},
|
||||
hook: {
|
||||
hookId: 22,
|
||||
hookReady: false,
|
||||
active: true,
|
||||
status: 200,
|
||||
latest: { hookId: 11, deliveryId: "duplicate-unpersisted", event: "push", statusCode: 202 },
|
||||
latestPush: { hookId: 11, deliveryId: "duplicate-unpersisted", event: "push", statusCode: 202 },
|
||||
selectedPush: { hookId: 22, deliveryId: "durable-exact", event: "push", statusCode: 202, repository: "pikasTech/fixture", ref: "refs/heads/main", requestedCommit: head },
|
||||
deliverySelectionReason: "committed-durable-proof-for-github-head",
|
||||
matchingHookCount: 2,
|
||||
matchingHookIds: [11, 22],
|
||||
hookTopologyReason: "github-hook-duplicate-exact-url",
|
||||
},
|
||||
head: { ok: true, sha: head },
|
||||
mirror: {
|
||||
branchCommit: head,
|
||||
snapshotCommit: head,
|
||||
snapshotRef: `refs/snapshots/source/${head}`,
|
||||
},
|
||||
bridgeEvents: [terminalEvent("durable-exact", head)],
|
||||
inboxRecords: [committedInbox("durable-exact", head)],
|
||||
});
|
||||
expect(row.latestPushDelivery.deliveryId).toBe("duplicate-unpersisted");
|
||||
expect(row.selectedPushDelivery.deliveryId).toBe("durable-exact");
|
||||
expect(row.deliveryId).toBe("durable-exact");
|
||||
expect(row.deliverySelectionReason).toBe("committed-durable-proof-for-github-head");
|
||||
expect(row.durableInboxCommitted).toBe(true);
|
||||
expect(row.bridgeEventStale).toBe(false);
|
||||
expect(row.hookReady).toBe(false);
|
||||
expect(row.hookTopologyReason).toBe("github-hook-duplicate-exact-url");
|
||||
});
|
||||
|
||||
test("MATCH=false is always STALE=true and exact delivery correlation is repo isolated", () => {
|
||||
@@ -98,6 +209,7 @@ describe("Gitea source authority status evaluator", () => {
|
||||
branchCommit: head,
|
||||
snapshotCommit: head,
|
||||
deliveryId: "delivery-old",
|
||||
requestedCommit: older,
|
||||
statusCode: 202,
|
||||
bridgeEvents: [event],
|
||||
inboxRecords: [committedInbox("delivery-old", older, head, "superseded-with-immutable-snapshot")],
|
||||
@@ -182,6 +294,7 @@ function evaluateRow(input: {
|
||||
inboxRecords?: Array<Record<string, unknown>>;
|
||||
deliveredAt?: string;
|
||||
journalCreatedAt?: string;
|
||||
requestedCommit?: string;
|
||||
}): Record<string, any> {
|
||||
return evaluate({
|
||||
action: "evaluate-repository",
|
||||
@@ -196,7 +309,7 @@ function evaluateRow(input: {
|
||||
active: true,
|
||||
status: 200,
|
||||
latest: { deliveryId: input.deliveryId, event: "push", statusCode: input.statusCode, deliveredAt: input.deliveredAt },
|
||||
latestPush: { deliveryId: input.deliveryId, event: "push", statusCode: input.statusCode, deliveredAt: input.deliveredAt },
|
||||
latestPush: { deliveryId: input.deliveryId, event: "push", statusCode: input.statusCode, deliveredAt: input.deliveredAt, repository: "pikasTech/fixture", ref: "refs/heads/main", requestedCommit: input.requestedCommit ?? input.head },
|
||||
},
|
||||
head: { ok: true, sha: input.head },
|
||||
mirror: {
|
||||
@@ -214,6 +327,7 @@ function committedInbox(deliveryId: string, sourceCommit: string, authorityCommi
|
||||
return {
|
||||
deliveryId,
|
||||
repo: "fixture",
|
||||
requestedCommit: sourceCommit,
|
||||
state: "committed",
|
||||
result: {
|
||||
sourceCommit,
|
||||
|
||||
@@ -30,12 +30,34 @@ test("admission contract rejects malformed required declarations instead of sile
|
||||
expect(() => parsePacAdmissionContract(source)).toThrow("deliveryProvenance.required must be boolean true or false");
|
||||
});
|
||||
|
||||
test("admission queue transition contract rejects absent sources, unknown Tekton states, and duplicate transitions", () => {
|
||||
const source = Bun.YAML.parse(readFileSync(resolve(root, "config/platform-infra/pipelines-as-code.yaml"), "utf8")) as any;
|
||||
const absentSource = structuredClone(source);
|
||||
absentSource.deliveryProvenance.queueTransition.allowed[0].fromSpecStatus = "absent";
|
||||
expect(() => parsePacAdmissionContract(absentSource)).toThrow("fromSpecStatus must be a known Tekton v1 PipelineRun spec status");
|
||||
|
||||
const unknownTarget = structuredClone(source);
|
||||
unknownTarget.deliveryProvenance.queueTransition.allowed[0].toSpecStatus = "UnknownStatus";
|
||||
expect(() => parsePacAdmissionContract(unknownTarget)).toThrow("toSpecStatus must be a known Tekton v1 PipelineRun spec status or absent");
|
||||
|
||||
const duplicate = structuredClone(source);
|
||||
duplicate.deliveryProvenance.queueTransition.allowed.push(structuredClone(duplicate.deliveryProvenance.queueTransition.allowed[0]));
|
||||
expect(() => parsePacAdmissionContract(duplicate)).toThrow("deliveryProvenance.queueTransition.allowed must be unique");
|
||||
});
|
||||
|
||||
test("versioned admission desired state protects controller proof, candidate identity, params, and execution SA", () => {
|
||||
const contract = readPacAdmissionContract();
|
||||
const owningYaml = Bun.YAML.parse(readFileSync(resolve(root, "config/platform-infra/pipelines-as-code.yaml"), "utf8")) as any;
|
||||
expect(owningYaml.deliveryProvenance.terminalRoles).toBeUndefined();
|
||||
expect(contract.policyName).toEndWith("-v1");
|
||||
expect(contract.bindingName).toEndWith("-v1");
|
||||
expect(contract.policyName).toEndWith("-v2");
|
||||
expect(contract.bindingName).toEndWith("-v2");
|
||||
expect(contract.queueControllerUsername).toBe("system:serviceaccount:pipelines-as-code:pipelines-as-code-watcher");
|
||||
expect(contract.allowedQueueTransitions).toEqual([{
|
||||
fromSpecStatus: "PipelineRunPending",
|
||||
toSpecStatus: "absent",
|
||||
fromState: "queued",
|
||||
toState: "started",
|
||||
}]);
|
||||
expect(contract.child.enabled).toBe(false);
|
||||
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02"]);
|
||||
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
|
||||
@@ -52,7 +74,7 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
expect(expressions).toContain("spec.pipelineRef.name");
|
||||
expect(expressions).toContain("spec.params == oldObject.spec.params");
|
||||
expect(expressions).toContain("object.spec.pipelineRef == oldObject.spec.pipelineRef");
|
||||
expect(expressions).toContain("object.spec == oldObject.spec");
|
||||
expect(expressions).not.toContain("object.spec == oldObject.spec");
|
||||
expect(expressions).toContain("taskRunTemplate.serviceAccountName == oldObject.spec.taskRunTemplate.serviceAccountName");
|
||||
expect(expressions).toContain("object.metadata.name.startsWith");
|
||||
expect(expressions).toContain('object.metadata.labels["tekton.dev/pipeline"]');
|
||||
@@ -61,12 +83,38 @@ test("versioned admission desired state protects controller proof, candidate ide
|
||||
expect(expressions).toContain(contract.child.parentUidAnnotation);
|
||||
expect(expressions).toContain("oldObject");
|
||||
expect(JSON.stringify(items)).not.toContain('"kind":"ServiceAccount"');
|
||||
const specGuard = policy.spec.validations.find((item: any) => item.message.includes("PipelineRun spec is immutable"));
|
||||
const specGuard = policy.spec.validations.find((item: any) => item.message.includes("PipelineRun spec fields are immutable"));
|
||||
const queueGuard = policy.spec.validations.find((item: any) => item.message.includes("YAML-declared queue transition"));
|
||||
const oldRun = { spec: { pipelineSpec: { tasks: [{ name: "plan-artifacts" }] }, workspaces: [], timeouts: { pipeline: "1h" } } };
|
||||
const mutatedRun = structuredClone(oldRun);
|
||||
mutatedRun.spec.pipelineSpec.tasks[0].name = "forged-task";
|
||||
expect(specGuard.expression).toMatch(/object\.spec == oldObject\.spec/u);
|
||||
for (const field of ["managedBy", "params", "pipelineRef", "taskRunSpecs", "taskRunTemplate", "timeouts", "workspaces"]) {
|
||||
expect(specGuard.expression).toContain(`has(object.spec.${field})`);
|
||||
expect(specGuard.expression).toContain(`object.spec.${field} == oldObject.spec.${field}`);
|
||||
}
|
||||
expect(specGuard.expression).toContain("has(dyn(object.spec).pipelineSpec)");
|
||||
expect(specGuard.expression).toContain("has(dyn(oldObject.spec).pipelineSpec)");
|
||||
expect(specGuard.expression).toContain("dyn(object.spec).pipelineSpec == dyn(oldObject.spec).pipelineSpec");
|
||||
expect(specGuard.expression).not.toContain("has(object.spec.pipelineSpec)");
|
||||
expect(specGuard.expression).not.toContain("object.spec.pipelineSpec == oldObject.spec.pipelineSpec");
|
||||
expect(specGuard.expression).not.toContain("pipelinesascode.tekton.dev/state");
|
||||
expect(mutatedRun.spec).not.toEqual(oldRun.spec);
|
||||
expect(queueGuard.expression).toContain('request.userInfo.username == "system:serviceaccount:pipelines-as-code:pipelines-as-code-watcher"');
|
||||
expect(queueGuard.expression).toContain('oldObject.spec.status == "PipelineRunPending"');
|
||||
expect(queueGuard.expression).toContain("!has(object.spec.status)");
|
||||
expect(queueGuard.expression).toContain('oldObject.metadata.labels["pipelinesascode.tekton.dev/state"] == "queued"');
|
||||
expect(queueGuard.expression).toContain('object.metadata.labels["pipelinesascode.tekton.dev/state"] == "started"');
|
||||
expect(queueGuard.expression).toContain('oldObject.metadata.annotations["unidesk.ai/pac-admission-provenance"] == "admission-pac-v2:agentrun-nc01-v02"');
|
||||
expect(queueGuard.expression).toContain('object.metadata.annotations["unidesk.ai/pac-admission-provenance"] == "admission-pac-v2:agentrun-nc01-v02"');
|
||||
expect(queueGuard.expression).toContain("oldObject.metadata.name.startsWith");
|
||||
expect(queueGuard.expression).toContain("object.metadata.name.startsWith");
|
||||
expect(queueGuard.expression).toContain("oldObject.spec.taskRunTemplate.serviceAccountName");
|
||||
expect(queueGuard.expression).toContain("object.spec.taskRunTemplate.serviceAccountName");
|
||||
expect(queueGuard.expression.match(/admission-pac-v2:agentrun-nc01-v02/gu)).toHaveLength(2);
|
||||
expect(queueGuard.expression.match(/pipelines-as-code-watcher/gu)).toHaveLength(1);
|
||||
expect(queueGuard.expression.match(/agentrun-nc01-v02-tekton-runner/gu)).toHaveLength(2);
|
||||
expect(queueGuard.expression).not.toContain("admission-pac-v1:");
|
||||
expect(queueGuard.expression).not.toContain('== "Cancelled"');
|
||||
});
|
||||
|
||||
test("required consumer default RBAC has one automatic desired owner and no Tekton mutation verbs", () => {
|
||||
@@ -106,6 +154,37 @@ test("live admission evaluator requires exact desired hashes, observed generatio
|
||||
};
|
||||
const input = { policy, binding, role: rbac[0], roleBinding: rbac[1], namespace: "agentrun-ci", defaultCanMutate: false, expected };
|
||||
expect(evaluator.evaluatePacAdmissionState(input)).toMatchObject({ ready: true, activeAfter: "2026-01-01T00:00:01Z", defaultCanMutate: false });
|
||||
expect(evaluator.evaluatePacAdmissionState({
|
||||
...input,
|
||||
policy: {
|
||||
...policy,
|
||||
status: {
|
||||
...policy.status,
|
||||
typeChecking: {
|
||||
expressionWarnings: [{
|
||||
fieldRef: "spec.validations[7].expression",
|
||||
warning: "undefined field 'pipelineSpec'",
|
||||
}],
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-expression-warning"]) });
|
||||
const apiDefaultedPolicy = {
|
||||
...policy,
|
||||
spec: {
|
||||
...policy.spec,
|
||||
matchConstraints: {
|
||||
...policy.spec.matchConstraints,
|
||||
namespaceSelector: {},
|
||||
objectSelector: {},
|
||||
resourceRules: policy.spec.matchConstraints.resourceRules.map((rule: any) => ({ ...rule, scope: "*" })),
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, policy: apiDefaultedPolicy })).toMatchObject({
|
||||
ready: true,
|
||||
liveSpecSha256: expected.configSha256,
|
||||
});
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: true })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["default-serviceaccount-can-mutate-tekton-runs"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: null })).toMatchObject({ ready: false, defaultCanMutate: null, reasons: expect.arrayContaining(["default-serviceaccount-mutation-probe-unavailable"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({ ...input, role: { ...rbac[0], rules: [rbac[0].rules[0]] } })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["default-role-rules-mismatch"]) });
|
||||
@@ -129,6 +208,32 @@ test("live admission evaluator requires exact desired hashes, observed generatio
|
||||
...input,
|
||||
binding: { ...binding, spec: { ...binding.spec, matchResources: { namespaceSelector: { matchLabels: { disabled: "true" } } } } },
|
||||
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({
|
||||
...input,
|
||||
policy: {
|
||||
...apiDefaultedPolicy,
|
||||
spec: {
|
||||
...apiDefaultedPolicy.spec,
|
||||
matchConstraints: {
|
||||
...apiDefaultedPolicy.spec.matchConstraints,
|
||||
namespaceSelector: { matchLabels: { disabled: "true" } },
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]) });
|
||||
expect(evaluator.evaluatePacAdmissionState({
|
||||
...input,
|
||||
policy: {
|
||||
...apiDefaultedPolicy,
|
||||
spec: {
|
||||
...apiDefaultedPolicy.spec,
|
||||
matchConstraints: {
|
||||
...apiDefaultedPolicy.spec.matchConstraints,
|
||||
resourceRules: apiDefaultedPolicy.spec.matchConstraints.resourceRules.map((rule: any) => ({ ...rule, scope: "Namespaced" })),
|
||||
},
|
||||
},
|
||||
},
|
||||
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["admission-live-spec-sha256-mismatch"]) });
|
||||
});
|
||||
|
||||
test("AgentRun owning renderer emits ordered terminal roles over the shared workspace without changing build and publish steps", () => {
|
||||
|
||||
@@ -17,10 +17,20 @@ export interface PacAdmissionConsumerContract {
|
||||
readonly gitOps: { readonly repoUrl: string; readonly targetRevision: string };
|
||||
}
|
||||
|
||||
export interface PacAdmissionQueueTransitionContract {
|
||||
readonly fromSpecStatus: string;
|
||||
readonly toSpecStatus: string;
|
||||
readonly fromState: string;
|
||||
readonly toState: string;
|
||||
}
|
||||
|
||||
export interface PacAdmissionContract {
|
||||
readonly version: string;
|
||||
readonly markerAnnotation: string;
|
||||
readonly controllerUsername: string;
|
||||
readonly queueControllerUsername: string;
|
||||
readonly queueStateLabel: string;
|
||||
readonly allowedQueueTransitions: readonly PacAdmissionQueueTransitionContract[];
|
||||
readonly policyName: string;
|
||||
readonly bindingName: string;
|
||||
readonly failurePolicy: "Fail";
|
||||
@@ -51,9 +61,27 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
|
||||
const provenance = record(root.deliveryProvenance, `${configPath}#deliveryProvenance`);
|
||||
const child = record(provenance.child, `${configPath}#deliveryProvenance.child`);
|
||||
const controller = record(provenance.controller, `${configPath}#deliveryProvenance.controller`);
|
||||
const queueTransition = record(provenance.queueTransition, `${configPath}#deliveryProvenance.queueTransition`);
|
||||
const queueController = record(queueTransition.controller, `${configPath}#deliveryProvenance.queueTransition.controller`);
|
||||
const admission = record(provenance.admission, `${configPath}#deliveryProvenance.admission`);
|
||||
const controllerNamespace = required(controller.namespace, "deliveryProvenance.controller.namespace");
|
||||
const controllerServiceAccount = required(controller.serviceAccountName, "deliveryProvenance.controller.serviceAccountName");
|
||||
const queueControllerNamespace = required(queueController.namespace, "deliveryProvenance.queueTransition.controller.namespace");
|
||||
const queueControllerServiceAccount = required(queueController.serviceAccountName, "deliveryProvenance.queueTransition.controller.serviceAccountName");
|
||||
const queueStateLabel = required(queueTransition.stateLabel, "deliveryProvenance.queueTransition.stateLabel");
|
||||
const allowedQueueTransitions = recordArray(queueTransition.allowed, "deliveryProvenance.queueTransition.allowed").map((transition, index) => {
|
||||
const fromSpecStatus = pipelineRunSpecStatus(transition.fromSpecStatus, `deliveryProvenance.queueTransition.allowed[${index}].fromSpecStatus`, false);
|
||||
const toSpecStatus = pipelineRunSpecStatus(transition.toSpecStatus, `deliveryProvenance.queueTransition.allowed[${index}].toSpecStatus`, true);
|
||||
return {
|
||||
fromSpecStatus,
|
||||
toSpecStatus,
|
||||
fromState: required(transition.fromState, `deliveryProvenance.queueTransition.allowed[${index}].fromState`),
|
||||
toState: required(transition.toState, `deliveryProvenance.queueTransition.allowed[${index}].toState`),
|
||||
};
|
||||
});
|
||||
if (allowedQueueTransitions.length === 0) throw new Error("deliveryProvenance.queueTransition.allowed must not be empty");
|
||||
const transitionKeys = new Set(allowedQueueTransitions.map((transition) => [transition.fromSpecStatus, transition.toSpecStatus, transition.fromState, transition.toState].join("\u0000")));
|
||||
if (transitionKeys.size !== allowedQueueTransitions.length) throw new Error("deliveryProvenance.queueTransition.allowed must be unique");
|
||||
const validationActions = stringArray(admission.validationActions, "deliveryProvenance.admission.validationActions");
|
||||
if (validationActions.length !== 1 || validationActions[0] !== "Deny") throw new Error("deliveryProvenance.admission.validationActions must be exactly [Deny]");
|
||||
const consumers = recordArray(root.consumers, "consumers").flatMap((consumer, index) => {
|
||||
@@ -89,6 +117,9 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
|
||||
version,
|
||||
markerAnnotation: required(provenance.markerAnnotation, "deliveryProvenance.markerAnnotation"),
|
||||
controllerUsername: `system:serviceaccount:${controllerNamespace}:${controllerServiceAccount}`,
|
||||
queueControllerUsername: `system:serviceaccount:${queueControllerNamespace}:${queueControllerServiceAccount}`,
|
||||
queueStateLabel,
|
||||
allowedQueueTransitions,
|
||||
policyName,
|
||||
bindingName,
|
||||
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
|
||||
@@ -114,6 +145,8 @@ export function renderPacAdmissionDesiredFragment(targetId: string): string {
|
||||
const markerPresent = `has(object.metadata.annotations) && ${cel(annotation)} in object.metadata.annotations`;
|
||||
const oldMarkerPresent = `has(oldObject.metadata.annotations) && ${cel(annotation)} in oldObject.metadata.annotations`;
|
||||
const protectedUpdate = [markerPresent, oldMarkerPresent, ...consumers.flatMap((consumer) => [candidateExpression(consumer, "object"), candidateExpression(consumer, "oldObject")])].map((item) => `(${item})`).join(" || ");
|
||||
const allowedQueueStatusTransition = queueTransitionExpression(contract, consumers, annotation);
|
||||
const immutableSpecFields = ["managedBy", "params", "pipelineRef", "pipelineSpec", "taskRunSpecs", "taskRunTemplate", "timeouts", "workspaces"] as const;
|
||||
const immutableProofKeys = [
|
||||
["labels", "app.kubernetes.io/managed-by"],
|
||||
["labels", "pipelinesascode.tekton.dev/event-type"],
|
||||
@@ -171,8 +204,13 @@ export function renderPacAdmissionDesiredFragment(targetId: string): string {
|
||||
reason: "Invalid",
|
||||
},
|
||||
{
|
||||
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || object.spec == oldObject.spec`,
|
||||
message: "admission-proven PaC PipelineRun spec is immutable, including embedded pipelineSpec, workspaces, and timeouts",
|
||||
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${immutableSpecFields.map(specFieldEqual).join(" && ")})`,
|
||||
message: "admission-proven PaC PipelineRun spec fields are immutable, including embedded pipelineSpec, workspaces, and timeouts",
|
||||
reason: "Invalid",
|
||||
},
|
||||
{
|
||||
expression: `request.operation != 'UPDATE' || !(${protectedUpdate}) || (${specFieldEqual("status")} || (${allowedQueueStatusTransition}))`,
|
||||
message: "PaC PipelineRun spec.status may only follow an authenticated YAML-declared queue transition",
|
||||
reason: "Invalid",
|
||||
},
|
||||
...consumers.flatMap((consumer) => [{
|
||||
@@ -280,6 +318,46 @@ function pipelineRefEqual(): string {
|
||||
return `((${current}) == (${previous}) && (!(${current}) || object.spec.pipelineRef == oldObject.spec.pipelineRef))`;
|
||||
}
|
||||
|
||||
function specFieldEqual(field: string): string {
|
||||
if (field === "pipelineSpec") return schemalessSpecFieldEqual(field);
|
||||
const current = `has(object.spec.${field})`;
|
||||
const previous = `has(oldObject.spec.${field})`;
|
||||
return `((${current}) == (${previous}) && (!(${current}) || object.spec.${field} == oldObject.spec.${field}))`;
|
||||
}
|
||||
|
||||
function schemalessSpecFieldEqual(field: string): string {
|
||||
// Tekton pipelineSpec 保留未知字段且没有静态类型,必须在 dyn 根上检查存在性并做完整深度相等。
|
||||
const current = `has(dyn(object.spec).${field})`;
|
||||
const previous = `has(dyn(oldObject.spec).${field})`;
|
||||
return `((${current}) == (${previous}) && (!(${current}) || dyn(object.spec).${field} == dyn(oldObject.spec).${field}))`;
|
||||
}
|
||||
|
||||
function specStatusState(root: "object" | "oldObject", state: string): string {
|
||||
if (state === "absent") return `!has(${root}.spec.status)`;
|
||||
return `has(${root}.spec.status) && ${root}.spec.status == ${cel(state)}`;
|
||||
}
|
||||
|
||||
function metadataEntryState(root: "object" | "oldObject", map: "annotations" | "labels", key: string, value: string): string {
|
||||
return `has(${root}.metadata.${map}) && ${cel(key)} in ${root}.metadata.${map} && ${root}.metadata.${map}[${cel(key)}] == ${cel(value)}`;
|
||||
}
|
||||
|
||||
function exactConsumerAdmissionProof(consumer: PacAdmissionConsumerContract, annotation: string): string {
|
||||
const marker = (root: "object" | "oldObject") => metadataEntryState(root, "annotations", annotation, consumer.markerValue);
|
||||
return `(${marker("oldObject")} && ${marker("object")} && ${candidateExpression(consumer, "oldObject")} && ${candidateExpression(consumer, "object")} && ${usesServiceAccount(consumer, "oldObject")} && ${usesServiceAccount(consumer, "object")})`;
|
||||
}
|
||||
|
||||
function queueTransitionExpression(contract: PacAdmissionContract, consumers: readonly PacAdmissionConsumerContract[], annotation: string): string {
|
||||
const exactConsumerProof = consumers.map((consumer) => exactConsumerAdmissionProof(consumer, annotation)).join(" || ");
|
||||
const transitions = contract.allowedQueueTransitions.map((transition) => [
|
||||
specStatusState("oldObject", transition.fromSpecStatus),
|
||||
specStatusState("object", transition.toSpecStatus),
|
||||
metadataEntryState("oldObject", "labels", contract.queueStateLabel, transition.fromState),
|
||||
metadataEntryState("object", "labels", contract.queueStateLabel, transition.toState),
|
||||
`(${exactConsumerProof})`,
|
||||
].join(" && "));
|
||||
return `request.userInfo.username == ${cel(contract.queueControllerUsername)} && (${transitions.map((transition) => `(${transition})`).join(" || ")})`;
|
||||
}
|
||||
|
||||
function cel(value: string): string {
|
||||
return JSON.stringify(value);
|
||||
}
|
||||
@@ -299,6 +377,13 @@ function required(value: unknown, path: string): string {
|
||||
return value;
|
||||
}
|
||||
|
||||
function pipelineRunSpecStatus(value: unknown, path: string, allowAbsent: boolean): string {
|
||||
const status = required(value, path);
|
||||
const known = ["Cancelled", "CancelledRunFinally", "StoppedRunFinally", "PipelineRunPending"];
|
||||
if ((status === "absent" && !allowAbsent) || (status !== "absent" && !known.includes(status))) throw new Error(`${path} must be a known Tekton v1 PipelineRun spec status${allowAbsent ? " or absent" : ""}`);
|
||||
return status;
|
||||
}
|
||||
|
||||
function stringArray(value: unknown, path: string): string[] {
|
||||
if (!Array.isArray(value) || value.length === 0 || value.some((item) => typeof item !== "string" || item.length === 0)) throw new Error(`${path} must be a non-empty string array`);
|
||||
return value as string[];
|
||||
|
||||
@@ -1,8 +1,47 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import re
|
||||
import sys
|
||||
|
||||
|
||||
def parse_ls_remote_lines(lines):
|
||||
refs = {}
|
||||
for line in lines:
|
||||
parts = line.split()
|
||||
if len(parts) == 2:
|
||||
refs[parts[1]] = parts[0]
|
||||
return refs
|
||||
|
||||
|
||||
def select_committed_head_record(repo_key, github_head, records):
|
||||
candidates = []
|
||||
for record in records:
|
||||
if not isinstance(record, dict) or record.get("repo") != repo_key or record.get("state") != "committed":
|
||||
continue
|
||||
result = record.get("result") if isinstance(record.get("result"), dict) else {}
|
||||
if record.get("requestedCommit") != github_head or result.get("sourceCommit") != github_head:
|
||||
continue
|
||||
if result.get("authorityCommit") not in (None, github_head):
|
||||
continue
|
||||
candidates.append(record)
|
||||
return max(candidates, key=lambda item: (int(item.get("acceptedSequence") or 0), str(item.get("committedAt") or "")), default=None)
|
||||
|
||||
|
||||
def select_target_delivery(deliveries, repository, ref, requested_commit=None, delivery_id=None):
|
||||
candidates = []
|
||||
for delivery in deliveries:
|
||||
if not isinstance(delivery, dict):
|
||||
continue
|
||||
if delivery.get("repository") != repository or delivery.get("ref") != ref:
|
||||
continue
|
||||
if requested_commit is not None and delivery.get("requestedCommit") != requested_commit:
|
||||
continue
|
||||
if delivery_id is not None and _delivery_id(delivery) != str(delivery_id):
|
||||
continue
|
||||
candidates.append(delivery)
|
||||
return max(candidates, key=lambda item: (str(item.get("deliveredAt") or ""), str(item.get("id") or "")), default=None)
|
||||
|
||||
|
||||
def select_snapshot(branch_commit, snapshot_prefix, refs):
|
||||
if not branch_commit:
|
||||
return {
|
||||
@@ -30,10 +69,21 @@ def evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records=N
|
||||
snapshot_ref = mirror.get("snapshotRef")
|
||||
latest_delivery = hook.get("latest")
|
||||
latest_push_delivery = hook.get("latestPush")
|
||||
delivery_id = _delivery_id(latest_push_delivery)
|
||||
selected_push_delivery = hook.get("selectedPush") or latest_push_delivery
|
||||
delivery_id = _delivery_id(selected_push_delivery)
|
||||
delivery_repository = selected_push_delivery.get("repository") if isinstance(selected_push_delivery, dict) else None
|
||||
delivery_ref = selected_push_delivery.get("ref") if isinstance(selected_push_delivery, dict) else None
|
||||
delivery_requested_commit = selected_push_delivery.get("requestedCommit") if isinstance(selected_push_delivery, dict) else None
|
||||
delivery_scope_matches = bool(
|
||||
delivery_repository == repo["upstream"]["repository"]
|
||||
and delivery_ref == f"refs/heads/{repo['upstream']['branch']}"
|
||||
and isinstance(delivery_requested_commit, str)
|
||||
and re.fullmatch(r"[0-9a-f]{40,64}", delivery_requested_commit)
|
||||
)
|
||||
delivery_accepted = bool(
|
||||
isinstance(latest_push_delivery, dict)
|
||||
and latest_push_delivery.get("statusCode") in (200, 202)
|
||||
isinstance(selected_push_delivery, dict)
|
||||
and delivery_scope_matches
|
||||
and selected_push_delivery.get("statusCode") in (200, 202)
|
||||
)
|
||||
correlated_inbox_records = [
|
||||
record for record in inbox_records
|
||||
@@ -41,7 +91,7 @@ def evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records=N
|
||||
and record.get("deliveryId") == delivery_id
|
||||
]
|
||||
inbox_record = correlated_inbox_records[-1] if correlated_inbox_records else None
|
||||
pre_durable_bootstrap = _is_pre_durable_bootstrap(latest_push_delivery, inbox_record, journal)
|
||||
pre_durable_bootstrap = _is_pre_durable_bootstrap(selected_push_delivery, inbox_record, journal)
|
||||
inbox_state = inbox_record.get("state") if inbox_record else ("pre-durable-bootstrap" if pre_durable_bootstrap else None)
|
||||
inbox_result = inbox_record.get("result") if isinstance(inbox_record, dict) and isinstance(inbox_record.get("result"), dict) else {}
|
||||
correlated = [
|
||||
@@ -63,15 +113,17 @@ def evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records=N
|
||||
inbox_committed = bool(
|
||||
inbox_record
|
||||
and inbox_state == "committed"
|
||||
and inbox_record.get("requestedCommit") == delivery_requested_commit
|
||||
and inbox_result.get("sourceCommit") == delivery_requested_commit
|
||||
and (
|
||||
inbox_result.get("sourceCommit") == github_head
|
||||
delivery_requested_commit == github_head
|
||||
or (
|
||||
inbox_result.get("disposition") == "superseded-with-immutable-snapshot"
|
||||
and inbox_result.get("authorityCommit") == github_head
|
||||
)
|
||||
)
|
||||
)
|
||||
durable_proof_acceptable = inbox_committed or pre_durable_bootstrap
|
||||
durable_proof_acceptable = inbox_committed or (pre_durable_bootstrap and delivery_requested_commit == github_head)
|
||||
stale = not (
|
||||
authority_matches
|
||||
and delivery_accepted
|
||||
@@ -81,6 +133,7 @@ def evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records=N
|
||||
hook,
|
||||
head,
|
||||
delivery_accepted,
|
||||
delivery_scope_matches,
|
||||
inbox_record,
|
||||
pre_durable_bootstrap,
|
||||
github_head,
|
||||
@@ -106,7 +159,16 @@ def evaluate_repository(repo, hook, head, mirror, bridge_events, inbox_records=N
|
||||
"authorityMatches": authority_matches,
|
||||
"latestDelivery": latest_delivery,
|
||||
"latestPushDelivery": latest_push_delivery,
|
||||
"selectedPushDelivery": selected_push_delivery,
|
||||
"deliverySelectionReason": hook.get("deliverySelectionReason") or "latest-push-delivery",
|
||||
"matchingHookCount": hook.get("matchingHookCount"),
|
||||
"matchingHookIds": hook.get("matchingHookIds") or [],
|
||||
"hookTopologyReason": hook.get("hookTopologyReason"),
|
||||
"deliveryId": delivery_id,
|
||||
"deliveryRepository": delivery_repository,
|
||||
"deliveryRef": delivery_ref,
|
||||
"deliveryRequestedCommit": delivery_requested_commit,
|
||||
"deliveryScopeMatches": delivery_scope_matches,
|
||||
"correlatedBridgeEvent": correlated_event,
|
||||
"durableInboxRecord": inbox_record,
|
||||
"durableInboxState": inbox_state,
|
||||
@@ -126,13 +188,15 @@ def _delivery_id(delivery):
|
||||
return str(delivery.get("deliveryId") or delivery.get("guid") or delivery.get("id") or "")
|
||||
|
||||
|
||||
def _stale_reason(hook, head, delivery_accepted, inbox_record, pre_durable_bootstrap, github_head, branch_matches, snapshot_matches):
|
||||
def _stale_reason(hook, head, delivery_accepted, delivery_scope_matches, inbox_record, pre_durable_bootstrap, github_head, branch_matches, snapshot_matches):
|
||||
if hook.get("hookReady") is not True:
|
||||
return "github-hook-not-ready"
|
||||
if head.get("ok") is not True or not github_head:
|
||||
return "github-head-unavailable"
|
||||
if not delivery_scope_matches:
|
||||
return "target-branch-delivery-scope-mismatch-or-missing"
|
||||
if not delivery_accepted:
|
||||
return "latest-push-delivery-failed-or-missing"
|
||||
return "target-branch-delivery-failed"
|
||||
if not inbox_record and not pre_durable_bootstrap:
|
||||
return "no-correlated-durable-inbox-record"
|
||||
if not pre_durable_bootstrap:
|
||||
@@ -140,12 +204,17 @@ def _stale_reason(hook, head, delivery_accepted, inbox_record, pre_durable_boots
|
||||
if inbox_state != "committed":
|
||||
return f"durable-inbox-{inbox_state or 'unknown'}-not-committed"
|
||||
result = inbox_record.get("result") if isinstance(inbox_record.get("result"), dict) else {}
|
||||
delivery_requested_commit = (hook.get("selectedPush") or hook.get("latestPush") or {}).get("requestedCommit")
|
||||
inbox_proves_head = (
|
||||
result.get("sourceCommit") == github_head
|
||||
inbox_record.get("requestedCommit") == delivery_requested_commit
|
||||
and result.get("sourceCommit") == delivery_requested_commit
|
||||
and (
|
||||
delivery_requested_commit == github_head
|
||||
or (
|
||||
result.get("disposition") == "superseded-with-immutable-snapshot"
|
||||
and result.get("authorityCommit") == github_head
|
||||
)
|
||||
)
|
||||
)
|
||||
if not inbox_proves_head:
|
||||
return "correlated-inbox-proof-head-mismatch"
|
||||
@@ -183,7 +252,13 @@ def _error(hook, head, stale_reason):
|
||||
def main():
|
||||
payload = json.load(sys.stdin)
|
||||
action = payload.get("action")
|
||||
if action == "select-snapshot":
|
||||
if action == "parse-ls-remote":
|
||||
result = parse_ls_remote_lines(str(payload.get("output") or "").splitlines())
|
||||
elif action == "select-committed-head-record":
|
||||
result = select_committed_head_record(payload.get("repoKey"), payload.get("githubHead"), payload.get("records", []))
|
||||
elif action == "select-target-delivery":
|
||||
result = select_target_delivery(payload.get("deliveries", []), payload.get("repository"), payload.get("ref"), payload.get("requestedCommit"), payload.get("deliveryId"))
|
||||
elif action == "select-snapshot":
|
||||
result = select_snapshot(payload.get("branchCommit"), payload["snapshotPrefix"], payload.get("refs", {}))
|
||||
elif action == "evaluate-repository":
|
||||
result = evaluate_repository(payload["repo"], payload["hook"], payload["head"], payload.get("mirror", {}), payload.get("bridgeEvents", []), payload.get("inboxRecords", []), payload.get("journal"))
|
||||
|
||||
Reference in New Issue
Block a user