Merge remote-tracking branch 'origin/master' into fix/1784-managed-repository-materialization

This commit is contained in:
Codex
2026-07-12 01:07:09 +02:00
7 changed files with 579 additions and 53 deletions
+15 -4
View File
@@ -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);
+152 -30
View File
@@ -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", () => {
+87 -2
View File
@@ -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"))