Merge origin/master into feat/2010-hwlab-release-production-lane

This commit is contained in:
Codex
2026-07-15 09:26:46 +02:00
114 changed files with 4006 additions and 561 deletions
+19 -11
View File
@@ -222,9 +222,9 @@ function evaluatePacAdmissionState(inputValue) {
if (annotations["unidesk.ai/pac-controller-identity"] !== expected.controllerIdentity) reasons.push(`${kind}-controller-identity-mismatch`);
if (annotations["unidesk.ai/effective-config-sha256"] !== expected.configSha256) reasons.push(`${kind}-config-sha256-mismatch`);
}
if (policySpec.failurePolicy !== "Fail") reasons.push("policy-failure-policy-not-fail");
if (policySpec.failurePolicy !== "Ignore") reasons.push("policy-failure-policy-not-ignore");
if (bindingSpec.policyName !== expected.policyName) reasons.push("binding-policy-name-mismatch");
if (!Array.isArray(bindingSpec.validationActions) || bindingSpec.validationActions.length !== 1 || bindingSpec.validationActions[0] !== "Deny") reasons.push("binding-validation-actions-not-deny");
if (!Array.isArray(bindingSpec.validationActions) || stableCanonicalJson(bindingSpec.validationActions) !== stableCanonicalJson(["Warn", "Audit"])) reasons.push("binding-validation-actions-not-warning-only");
if (!Number.isInteger(policyMetadata.generation) || !Number.isInteger(policyStatus.observedGeneration) || policyStatus.observedGeneration < policyMetadata.generation) reasons.push("policy-generation-not-observed");
if (warnings.length > 0) reasons.push("policy-expression-warning");
if (roleMetadata.name !== "unidesk-pac-consumer-runner") reasons.push("default-role-missing");
@@ -258,6 +258,9 @@ function evaluatePacAdmissionState(inputValue) {
configured: true,
required: true,
ready: reasons.length === 0,
blocking: false,
warning: reasons.length > 0,
warnings: reasons,
source: "live-admissionregistration-v1",
policyName: stringOrNull(policyMetadata.name),
bindingName: stringOrNull(bindingMetadata.name),
@@ -357,13 +360,18 @@ function classifyPacPipelineRun(consumerInput, itemInput) {
deliveryClass,
reason,
candidate,
deliveryAuthorityEligible: outer && (!provenanceRequired || admissionProvenanceVerified),
deliveryAuthorityEligible: outer,
deliveryOwner: outer ? admissionProvenanceVerified ? "pac-controller-admission" : "pac-controller-observed" : null,
executionOwner: inner ? "tekton-child-unattached" : null,
parentPipelineRun: null,
parentRelation: inner ? "unproven" : null,
metadataObservationOnly: outer && !admissionProvenanceVerified,
admissionProvenanceVerified,
admissionWarning: outer && !admissionProvenanceVerified ? {
code: "pac-admission-provenance-unverified",
blocking: false,
reasons: Array.isArray(admissionState.reasons) ? admissionState.reasons : [],
} : null,
admissionPolicyReady: admissionState.ready === true,
admissionPolicyName: stringOrNull(admissionState.policyName),
admissionBindingName: stringOrNull(admissionState.bindingName),
@@ -1285,19 +1293,19 @@ function runPacStatusFixtureChecks() {
};
const classificationCases = [
{ id: "admission-provenance-verified", expected: true, item: verifiedRun, state: admissionState },
{ id: "pre-bootstrap-run-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, creationTimestamp: "2025-12-31T23:59:59Z" } }, state: admissionState },
{ id: "forged-name-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "forged-label-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", labels: { ...verifiedRun.metadata.labels, "tekton.dev/pipeline": consumer.pipeline }, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "forged-pipeline-ref-candidate-fails-closed", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } }, spec: { ...verifiedRun.spec, pipelineRef: { name: consumer.pipeline } } }, state: admissionState },
{ id: "wrong-service-account-fails-closed", expected: false, item: { ...verifiedRun, spec: { taskRunTemplate: { serviceAccountName: "default" } } }, state: admissionState },
{ id: "policy-identity-mismatch-fails-closed", expected: false, item: verifiedRun, state: { ...admissionState, configSha256: `sha256:${"2".repeat(64)}` } },
{ id: "pre-bootstrap-run-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, creationTimestamp: "2025-12-31T23:59:59Z" } }, state: admissionState },
{ id: "marker-mismatch-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "candidate-label-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", labels: { ...verifiedRun.metadata.labels, "tekton.dev/pipeline": consumer.pipeline }, annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } } }, state: admissionState },
{ id: "candidate-pipeline-ref-warning-only", expected: false, item: { ...verifiedRun, metadata: { ...verifiedRun.metadata, name: "unrelated", annotations: { ...verifiedRun.metadata.annotations, [provenance.markerAnnotation]: "forged" } }, spec: { ...verifiedRun.spec, pipelineRef: { name: consumer.pipeline } } }, state: admissionState },
{ id: "service-account-mismatch-warning-only", expected: false, item: { ...verifiedRun, spec: { taskRunTemplate: { serviceAccountName: "default" } } }, state: admissionState },
{ id: "policy-identity-mismatch-warning-only", expected: false, item: verifiedRun, state: { ...admissionState, configSha256: `sha256:${"2".repeat(64)}` } },
];
for (const item of classificationCases) {
const result = classifyPacPipelineRun({ ...consumer, admissionState: item.state }, item.item);
checks.push({
id: item.id,
ok: result.admissionProvenanceVerified === item.expected && result.deliveryAuthorityEligible === item.expected,
expectedOk: item.expected,
ok: result.admissionProvenanceVerified === item.expected && result.deliveryAuthorityEligible === true,
expectedOk: true,
actualOk: result.deliveryAuthorityEligible,
expectedCode: item.expected ? "admission-owned-pac-push-provenance-verified" : "pac-push-metadata-observed-but-admission-provenance-unverified",
actualCode: result.reason,
@@ -0,0 +1,205 @@
import { rmSync } from "node:fs";
import { describe, expect, test } from "bun:test";
import { runAgentRunCommand } from "./agentrun";
const RUN_ID = "run_long_poll_fixture";
const agentRunClientYaml = [
"version: 1",
"kind: AgentRunConfig",
"metadata:",
" name: agentrun",
"manager:",
" baseUrl: __BASE_URL__",
" timeoutMs: 15000",
"auth:",
" env: HWLAB_API_KEY",
" file: /tmp/hwlab-api-key.env",
" header: Authorization",
" scheme: Bearer",
"client:",
" role: render-only",
" transport: direct-http",
" sessionPolicy:",
" tenantId: unidesk",
" projectId: test",
" providerId: NC01",
" backendProfile: codex",
" workspaceRef:",
" kind: opaque",
" executionPolicy:",
" sandbox: workspace-write",
" approval: never",
" timeoutMs: 7200000",
" network: enabled",
" secretScope:",
" allowCredentialEcho: false",
].join("\n");
function renderedText(value: Awaited<ReturnType<typeof runAgentRunCommand>>): string {
if (!("renderedText" in value) || typeof value.renderedText !== "string") throw new Error("expected rendered AgentRun CLI output");
return value.renderedText;
}
function event(seq: number): Record<string, unknown> {
return {
id: `evt_${seq}`,
seq,
type: "backend_status",
payload: { commandId: "cmd_fixture", phase: `phase-${seq}`, message: `event-${seq}` },
};
}
async function withManager(
handler: (request: Request) => Response | Promise<Response>,
action: () => Promise<void>,
): Promise<void> {
const server = Bun.serve({ port: 0, fetch: handler });
const previousConfig = process.env.AGENTRUN_CLIENT_CONFIG;
const previousKey = process.env.HWLAB_API_KEY;
const configPath = `/tmp/unidesk-agentrun-events-long-poll-${process.pid}-${Date.now()}.yaml`;
process.env.AGENTRUN_CLIENT_CONFIG = configPath;
process.env.HWLAB_API_KEY = "fixture-secret";
await Bun.write(configPath, agentRunClientYaml.replace("__BASE_URL__", server.url.href.replace(/\/$/u, "")));
try {
await action();
} finally {
server.stop(true);
rmSync(configPath, { force: true });
if (previousConfig === undefined) delete process.env.AGENTRUN_CLIENT_CONFIG;
else process.env.AGENTRUN_CLIENT_CONFIG = previousConfig;
if (previousKey === undefined) delete process.env.HWLAB_API_KEY;
else process.env.HWLAB_API_KEY = previousKey;
}
}
describe.serial("AgentRun events manager-held long polling", () => {
test("forwards default 120s timeout and returns as soon as expect is reached", async () => {
let requests = 0;
await withManager(async (request) => {
requests += 1;
const url = new URL(request.url);
expect(url.pathname).toBe(`/api/v1/runs/${RUN_ID}/events`);
expect(url.searchParams.get("afterSeq")).toBe("100");
expect(url.searchParams.get("expect")).toBe("2");
expect(url.searchParams.get("timeoutMs")).toBe("120000");
expect(url.searchParams.get("limit")).toBe("21");
await Promise.all([Bun.sleep(10), Bun.sleep(20)]);
return Response.json({
ok: true,
data: {
items: [event(101), event(102)],
timedOut: false,
observed: 2,
nextAfterSeq: 102,
completionReason: "expect-reached",
},
});
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "100", "--expect", "2", "-o", "json"]);
expect(result.ok).toBe(true);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ count: 2, expect: 2, timeoutMs: 120000, timedOut: false, observed: 2, nextAfterSeq: 102, completionReason: "expect-reached" });
expect(requests).toBe(1);
});
});
test("renders partial and empty timeout responses with an explicit cursor", async () => {
for (const fixture of [
{ items: [event(201)], observed: 1, nextAfterSeq: 201 },
{ items: [], observed: 0, nextAfterSeq: 200 },
]) {
await withManager((request) => {
const url = new URL(request.url);
expect(url.searchParams.get("timeoutMs")).toBe("3000");
return Response.json({ ok: true, data: { ...fixture, timedOut: true, completionReason: "timeout" } });
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "200", "--expect", "10", "--timeout", "3s", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ timedOut: true, observed: fixture.observed, nextAfterSeq: fixture.nextAfterSeq, completionReason: "timeout" });
});
}
});
test("returns concurrent event arrivals in one manager request", async () => {
let requests = 0;
await withManager(async () => {
requests += 1;
const arrivals: Record<string, unknown>[] = [];
await Promise.all([
Bun.sleep(10).then(() => arrivals.push(event(301))),
Bun.sleep(20).then(() => arrivals.push(event(302))),
]);
return Response.json({ ok: true, data: { items: arrivals, timedOut: false, observed: 2, nextAfterSeq: 302, completionReason: "expect-reached" } });
}, async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "300", "--expect", "2", "--timeout", "1m", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as { items?: Array<{ seq?: number }> };
expect(payload.items?.map((item) => item.seq)).toEqual([301, 302]);
expect(requests).toBe(1);
});
});
test("returns immediately when the target is terminal before expect is reached", async () => {
await withManager(() => Response.json({
ok: true,
data: {
items: [],
timedOut: false,
observed: 0,
nextAfterSeq: 400,
completionReason: "target-completed",
},
}), async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "400", "--expect", "10", "--timeout", "120s", "-o", "json"]);
const payload = JSON.parse(renderedText(result)) as Record<string, unknown>;
expect(payload).toMatchObject({ timedOut: false, observed: 0, nextAfterSeq: 400, completionReason: "target-completed" });
});
});
test("rejects invalid duration before request and preserves manager errors", async () => {
let requests = 0;
await withManager(() => {
requests += 1;
return Response.json({ ok: false, failureKind: "manager-unavailable", message: "event notifier unavailable" }, { status: 503 });
}, async () => {
const invalid = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "3minutes", "-o", "json"]);
expect(invalid.ok).toBe(false);
expect(renderedText(invalid)).toContain("--timeout must use a positive integer duration with ms, s, or m units");
expect(requests).toBe(0);
const failed = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "2", "--timeout", "1s", "-o", "json"]);
expect(failed.ok).toBe(false);
expect(renderedText(failed)).toContain("event notifier unavailable");
expect(requests).toBe(1);
});
});
test("fails visibly when a manager ignores the long-poll response contract", async () => {
await withManager(() => Response.json({ ok: true, data: { items: [], nextAfterSeq: 500 } }), async () => {
const result = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--after-seq", "500", "--expect", "2", "--timeout", "1s", "-o", "json"]);
expect(result.ok).toBe(false);
expect(renderedText(result)).toContain("schema-mismatch");
expect(renderedText(result)).toContain("must include boolean timedOut and non-negative observed fields");
});
});
test("accepts 60m and rejects zero, values above 60m, and hour units before request", async () => {
let requests = 0;
await withManager((request) => {
requests += 1;
const url = new URL(request.url);
expect(url.searchParams.get("timeoutMs")).toBe("3600000");
return Response.json({ ok: true, data: { items: [], timedOut: true, observed: 0, nextAfterSeq: 0, completionReason: "timeout" } });
}, async () => {
const accepted = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "1", "--timeout", "60m", "-o", "json"]);
expect(accepted.ok).toBe(true);
expect(requests).toBe(1);
for (const invalidDuration of ["0ms", "61m", "1h"]) {
const invalid = await runAgentRunCommand(null, ["events", `run/${RUN_ID}`, "--expect", "1", "--timeout", invalidDuration, "-o", "json"]);
expect(invalid.ok).toBe(false);
}
expect(requests).toBe(1);
});
});
});
+1
View File
@@ -371,6 +371,7 @@ export function agentRunQuery(args: string[], names: string[]): string {
const params = new URLSearchParams();
const keyMap: Record<string, string> = {
"after-seq": "afterSeq",
"timeout-ms": "timeoutMs",
"backend-profile": "backendProfile",
"command": "commandId",
"command-id": "commandId",
+5 -4
View File
@@ -59,7 +59,7 @@ export function agentRunHelp(): unknown {
"bun scripts/cli.ts agentrun get tasks -o json",
"bun scripts/cli.ts agentrun describe task/<taskId>",
"bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
"bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
"bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq>",
"bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"bun scripts/cli.ts agentrun result run/<runId> --command <commandId>",
@@ -292,9 +292,10 @@ export function agentRunHelpText(args: string[]): string {
}
if (verb === "events") {
return [
"Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--limit 100] [-o json|yaml] [--full|--raw]",
"Usage: bun scripts/cli.ts agentrun events run/<runId> [--after-seq N] [--expect N [--timeout 120s]] [--limit 100] [-o json|yaml] [--full|--raw]",
" bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq> [-o json|yaml]",
"",
"--expect waits in one manager-held request until N new events, target completion, or timeout; --timeout defaults to 120s. --limit remains an independent output budget.",
"--detail-seq returns one bounded Secret-safe EventDetail projection. Use the separate --after-seq <seq-1> --limit 1 --full|--raw path only for explicit complete disclosure.",
].join("\n");
}
@@ -390,7 +391,7 @@ export function agentRunHelpText(args: string[]): string {
" bun scripts/cli.ts agentrun get tasks --queue commander --limit 20",
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
"",
"Use --raw on a resource command when you need the direct AgentRun REST envelope.",
@@ -407,7 +408,7 @@ export function agentRunHelpText(args: string[]): string {
" bun scripts/cli.ts agentrun describe task/<taskId>",
" bun scripts/cli.ts agentrun describe task/<taskId> --input -o json",
" bun scripts/cli.ts agentrun describe run/<runId> --node NC01 --lane nc01-v02",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --after-seq 0 --expect 10 --timeout 120s --limit 100",
" bun scripts/cli.ts agentrun events run/<runId> --detail-seq <seq>",
" bun scripts/cli.ts agentrun logs session/<sessionId> --tail 100",
" bun scripts/cli.ts agentrun retry task/<taskId> --idempotency-key <key> --reason <text> --dry-run",
+11
View File
@@ -360,6 +360,13 @@ export interface EventLikePageDisclosure {
hasMore: boolean;
nextAfterSeq: number | string | null;
detailCommand: string;
wait: {
expect: number;
timeoutMs: number;
timedOut: boolean;
observed: number;
completionReason: string | null;
} | null;
}
export function renderEventLike(command: string, raw: Record<string, unknown>, options: AgentRunResourceOptions, kind: string, items: Record<string, unknown>[], sourceName: string, pageDisclosure: EventLikePageDisclosure | null = null): RenderedCliResult {
@@ -376,6 +383,7 @@ export function renderEventLike(command: string, raw: Record<string, unknown>, o
effectiveLimit: pageDisclosure.effectiveLimit,
fetchedCount: pageDisclosure.fetchedCount,
hasMore: pageDisclosure.hasMore,
...(pageDisclosure.wait === null ? {} : pageDisclosure.wait),
disclosure: {
output: "omitted; metadata only",
identity: "item.identity; detail --detail-seq item.seq",
@@ -415,6 +423,9 @@ export function renderEventLike(command: string, raw: Record<string, unknown>, o
])),
];
if (pageDisclosure !== null) lines.push(`Detail: ${pageDisclosure.detailCommand}`);
if (pageDisclosure?.wait !== null && pageDisclosure?.wait !== undefined) {
lines.push(`Wait: expect=${pageDisclosure.wait.expect} observed=${pageDisclosure.wait.observed} timedOut=${String(pageDisclosure.wait.timedOut)} timeoutMs=${pageDisclosure.wait.timeoutMs} nextAfterSeq=${String(nextAfterSeq ?? 0)}${pageDisclosure.wait.completionReason === null ? "" : ` reason=${pageDisclosure.wait.completionReason}`}`);
}
if (nextAfterSeq !== undefined && nextAfterSeq !== null) lines.push(`Next: bun scripts/cli.ts ${nextPagedResourceCommand(command, String(nextAfterSeq), options.limit)}`);
return renderedCliResult(raw.ok !== false, command, lines.join("\n"));
}
+70 -4
View File
@@ -178,6 +178,8 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
commandId: null,
sessionId: null,
afterSeq: null,
expect: null,
timeoutMs: 120_000,
eventDetailSeq: null,
tail: null,
fullText: false,
@@ -196,7 +198,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
mdtodoId: null,
passthroughArgs: [],
};
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const valueFlags = new Set(["-o", "--output", "--limit", "--cursor", "--queue", "--state", "--reader-id", "--task", "--task-id", "--run", "--run-id", "--command", "--command-id", "--session", "--session-id", "--after-seq", "--expect", "--timeout", "--detail-seq", "--tail", "--reason", "-f", "--file", "--filename", "--aipod", "--idempotency-key", "--node", "--lane", "--target", "--target-workspace", "--repo", "--ref", "--mdtodo-id"]);
const booleanFlags = new Set(["--full", "--raw", "--debug", "--input", "--unread", "--dry-run", "--full-text", "--prompt-stdin", "--stdin"]);
for (let index = 0; index < args.length; index += 1) {
const arg = args[index] ?? "";
@@ -243,7 +245,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
);
}
if (options.eventDetailSeq !== null) {
const conflictingOptions = ["--after-seq", "--limit", "--full", "--raw"]
const conflictingOptions = ["--after-seq", "--expect", "--timeout", "--limit", "--full", "--raw"]
.filter((flag) => resourceOptionPresent(args, flag));
if (conflictingOptions.length > 0) {
throw new AgentRunRestError(
@@ -253,7 +255,7 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
details: {
conflictingOptions: ["--detail-seq", ...conflictingOptions],
recoveryActions: [
"Use --detail-seq <seq> without --after-seq, --limit, --full, or --raw for the bounded Secret-safe EventDetail projection.",
"Use --detail-seq <seq> without --after-seq, --expect, --timeout, --limit, --full, or --raw for the bounded Secret-safe EventDetail projection.",
"Use --after-seq <seq-1> --limit 1 --full -o json or --raw only when explicitly requesting the complete durable event.",
],
valuesPrinted: false,
@@ -262,6 +264,18 @@ export function parseResourceOptions(args: string[]): AgentRunResourceOptions {
);
}
}
if (resourceOptionPresent(args, "--timeout") && options.expect === null) {
throw new AgentRunRestError(
"validation-failed",
"--timeout requires --expect <N>; timeout controls the manager-held long poll and is not a client-side polling interval.",
{
details: {
recoveryActions: ["Use --expect <N> [--timeout <duration>] or remove --timeout for an immediate paged read."],
valuesPrinted: false,
},
},
);
}
return options;
}
@@ -278,6 +292,18 @@ function validateResourceOptionsForVerb(verb: AgentRunResourceVerb, options: Age
},
);
}
if (options.expect !== null && verb !== "events") {
throw new AgentRunRestError(
"validation-failed",
"--expect and --timeout are supported only by agentrun events run/<runId>.",
{
details: {
recoveryActions: ["Use: bun scripts/cli.ts agentrun events run/<runId> --after-seq <N> --expect <N> [--timeout 120s]."],
valuesPrinted: false,
},
},
);
}
}
function resourceOptionPresent(args: string[], flag: string): boolean {
@@ -308,6 +334,8 @@ export function applyResourceOption(options: AgentRunResourceOptions, flag: stri
else if (flag === "--command" || flag === "--command-id") options.commandId = requiredValue(value, flag);
else if (flag === "--session" || flag === "--session-id") options.sessionId = requiredValue(value, flag);
else if (flag === "--after-seq") options.afterSeq = parseNonNegativeInt(value, "--after-seq", 0, Number.MAX_SAFE_INTEGER);
else if (flag === "--expect") options.expect = parsePositiveInt(value, "--expect");
else if (flag === "--timeout") options.timeoutMs = parseDurationMs(value, "--timeout");
else if (flag === "--detail-seq") options.eventDetailSeq = parsePositiveInt(value, "--detail-seq");
else if (flag === "--tail") options.tail = parseNonNegativeInt(value, "--tail", 100, 1000);
else if (flag === "--reason") options.reason = requiredValue(value, flag);
@@ -330,6 +358,17 @@ function parsePositiveInt(raw: string | null, flag: string): number {
return value;
}
export function parseDurationMs(raw: string | null, flag: string): number {
if (raw === null || raw.length === 0) throw new Error(`${flag} requires a duration`);
const match = /^(\d+)(ms|s|m)$/u.exec(raw.trim());
if (match === null) throw new Error(`${flag} must use a positive integer duration with ms, s, or m units, such as 500ms, 120s, or 3m`);
const amount = Number(match[1]);
const multiplier = match[2] === "ms" ? 1 : match[2] === "s" ? 1_000 : 60_000;
const value = amount * multiplier;
if (!Number.isSafeInteger(value) || value < 1 || value > 3_600_000) throw new Error(`${flag} must be between 1ms and 60m`);
return value;
}
export function parseNonNegativeInt(raw: string | null, flag: string, defaultValue: number, maxValue: number): number {
if (raw === null || raw.length === 0) return defaultValue;
const value = Number(raw);
@@ -607,6 +646,7 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
const effectiveLimit = options.full || options.raw ? requestedLimit : Math.min(requestedLimit, 20);
const requestLimit = options.full || options.raw ? effectiveLimit : Math.min(500, effectiveLimit + 1);
const eventArgs = ["events", runId, "--after-seq", String(options.afterSeq ?? 0), "--limit", String(requestLimit)];
if (options.expect !== null) eventArgs.push("--expect", String(options.expect), "--timeout-ms", String(options.timeoutMs));
const result = await runAgentRunRestCommand(config, "runs", eventArgs);
if (options.raw) return renderMachine(command, result, "json", result.ok !== false);
if (options.full) return renderMachine(command, result, options.output === "yaml" ? "yaml" : "json", result.ok !== false);
@@ -616,7 +656,32 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
const hasMore = fetchedItems.length > visibleItems.length;
const lastVisibleSeq = visibleItems.length === 0 ? null : nonNegativeIntegerOrNull(visibleItems.at(-1)?.seq);
const responseNextAfterSeq = nonNegativeIntegerOrNull(record(innerData(result)).nextAfterSeq);
const nextAfterSeq = hasMore ? lastVisibleSeq : responseNextAfterSeq;
const nextAfterSeq = hasMore ? lastVisibleSeq : responseNextAfterSeq ?? options.afterSeq ?? 0;
const waitData = record(innerData(result));
const observed = nonNegativeIntegerOrNull(waitData.observed);
if (options.expect !== null && (typeof waitData.timedOut !== "boolean" || observed === null)) {
throw new AgentRunRestError(
"schema-mismatch",
"AgentRun manager long-poll response must include boolean timedOut and non-negative observed fields.",
{
details: {
runId,
expect: options.expect,
timeoutMs: options.timeoutMs,
returnedTimedOutType: typeof waitData.timedOut,
returnedObserved: waitData.observed ?? null,
valuesPrinted: false,
},
},
);
}
const waitDisclosure = options.expect === null ? null : {
expect: options.expect,
timeoutMs: options.timeoutMs,
timedOut: waitData.timedOut === true,
observed: observed ?? fetchedItems.length,
completionReason: stringOrNull(waitData.completionReason) ?? stringOrNull(waitData.reason),
};
const targetArgs = [
options.node === null ? null : `--node ${options.node}`,
options.lane === null ? null : `--lane ${options.lane}`,
@@ -629,6 +694,7 @@ export async function resourceEvents(config: UniDeskConfig | null, command: stri
hasMore,
nextAfterSeq,
detailCommand,
wait: waitDisclosure,
});
}
+17 -7
View File
@@ -523,7 +523,16 @@ export async function runAgentRunSessionsRest(action: string | undefined, id: st
export async function runAgentRunRunsRest(action: string | undefined, id: string | undefined, args: string[]): Promise<Record<string, unknown>> {
if (action === "create") return await agentRunRestRequest("agentrun runs create", "POST", "/api/v1/runs", await requiredJsonBody(args, "runs create"));
if (action === "show" && id) return await agentRunRestRequest("agentrun runs show", "GET", `/api/v1/runs/${encodeURIComponent(id)}`);
if (action === "events" && id) return await agentRunRestRequest("agentrun runs events", "GET", `/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "limit"])}`);
if (action === "events" && id) {
const timeoutMs = Number(agentRunOption(args, "timeout-ms") ?? "0");
return await agentRunRestRequest(
"agentrun runs events",
"GET",
`/api/v1/runs/${encodeURIComponent(id)}/events${agentRunQuery(args, ["after-seq", "expect", "timeout-ms", "limit"])}`,
undefined,
Number.isSafeInteger(timeoutMs) && timeoutMs > 0 ? timeoutMs + 5_000 : undefined,
);
}
if (action === "result" && id) return await agentRunRestRequest("agentrun runs result", "GET", `/api/v1/runs/${encodeURIComponent(id)}/result${agentRunQuery(args, ["command-id", "command"])}`);
if (action === "cancel" && id) return await agentRunRestRequest("agentrun runs cancel", "POST", `/api/v1/runs/${encodeURIComponent(id)}/cancel`, cancelBodyFromArgs(args));
throw new AgentRunRestError("validation-failed", `unsupported runs command: ${[action, id].filter(Boolean).join(" ") || "(empty)"}`);
@@ -1157,14 +1166,15 @@ export async function sessionRunnerJobBody(args: string[], defaults: Record<stri
return runnerBody;
}
export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown): Promise<Record<string, unknown>> {
export async function agentRunRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body?: unknown, requestTimeoutMs?: number): Promise<Record<string, unknown>> {
const clientConfig = readAgentRunClientConfig();
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget);
const effectiveTimeoutMs = Math.max(clientConfig.manager.timeoutMs, requestTimeoutMs ?? 0);
if (activeAgentRunRestTarget !== null) return await agentRunLaneRestRequest(command, method, pathValue, body, clientConfig, activeAgentRunRestTarget, effectiveTimeoutMs);
const auth = resolveAgentRunAuth(clientConfig);
const bridgeBase = agentRunRestBridgeMetadata(clientConfig, auth, method, pathValue);
const startedAt = Date.now();
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), clientConfig.manager.timeoutMs);
const timeout = setTimeout(() => controller.abort(), effectiveTimeoutMs);
let response: Response;
try {
const headers: Record<string, string> = {
@@ -1178,7 +1188,7 @@ export async function agentRunRestRequest(command: string, method: AgentRunHttpM
response = await fetch(new URL(pathValue, clientConfig.manager.baseUrl), init);
} catch (error) {
const timedOut = isAbortLikeError(error);
throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${clientConfig.manager.timeoutMs}ms` : `AgentRun server connection failed for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
throw new AgentRunRestError(timedOut ? "agentrun-timeout" : "agentrun-connect-failed", timedOut ? `AgentRun server request timed out for ${method} ${pathValue} after ${effectiveTimeoutMs}ms` : `AgentRun server connection failed for ${method} ${pathValue}: ${error instanceof Error ? error.message : String(error)}`, { bridge: { ...bridgeBase, elapsedMs: Date.now() - startedAt } });
} finally {
clearTimeout(timeout);
}
@@ -1215,7 +1225,7 @@ export function isAbortLikeError(error: unknown): boolean {
return error.name === "AbortError" || /abort|timed out|timeout/iu.test(error.message);
}
export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget): Promise<Record<string, unknown>> {
export async function agentRunLaneRestRequest(command: string, method: AgentRunHttpMethod, pathValue: string, body: unknown, clientConfig: AgentRunClientConfig, target: AgentRunRestTarget, requestTimeoutMs = clientConfig.manager.timeoutMs): Promise<Record<string, unknown>> {
const bridgeBase = agentRunLaneRestBridgeMetadata(clientConfig, target, method, pathValue);
const startedAt = Date.now();
const proxyBaseUrl = `http://127.0.0.1:${target.spec.runtime.managerPort}`;
@@ -1225,7 +1235,7 @@ export async function agentRunLaneRestRequest(command: string, method: AgentRunH
body: body ?? null,
baseUrl: proxyBaseUrl,
serviceBaseUrl: target.spec.runtime.internalBaseUrl,
timeoutMs: clientConfig.manager.timeoutMs,
timeoutMs: requestTimeoutMs,
authEnv: clientConfig.auth.env,
header: clientConfig.auth.header,
scheme: clientConfig.auth.scheme,
+2
View File
@@ -76,6 +76,8 @@ export interface AgentRunResourceOptions {
commandId: string | null;
sessionId: string | null;
afterSeq: number | null;
expect: number | null;
timeoutMs: number;
eventDetailSeq: number | null;
tail: number | null;
fullText: boolean;
@@ -0,0 +1,153 @@
import { describe, expect, test } from "bun:test";
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { resolve } from "node:path";
import { renderPikaoaTestDesiredPipeline } from "./pikaoa-test-delivery-renderer";
function pacDocument(): Record<string, unknown> {
return Bun.YAML.parse(readFileSync(resolve(import.meta.dir, "../../config/platform-infra/pipelines-as-code.yaml"), "utf8")) as Record<string, unknown>;
}
function rendererBinding(id = "pikaoa-test-nc01"): Parameters<typeof renderPikaoaTestDesiredPipeline>[0] {
const document = pacDocument();
const repositories = document.repositories as Array<Record<string, unknown>>;
const consumers = document.consumers as Array<Record<string, unknown>>;
const consumer = consumers.find((item) => item.id === id);
const repository = repositories.find((item) => item.id === consumer?.repositoryRef);
if (repository === undefined || consumer === undefined) throw new Error(`PikaOA PaC fixture is required: ${id}`);
return {
consumer,
repository: {
...repository,
params: {
...(repository.params as Record<string, unknown>),
...((consumer.params as Record<string, unknown> | undefined) ?? {}),
},
},
} as Parameters<typeof renderPikaoaTestDesiredPipeline>[0];
}
function sourceFixture(): string {
const root = mkdtempSync(resolve(tmpdir(), "pikaoa-test-renderer-"));
mkdirSync(resolve(root, "deploy"), { recursive: true });
for (const file of ["api.Dockerfile", "worker.Dockerfile", "web.Dockerfile"]) writeFileSync(resolve(root, "deploy", file), "FROM scratch\n");
return root;
}
describe("PikaOA test PaC renderer", () => {
test("shares one PaC repository while isolating master and release consumers", () => {
const document = pacDocument();
const repositories = document.repositories as Array<Record<string, unknown>>;
const consumers = document.consumers as Array<Record<string, unknown>>;
const repository = repositories.find((item) => item.id === "pikaoa-test-nc01");
const testConsumer = consumers.find((item) => item.id === "pikaoa-test-nc01");
const productionConsumer = consumers.find((item) => item.id === "pikaoa-nc01");
const duplicateProductionRepository = repositories.find((item) => item.id === "pikaoa-nc01");
expect((repository?.params as Record<string, unknown>).source_branch).toBe("master");
expect((testConsumer?.sourceArtifact as Record<string, unknown>).pipelineRunPath).toBe(".tekton/pikaoa-test-nc01-pac.yaml");
expect((testConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-test-runtime");
expect((productionConsumer?.params as Record<string, unknown>).source_branch).toBe("release");
expect((productionConsumer?.params as Record<string, unknown>).source_snapshot_prefix).toBe("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
expect(productionConsumer?.repositoryRef).toBe("pikaoa-test-nc01");
expect(duplicateProductionRepository).toBeUndefined();
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).renderer).toBe("pikaoa-release-runtime");
expect((productionConsumer?.sourceArtifact as Record<string, unknown>).configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
});
test("renders release runtime from its isolated owning selector", () => {
const source = sourceFixture();
try {
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding("pikaoa-nc01"), source);
expect(rendered.configRef).toBe("config/pikaoa.yaml#releaseRuntime.targets.NC01");
const serialized = JSON.stringify(rendered.pipeline);
expect(serialized).toContain("pikaoa-nc01-pac");
expect(serialized).toContain("refs/unidesk/snapshots/gitea-actions/pikaoa-release-nc01");
expect(serialized).toContain("pac-gitea-pikaoa-nc01");
expect(serialized).not.toContain("pikaoa-test-nc01-pac");
} finally {
rmSync(source, { recursive: true, force: true });
}
});
test("renders parallel digest builds and a PreSync fresh database initializer", () => {
const source = sourceFixture();
try {
const rendered = renderPikaoaTestDesiredPipeline(rendererBinding(), source);
const spec = rendered.pipeline.spec as Record<string, unknown>;
const tasks = spec.tasks as Array<Record<string, unknown>>;
for (const task of tasks) {
const taskSpec = task.taskSpec as Record<string, unknown>;
expect(taskSpec.workspaces).toEqual([{ name: "workspace", mountPath: "/workspace" }]);
const steps = taskSpec.steps as Array<Record<string, unknown>>;
for (const step of steps) {
const mounts = (step.volumeMounts ?? []) as Array<Record<string, unknown>>;
expect(mounts.some((mount) => mount.name === "workspace")).toBe(false);
}
}
for (const name of ["build-api", "build-worker", "build-web"]) {
const task = tasks.find((item) => item.name === name);
expect(task?.runAfter).toEqual(["source"]);
expect(JSON.stringify(task)).toContain("buildctl-daemonless.sh build");
expect(JSON.stringify(task)).toContain("$(params.revision)");
expect(JSON.stringify(task)).not.toContain("source-token");
const buildScript = (((task?.taskSpec as Record<string, unknown>).steps as Array<Record<string, unknown>>)[0]?.script) as string;
expect(buildScript).toContain('"containerimage.digest"[[:space:]]*:[[:space:]]*"\\(sha256:[0-9a-f]\\{64\\}\\)"');
expect(buildScript).toContain(".*/\\1/p");
expect(buildScript).not.toContain("\u0001");
expect(buildScript).toContain("Docker-Content-Digest:");
expect(buildScript).toContain("build_status=$?");
expect(buildScript).toContain('"code":"buildctl-nonzero-artifact-verified"');
expect(buildScript).toContain('"artifactVerified":true');
}
const digest = "sha256:" + "a".repeat(64);
const metadata = JSON.stringify({ "containerimage.digest": digest }, null, 2);
const parsedDigest = /"containerimage\.digest"\s*:\s*"(sha256:[0-9a-f]{64})"/u.exec(metadata)?.[1];
expect(parsedDigest).toBe(digest);
const sourceTask = tasks.find((item) => item.name === "source");
expect(JSON.stringify(sourceTask)).toContain("source-token");
expect(JSON.stringify(sourceTask)).toContain("GIT_ASKPASS");
expect(JSON.stringify(sourceTask)).toContain("$(params.gitops-secret-name)");
for (const task of tasks) {
const taskSpec = task.taskSpec as Record<string, unknown> | undefined;
if (taskSpec === undefined) continue;
expect(taskSpec.workspaces).toEqual([{ name: "workspace", mountPath: "/workspace" }]);
const steps = taskSpec.steps as Array<Record<string, unknown>>;
for (const step of steps) {
expect((step.volumeMounts as Array<Record<string, unknown>> | undefined)?.some((mount) => mount.name === "workspace") ?? false).toBe(false);
}
}
const publish = tasks.find((item) => item.name === "gitops-publish");
expect(publish?.runAfter).toEqual(["build-api", "build-worker", "build-web"]);
const script = ((((publish?.taskSpec as Record<string, unknown>).steps as Array<Record<string, unknown>>)[0]?.script) as string);
for (const evidence of [
'"status":"published"',
'"registryVerified":true',
'"publishedCount":3',
'"requiredServiceCount":3',
'"gitopsCommit"',
'"service":"api"',
'"service":"worker"',
'"service":"web"',
]) expect(script).toContain(evidence);
const encoded = /printf '%s' '([A-Za-z0-9+/=]+)' \| base64 -d/u.exec(script)?.[1];
if (encoded === undefined) throw new Error("embedded GitOps manifest is required");
const manifest = Buffer.from(encoded, "base64").toString("utf8");
expect(manifest).toContain("argocd.argoproj.io/hook: PreSync");
expect(manifest).toContain("name: pikaoa-init");
expect(manifest).toContain("name: initializer");
expect(manifest).not.toContain("kind: Secret");
expect(manifest).toContain("secretName: pikaoa-test-runtime");
expect(manifest).toContain("image: __PIKAOA_API_IMAGE__");
expect(manifest).toContain("pikaoa.unidesk.io/source-commit: __PIKAOA_SOURCE_COMMIT__");
expect(script).toContain('s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g');
expect(script).toContain("GitOps manifest contains unresolved provenance or image placeholders");
expect(script).not.toContain("grep -Eq '$(params.revision)");
expect(script).toContain("grep -Eq '__PIKAOA_SOURCE_COMMIT__|__PIKAOA_(API|WORKER|WEB)_IMAGE__'");
expect(script).toContain("git ls-remote --exit-code --heads");
expect(script).toContain("git -C /workspace/gitops checkout --orphan");
} finally {
rmSync(source, { recursive: true, force: true });
}
});
});
@@ -0,0 +1,379 @@
import { existsSync } from "node:fs";
import { resolve } from "node:path";
import {
pikaoaTestRuntimeConfigRef,
pikaoaReleaseRuntimeConfigRef,
resolvePikaoaReleaseRuntimeTargetByConfigRef,
resolvePikaoaTestRuntimeTargetByConfigRef,
type TestTargetSpec,
} from "./pikaoa-test-target";
import { renderPikaoaTestRuntimeManifest, type PikaoaTestRenderContext } from "./pikaoa-test-runtime-manifest";
import { stableJsonSha256 } from "./stable-json";
export interface PikaoaTestRendererBinding {
readonly consumer: {
readonly id: string;
readonly node: string;
readonly lane: string;
readonly namespace: string;
readonly pipeline: string;
readonly pipelineRunPrefix: string;
readonly sourceArtifact: {
readonly configRef: string;
readonly mode: string;
readonly renderer: string;
};
};
readonly repository: {
readonly id: string;
readonly params: Readonly<Record<string, string>>;
};
}
export interface PikaoaTestRenderedPipeline {
readonly pipeline: Record<string, unknown>;
readonly configRef: string;
readonly effectiveConfigSha256: string;
readonly sourceWorktreeRemote: string;
}
export function renderPikaoaTestDesiredPipeline(binding: PikaoaTestRendererBinding, sourceWorktree: string): PikaoaTestRenderedPipeline {
const release = binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime";
const target = release
? resolvePikaoaReleaseRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef)
: resolvePikaoaTestRuntimeTargetByConfigRef(binding.consumer.sourceArtifact.configRef);
const configRef = release ? pikaoaReleaseRuntimeConfigRef(target.id) : pikaoaTestRuntimeConfigRef(target.id);
if ((!release && binding.consumer.sourceArtifact.renderer !== "pikaoa-test-runtime") || binding.consumer.sourceArtifact.mode !== "embedded-pipeline-spec") {
throw new Error(`${binding.consumer.sourceArtifact.renderer} requires embedded-pipeline-spec`);
}
if (binding.consumer.sourceArtifact.configRef !== configRef) {
throw new Error(`sourceArtifact.configRef must equal resolved owning selector ${configRef}`);
}
assertBinding(target, binding);
assertSourceRepositoryContract(target, sourceWorktree);
return {
pipeline: pipeline(target),
configRef,
effectiveConfigSha256: stableJsonSha256(target),
sourceWorktreeRemote: target.source.worktreeRemote,
};
}
export function pikaoaTestSourceWorktreeRemote(targetId: string): string {
return resolvePikaoaTestRuntimeTargetByConfigRef(pikaoaTestRuntimeConfigRef(targetId)).source.worktreeRemote;
}
export function pikaoaReleaseSourceWorktreeRemote(targetId: string): string {
return resolvePikaoaReleaseRuntimeTargetByConfigRef(pikaoaReleaseRuntimeConfigRef(targetId)).source.worktreeRemote;
}
function pipeline(target: TestTargetSpec): Record<string, unknown> {
const runtimeManifest = renderGitOpsManifest(target);
return {
apiVersion: "tekton.dev/v1",
kind: "Pipeline",
metadata: { name: target.ci.pipeline, namespace: target.ci.namespace },
spec: {
params: [
{ name: "revision", type: "string" },
{ name: "source-snapshot-prefix", type: "string", default: target.source.snapshotPrefix },
{ name: "git-read-url", type: "string", default: target.source.readUrl },
{ name: "gitops-read-url", type: "string", default: target.gitops.readUrl },
{ name: "gitops-write-url", type: "string", default: target.gitops.writeUrl },
{ name: "gitops-username", type: "string", default: target.gitops.credentialUsername },
{ name: "gitops-secret-name", type: "string", default: target.gitops.credentialSecretName },
],
workspaces: [{ name: "workspace" }],
tasks: [
sourceTask(target),
imageBuildTask(target, "api"),
imageBuildTask(target, "worker"),
imageBuildTask(target, "web"),
gitOpsTask(target, runtimeManifest),
],
},
};
}
function sourceTask(target: TestTargetSpec): Record<string, unknown> {
return {
name: "source",
taskSpec: {
params: [
{ name: "revision", type: "string" },
{ name: "source-snapshot-prefix", type: "string" },
{ name: "git-read-url", type: "string" },
{ name: "gitops-username", type: "string" },
{ name: "gitops-secret-name", type: "string" },
],
workspaces: [{ name: "workspace", mountPath: "/workspace" }],
steps: [{
name: "checkout",
image: target.ci.toolsImage,
imagePullPolicy: "IfNotPresent",
script: `#!/bin/sh
set -eu
revision="$(params.revision)"
case "$revision" in *[!0-9a-f]*|'') echo 'revision must be a full lowercase source commit' >&2; exit 2;; esac
test "\${#revision}" -eq 40
askpass=/tmp/pikaoa-source-git-askpass
cat >"$askpass" <<'ASKPASS'
#!/bin/sh
case "$1" in
*Username*) printf '%s' "$PIKAOA_GIT_USERNAME" ;;
*Password*) cat /var/run/pikaoa-source/token ;;
*) exit 1 ;;
esac
ASKPASS
chmod 700 "$askpass"
export PIKAOA_GIT_USERNAME="$(params.gitops-username)"
export GIT_ASKPASS="$askpass"
export GIT_TERMINAL_PROMPT=0
rm -rf /workspace/source
git init /workspace/source
git -C /workspace/source remote add origin "$(params.git-read-url)"
git -C /workspace/source fetch --depth=1 origin "$(params.source-snapshot-prefix)/$revision"
git -C /workspace/source checkout --detach FETCH_HEAD
test "$(git -C /workspace/source rev-parse HEAD)" = "$revision"
rm -f "$askpass"
printf '{"ok":true,"phase":"source","sourceCommit":"%s","valuesPrinted":false}\n' "$revision"
`,
securityContext: nonRootSecurity(),
volumeMounts: [
{ name: "source-token", mountPath: "/var/run/pikaoa-source", readOnly: true },
],
}],
volumes: [{ name: "source-token", secret: { secretName: "$(params.gitops-secret-name)", items: [{ key: target.gitops.credentialTokenKey, path: "token" }] } }],
},
params: [
{ name: "revision", value: "$(params.revision)" },
{ name: "source-snapshot-prefix", value: "$(params.source-snapshot-prefix)" },
{ name: "git-read-url", value: "$(params.git-read-url)" },
{ name: "gitops-username", value: "$(params.gitops-username)" },
{ name: "gitops-secret-name", value: "$(params.gitops-secret-name)" },
],
workspaces: [{ name: "workspace", workspace: "workspace" }],
};
}
function imageBuildTask(target: TestTargetSpec, component: "api" | "worker" | "web"): Record<string, unknown> {
const taskName = `build-${component}`;
const imageRepository = target.build.images[component];
const dockerfile = target.build.dockerfiles[component];
return {
name: taskName,
runAfter: ["source"],
taskSpec: {
params: [{ name: "revision", type: "string" }],
workspaces: [{ name: "workspace", mountPath: "/workspace" }],
results: [{ name: "digest", type: "string" }, { name: "image", type: "string" }],
steps: [{
name: "build",
image: target.ci.buildkitImage,
imagePullPolicy: "IfNotPresent",
workingDir: "/workspace/source",
env: [{ name: "BUILDKITD_FLAGS", value: "--oci-worker-no-process-sandbox --oci-worker-net=host --allow-insecure-entitlement network.host" }],
script: `#!/bin/sh
set -eu
revision="$(params.revision)"
tag=${shellSingleQuote(imageRepository)}:"$revision"
metadata=/tmp/${component}-metadata.json
set +e
buildctl-daemonless.sh build \\
--frontend dockerfile.v0 \\
--local context=. \\
--local dockerfile=. \\
--opt filename=${shellSingleQuote(dockerfile)} \\
--opt network=${shellSingleQuote(target.build.networkMode)} \\
--output "type=image,name=$tag,push=true" \\
--metadata-file "$metadata"
build_status=$?
set -e
metadata_compact=$(tr -d '\\n\\r' < "$metadata")
digest=$(printf '%s' "$metadata_compact" | sed -n 's/.*"containerimage.digest"[[:space:]]*:[[:space:]]*"\\(sha256:[0-9a-f]\\{64\\}\\)".*/\\1/p' | head -n1)
if ! printf '%s' "$digest" | grep -Eq '^sha256:[0-9a-f]{64}$'; then
registry_host=${shellSingleQuote(imageRepository.split("/")[0] ?? "")}
registry_path=${shellSingleQuote(imageRepository.split("/").slice(1).join("/"))}
headers=$(wget --server-response --spider --header='Accept: application/vnd.oci.image.manifest.v1+json, application/vnd.docker.distribution.manifest.v2+json' "http://$registry_host/v2/$registry_path/manifests/$revision" 2>&1 || true)
digest=$(printf '%s\\n' "$headers" | awk 'BEGIN{IGNORECASE=1} /^[[:space:]]*Docker-Content-Digest:/ {gsub(/\\r/, "", $2); print $2; exit}')
fi
case "$digest" in sha256:[0-9a-f][0-9a-f]*) test "\${#digest}" -eq 71 ;; *) echo 'build digest missing from metadata and registry manifest' >&2; exit 3;; esac
if [ "$build_status" -ne 0 ]; then
printf '{"level":"warning","code":"buildctl-nonzero-artifact-verified","component":"${component}","buildctlExitCode":%s,"artifactVerified":true,"valuesPrinted":false}\\n' "$build_status" >&2
fi
printf '%s' "$digest" > "$(results.digest.path)"
printf '%s@%s' ${shellSingleQuote(imageRepository)} "$digest" > "$(results.image.path)"
printf '{"ok":true,"phase":"${taskName}","sourceCommit":"%s","imageTag":"%s","digest":"%s","valuesPrinted":false}\n' "$revision" "$tag" "$digest"
`,
securityContext: { privileged: true, runAsUser: 1000, runAsGroup: 1000 },
}],
},
params: [{ name: "revision", value: "$(params.revision)" }],
workspaces: [{ name: "workspace", workspace: "workspace" }],
};
}
function gitOpsTask(target: TestTargetSpec, runtimeManifest: string): Record<string, unknown> {
const manifestB64 = Buffer.from(runtimeManifest, "utf8").toString("base64");
return {
name: "gitops-publish",
runAfter: ["build-api", "build-worker", "build-web"],
taskSpec: {
params: [
{ name: "revision", type: "string" },
{ name: "gitops-read-url", type: "string" },
{ name: "gitops-write-url", type: "string" },
{ name: "gitops-username", type: "string" },
{ name: "gitops-secret-name", type: "string" },
{ name: "api-image", type: "string" },
{ name: "worker-image", type: "string" },
{ name: "web-image", type: "string" },
],
workspaces: [{ name: "workspace", mountPath: "/workspace" }],
steps: [{
name: "publish",
image: target.ci.toolsImage,
imagePullPolicy: "IfNotPresent",
env: [{ name: "SOURCE_COMMIT", value: "$(params.revision)" }],
script: `#!/bin/sh
set -eu
askpass=/tmp/pikaoa-git-askpass
cat >"$askpass" <<'ASKPASS'
#!/bin/sh
case "$1" in
*Username*) printf '%s' "$PIKAOA_GIT_USERNAME" ;;
*Password*) cat /var/run/pikaoa-gitops/token ;;
*) exit 1 ;;
esac
ASKPASS
chmod 700 "$askpass"
export PIKAOA_GIT_USERNAME="$(params.gitops-username)"
export GIT_ASKPASS="$askpass"
export GIT_TERMINAL_PROMPT=0
case "$SOURCE_COMMIT" in *[!0-9a-f]*|'') echo 'source commit must be a full lowercase commit' >&2; exit 2;; esac
test "\${#SOURCE_COMMIT}" -eq 40
api_image="$(params.api-image)"
worker_image="$(params.worker-image)"
web_image="$(params.web-image)"
for image in "$api_image" "$worker_image" "$web_image"; do
case "$image" in *@sha256:[0-9a-f]*) ;; *) echo 'required digestRef missing' >&2; exit 4;; esac
done
rm -rf /workspace/gitops
mkdir -p /workspace/gitops
git -C /workspace/gitops init
git -C /workspace/gitops remote add origin "$(params.gitops-read-url)"
if git ls-remote --exit-code --heads "$(params.gitops-read-url)" ${shellSingleQuote(target.gitops.branch)} >/dev/null 2>&1; then
git -C /workspace/gitops fetch --depth=1 origin ${shellSingleQuote(target.gitops.branch)}
git -C /workspace/gitops checkout -B ${shellSingleQuote(target.gitops.branch)} FETCH_HEAD
else
git -C /workspace/gitops checkout --orphan ${shellSingleQuote(target.gitops.branch)}
git -C /workspace/gitops rm -rf . >/dev/null 2>&1 || true
fi
manifest=/workspace/gitops/${shellSingleQuote(target.gitops.manifestPath)}
mkdir -p "$(dirname "$manifest")"
printf '%s' ${shellSingleQuote(manifestB64)} | base64 -d > "$manifest"
sed -i \\
-e "s|__PIKAOA_SOURCE_COMMIT__|$SOURCE_COMMIT|g" \\
-e "s|__PIKAOA_API_IMAGE__|$(params.api-image)|g" \\
-e "s|__PIKAOA_WORKER_IMAGE__|$(params.worker-image)|g" \\
-e "s|__PIKAOA_WEB_IMAGE__|$(params.web-image)|g" \\
"$manifest"
if grep -Eq '__PIKAOA_SOURCE_COMMIT__|__PIKAOA_(API|WORKER|WEB)_IMAGE__' "$manifest"; then
echo 'GitOps manifest contains unresolved provenance or image placeholders' >&2
exit 6
fi
git -C /workspace/gitops config user.name ${shellSingleQuote(target.gitops.authorName)}
git -C /workspace/gitops config user.email ${shellSingleQuote(target.gitops.authorEmail)}
git -C /workspace/gitops add -- ${shellSingleQuote(target.gitops.manifestPath)}
if git -C /workspace/gitops diff --cached --quiet; then
changed=false
else
git -C /workspace/gitops commit -m "chore(gitops): deploy pikaoa $SOURCE_COMMIT"
git -C /workspace/gitops push "$(params.gitops-write-url)" HEAD:${shellSingleQuote(target.gitops.branch)}
changed=true
fi
gitops_commit=$(git -C /workspace/gitops rev-parse HEAD)
case "$gitops_commit" in [0-9a-f][0-9a-f][0-9a-f][0-9a-f]*) ;; *) echo 'gitops commit missing' >&2; exit 5;; esac
rm -f "$askpass"
printf '{"ok":true,"status":"published","phase":"gitops-publish","sourceCommitId":"%s","registryVerified":true,"publishedCount":3,"reusedCount":0,"requiredServiceCount":3,"services":[{"service":"api","digestRef":"%s","digest":"%s"},{"service":"worker","digestRef":"%s","digest":"%s"},{"service":"web","digestRef":"%s","digest":"%s"}],"gitopsCommit":"%s","changed":%s,"valuesPrinted":false}\n' \
"$SOURCE_COMMIT" "$api_image" "\${api_image##*@}" "$worker_image" "\${worker_image##*@}" "$web_image" "\${web_image##*@}" "$gitops_commit" "$changed"
`,
securityContext: nonRootSecurity(),
volumeMounts: [
{ name: "gitops-token", mountPath: "/var/run/pikaoa-gitops", readOnly: true },
],
}],
volumes: [{ name: "gitops-token", secret: { secretName: "$(params.gitops-secret-name)", items: [{ key: target.gitops.credentialTokenKey, path: "token" }] } }],
},
params: [
{ name: "revision", value: "$(params.revision)" },
{ name: "gitops-read-url", value: "$(params.gitops-read-url)" },
{ name: "gitops-write-url", value: "$(params.gitops-write-url)" },
{ name: "gitops-username", value: "$(params.gitops-username)" },
{ name: "gitops-secret-name", value: "$(params.gitops-secret-name)" },
{ name: "api-image", value: "$(tasks.build-api.results.image)" },
{ name: "worker-image", value: "$(tasks.build-worker.results.image)" },
{ name: "web-image", value: "$(tasks.build-web.results.image)" },
],
workspaces: [{ name: "workspace", workspace: "workspace" }],
};
}
function renderGitOpsManifest(target: TestTargetSpec): string {
const context: PikaoaTestRenderContext = {
instanceId: "default",
commit: "__PIKAOA_SOURCE_COMMIT__",
step: "all",
namespace: target.namespace,
expiresAt: "gitops-managed",
apiImage: "__PIKAOA_API_IMAGE__",
workerImage: "__PIKAOA_WORKER_IMAGE__",
webImage: "__PIKAOA_WEB_IMAGE__",
initializerImage: "__PIKAOA_API_IMAGE__",
};
const manifest = renderPikaoaTestRuntimeManifest(target, context, { secretMode: "existing", initializerHook: "argo-presync" });
return manifest.map((object) => Bun.YAML.stringify(object, null, 2).trim()).join("\n---\n") + "\n";
}
function assertBinding(target: TestTargetSpec, binding: PikaoaTestRendererBinding): void {
const expected: Readonly<Record<string, string>> = {
id: target.source.branch === "release" ? "pikaoa-nc01" : "pikaoa-test-nc01",
node: target.node,
lane: target.ci.lane,
namespace: target.ci.namespace,
pipeline: target.ci.pipeline,
pipelineRunPrefix: target.ci.pipelineRunPrefix,
sourceBranch: target.source.branch,
gitopsBranch: target.gitops.branch,
serviceAccount: target.ci.serviceAccountName,
};
const actual: Readonly<Record<string, string | undefined>> = {
id: binding.consumer.id,
node: binding.consumer.node,
lane: binding.consumer.lane,
namespace: binding.consumer.namespace,
pipeline: binding.consumer.pipeline,
pipelineRunPrefix: binding.consumer.pipelineRunPrefix,
sourceBranch: binding.repository.params.source_branch,
gitopsBranch: binding.repository.params.gitops_branch,
serviceAccount: binding.repository.params.service_account,
};
for (const [key, value] of Object.entries(expected)) {
if (actual[key] !== value) throw new Error(`PikaOA runtime binding ${key} must equal owning YAML value ${value}`);
}
}
function assertSourceRepositoryContract(target: TestTargetSpec, sourceWorktree: string): void {
for (const path of Object.values(target.build.dockerfiles)) {
if (!existsSync(resolve(sourceWorktree, path))) throw new Error(`PikaOA source worktree is missing ${path}`);
}
}
function nonRootSecurity(): Record<string, unknown> {
return { allowPrivilegeEscalation: false, runAsNonRoot: true, runAsUser: 1000, runAsGroup: 1000 };
}
function shellSingleQuote(value: string): string {
return `'${value.replaceAll("'", `'"'"'`)}'`;
}
+254
View File
@@ -0,0 +1,254 @@
import type { TestTargetSpec } from "./pikaoa-test-target";
export type PikaoaTestTargetStep = "all" | "foundation" | "init" | "api" | "worker" | "web";
export interface PikaoaTestRenderContext {
instanceId: string;
commit: string;
step: PikaoaTestTargetStep;
namespace: string;
expiresAt: string;
apiImage: string;
workerImage: string;
webImage: string;
initializerImage: string;
}
export interface PikaoaTestSecretMaterial {
databaseURL: string;
sessionSecret: string;
administratorPassword: string;
employeePassword: string;
}
export interface PikaoaTestManifestOptions {
secretMode: "materialized" | "existing";
secrets?: PikaoaTestSecretMaterial;
initializerHook?: "argo-presync";
}
const TARGET_LABEL = "pikaoa.unidesk.io/target";
const INSTANCE_LABEL = "pikaoa.unidesk.io/instance";
export function renderPikaoaTestRuntimeManifest(
target: TestTargetSpec,
context: PikaoaTestRenderContext,
options: PikaoaTestManifestOptions,
): Record<string, unknown>[] {
if (options.secretMode === "materialized" && options.secrets === undefined) {
throw new Error("materialized PikaOA test manifest requires secret material");
}
const labels = ownershipLabels(target, context);
const apiSelector = componentSelector("api", context.instanceId);
const workerSelector = componentSelector("worker", context.instanceId);
const webSelector = componentSelector("web", context.instanceId);
const initializerSelector = componentSelector("initializer", context.instanceId);
const prometheusPodLabels = target.observability.prometheusScrape ? {
[target.observability.prometheusSelector.key]: target.observability.prometheusSelector.value,
} : {};
const commonPodAnnotations = (path: string, port: number): Record<string, string> => target.observability.prometheusScrape ? {
"prometheus.io/scrape": "true",
"prometheus.io/path": path,
"prometheus.io/port": String(port),
} : {};
const initializerAnnotations = options.initializerHook === "argo-presync" ? {
"argocd.argoproj.io/hook": "PreSync",
"argocd.argoproj.io/hook-delete-policy": "BeforeHookCreation,HookSucceeded",
} : {};
return [
{
apiVersion: "v1", kind: "Namespace", metadata: { name: context.namespace, labels, annotations: namespaceAnnotations(target, context) },
},
...(options.secretMode === "materialized" ? [{
apiVersion: "v1", kind: "Secret", metadata: { name: target.runtime.secretName, namespace: context.namespace, labels }, type: "Opaque",
stringData: runtimeSecretData(target, context, options.secrets!),
}] : []),
{
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
spec: {
accessModes: target.runtime.attachment.accessModes,
resources: { requests: { storage: target.runtime.attachment.storageRequest } },
...(target.runtime.attachment.storageClassName === null ? {} : { storageClassName: target.runtime.attachment.storageClassName }),
},
},
{
apiVersion: "v1", kind: "Service", metadata: { name: target.runtime.apiServiceName, namespace: context.namespace, labels: { ...labels, ...apiSelector } },
spec: { selector: apiSelector, ports: [{ name: "http", port: target.runtime.apiPort, targetPort: target.runtime.apiPort }] },
},
{
apiVersion: "v1", kind: "Service", metadata: { name: target.exposure.serviceName, namespace: context.namespace, labels: { ...labels, ...webSelector } },
spec: {
type: target.exposure.serviceType,
selector: webSelector,
ports: [{ name: "http", port: target.runtime.webPort, targetPort: target.runtime.webPort, nodePort: target.exposure.port }],
},
},
{
apiVersion: "batch/v1", kind: "Job",
metadata: { name: target.initializer.jobName, namespace: context.namespace, labels: { ...labels, ...initializerSelector }, annotations: initializerAnnotations },
spec: {
backoffLimit: target.initializer.backoffLimit,
template: {
metadata: { labels: { ...labels, ...initializerSelector } },
spec: {
restartPolicy: "Never",
containers: [{
name: "initializer", image: context.initializerImage, imagePullPolicy: target.source.pullPolicy,
command: target.initializer.command,
resources: target.runtime.resources.initializer,
env: [{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" }],
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
}],
volumes: [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
},
},
},
},
deployment(target, context, "api", context.apiImage, apiSelector, target.runtime.apiPort, prometheusPodLabels, commonPodAnnotations(target.observability.apiMetricsPath, target.runtime.apiPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
{ name: "PIKAOA_SESSION_SECRET", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.sessionSecret.targetKey } } },
{ name: "PIKAOA_BOOTSTRAP_ADMIN_USERNAME", value: target.runtime.administrator.username },
{ name: "PIKAOA_BOOTSTRAP_ADMIN_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.administrator.password.targetKey } } },
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_USERNAME", value: target.runtime.employee.username },
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.employee.password.targetKey } } },
], target.probes.apiLivenessPath, target.probes.apiReadinessPath),
deployment(target, context, "worker", context.workerImage, workerSelector, target.runtime.workerMetricsPort, prometheusPodLabels, commonPodAnnotations(target.observability.workerMetricsPath, target.runtime.workerMetricsPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
], target.probes.workerLivenessPath, target.probes.workerReadinessPath),
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, {}, [
{ name: target.runtime.webApiUpstreamEnv, value: `http://${target.runtime.apiServiceName}.${context.namespace}.svc.cluster.local:${target.runtime.apiPort}` },
], target.probes.webLivenessPath, target.probes.webReadinessPath),
];
}
export function pikaoaManifestForStep(manifest: Record<string, unknown>[], step: PikaoaTestTargetStep): Record<string, unknown>[] {
if (step === "all") return manifest;
return manifest.filter((object) => {
if (object.kind === "Namespace" || object.kind === "PersistentVolumeClaim") return true;
if (step === "foundation") return false;
if (object.kind === "Secret") return true;
if (step === "init") return object.kind === "Job";
const metadata = optionalRecord(object.metadata);
const labels = optionalRecord(metadata?.labels);
return labels?.["app.kubernetes.io/component"] === step;
});
}
export function ownershipLabels(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
const release = target.source.branch === "release";
return {
"app.kubernetes.io/name": "pikaoa",
"app.kubernetes.io/part-of": release ? "pikaoa-release-runtime" : "pikaoa-test-runtime",
"app.kubernetes.io/managed-by": target.fieldManager,
[release ? "pikaoa.unidesk.io/release-runtime" : "pikaoa.unidesk.io/test-runtime"]: "true",
[TARGET_LABEL]: target.id,
[INSTANCE_LABEL]: context.instanceId,
};
}
export function componentSelector(component: string, instanceId: string): Record<string, string> {
return { "app.kubernetes.io/name": "pikaoa", "app.kubernetes.io/component": component, [INSTANCE_LABEL]: instanceId };
}
export function namespaceAnnotations(target: TestTargetSpec, context: PikaoaTestRenderContext): Record<string, string> {
return {
"pikaoa.unidesk.io/source-repository": target.source.repository,
"pikaoa.unidesk.io/source-commit": context.commit,
"pikaoa.unidesk.io/expires-at": context.expiresAt,
"pikaoa.unidesk.io/ttl-seconds": String(target.ttlSeconds),
};
}
export function renderDatabaseURL(target: TestTargetSpec, sourceURL: string): string {
const databaseURL = new URL(sourceURL);
databaseURL.searchParams.set("search_path", target.database.schema);
return databaseURL.toString();
}
export function placeholderPikaoaSecrets(): PikaoaTestSecretMaterial {
return {
databaseURL: "postgresql://redacted@db.invalid/pikaoa_test?sslmode=require",
sessionSecret: "<redacted>",
administratorPassword: "<redacted>",
employeePassword: "<redacted>",
};
}
function deployment(
target: TestTargetSpec,
context: PikaoaTestRenderContext,
component: "api" | "worker" | "web",
image: string,
selector: Record<string, string>,
port: number,
podLabels: Record<string, string>,
annotations: Record<string, string>,
env: Array<Record<string, unknown>>,
livenessPath: string,
readinessPath: string,
): Record<string, unknown> {
const labels = ownershipLabels(target, context);
return {
apiVersion: "apps/v1", kind: "Deployment", metadata: { name: `pikaoa-${component}`, namespace: context.namespace, labels: { ...labels, ...selector } },
spec: {
replicas: target.runtime.replicas[component], selector: { matchLabels: selector },
template: {
metadata: { labels: { ...labels, ...selector, ...podLabels }, annotations },
spec: {
...(component === "api" ? { securityContext: { fsGroup: target.runtime.attachment.fsGroup } } : {}),
containers: [{
name: component, image, imagePullPolicy: target.source.pullPolicy, env, resources: target.runtime.resources[component],
ports: [{ name: component === "worker" ? "metrics" : "http", containerPort: port }],
volumeMounts: [
...(component === "web" ? [] : [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }]),
...(component === "api" ? [{ name: target.runtime.attachment.claimName, mountPath: target.runtime.attachment.storageRoot }] : []),
],
livenessProbe: { httpGet: { path: livenessPath, port }, periodSeconds: 5, failureThreshold: 12 },
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
}],
volumes: [
...(component === "web" ? [] : [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
...(component === "api" ? [{ name: target.runtime.attachment.claimName, persistentVolumeClaim: { claimName: target.runtime.attachment.claimName } }] : []),
],
},
},
},
};
}
function runtimeSecretData(target: TestTargetSpec, context: PikaoaTestRenderContext, secrets: PikaoaTestSecretMaterial): Record<string, string> {
const databaseURL = renderDatabaseURL(target, secrets.databaseURL);
return {
[target.database.connection.targetKey]: databaseURL,
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
[target.runtime.administrator.password.targetKey]: secrets.administratorPassword,
[target.runtime.employee.password.targetKey]: secrets.employeePassword,
"pikaoa.yaml": [
"api:",
` listen: 0.0.0.0:${target.runtime.apiPort}`,
"worker:",
` interval: ${target.runtime.workerInterval}`,
` metricsListen: 0.0.0.0:${target.runtime.workerMetricsPort}`,
" outbox:",
` batchSize: ${target.runtime.outboxBatchSize}`,
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
` retryDelay: ${target.runtime.outboxRetryDelay}`,
"database:",
` url: ${JSON.stringify(databaseURL)}`,
"identity:",
` sessionTTL: ${target.runtime.sessionTTL}`,
"attachment:",
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
` maxBytes: ${target.runtime.attachment.maxBytes}`,
"observability:",
` serviceName: ${target.source.branch === "release" ? "pikaoa" : `pikaoa-test-${context.instanceId}`}`,
` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`,
`shutdownTimeout: ${target.runtime.shutdownTimeout}`,
"",
].join("\n"),
};
}
function optionalRecord(value: unknown): Record<string, unknown> | null {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : null;
}
+27 -27
View File
@@ -74,11 +74,11 @@ if printf '%s' "$args" | grep -q ' rollout status '; then
[ ! -f "$root/slow" ] || sleep 0.4
exit 0
fi
if printf '%s' "$args" | grep -q ' get job/pikaoa-migrate '; then
if [ -f "$root/fail-migration-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi
if [ -f "$root/failure-target-migration" ]; then
if printf '%s' "$args" | grep -q ' get job/pikaoa-init '; then
if [ -f "$root/fail-initializer-query" ]; then printf '%s\n' 'job query failed' >&2; exit 1; fi
if [ -f "$root/failure-target-initializer" ]; then
printf '%s\n' '{"status":{"conditions":[{"type":"FailureTarget","status":"True","reason":"BackoffLimitExceeded"}]}}'
elif [ -f "$root/fail-migration" ]; then
elif [ -f "$root/fail-initializer" ]; then
printf '%s\n' '{"status":{"conditions":[{"type":"Failed","status":"True","reason":"BackoffLimitExceeded"}]}}'
else
printf '%s\n' '{"status":{"conditions":[{"type":"Complete","status":"True"}]}}'
@@ -273,33 +273,33 @@ exit 64
rmSync(join(fakeRoot, "slow"));
const workerMissingSucceeded = await waitFor("runtime", "start-succeeded", "api");
writeFileSync(join(fakeRoot, "fail-migration"), "1\n");
await projection("start", "runtime", true, "migration");
const failed = await waitFor("runtime", "start-failed", "migration");
writeFileSync(join(fakeRoot, "fail-initializer"), "1\n");
await projection("start", "runtime", true, "init");
const failed = await waitFor("runtime", "start-failed", "init");
const failedTask = ((failed.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(failedTask.code, "migration-job-failed");
assert.equal(failedTask.code, "initializer-job-failed");
assert.equal(failedTask.exitCode, 46);
rmSync(join(fakeRoot, "fail-migration"));
rmSync(join(fakeRoot, "fail-initializer"));
writeFileSync(join(fakeRoot, "failure-target-migration"), "1\n");
await projectionWithCommit("start", "runtime", concurrentCommit, "migration");
const failureTarget = await waitFor("runtime", "start-failed", "migration");
writeFileSync(join(fakeRoot, "failure-target-initializer"), "1\n");
await projectionWithCommit("start", "runtime", concurrentCommit, "init");
const failureTarget = await waitFor("runtime", "start-failed", "init");
const failureTargetTask = ((failureTarget.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(failureTargetTask.code, "migration-job-failure-target");
assert.equal(failureTargetTask.code, "initializer-job-failure-target");
assert.equal(failureTargetTask.exitCode, 46);
rmSync(join(fakeRoot, "failure-target-migration"));
rmSync(join(fakeRoot, "failure-target-initializer"));
writeFileSync(join(fakeRoot, "fail-migration-query"), "1\n");
await projectionWithCommit("start", "runtime", "abcdef0123456789", "migration");
const migrationQueryFailed = await waitFor("runtime", "start-failed", "migration");
const migrationQueryFailedTask = ((migrationQueryFailed.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(migrationQueryFailedTask.code, "migration-job-query-failed");
assert.equal(migrationQueryFailedTask.exitCode, 46);
rmSync(join(fakeRoot, "fail-migration-query"));
writeFileSync(join(fakeRoot, "fail-initializer-query"), "1\n");
await projectionWithCommit("start", "runtime", "abcdef0123456789", "init");
const initializerQueryFailed = await waitFor("runtime", "start-failed", "init");
const initializerQueryFailedTask = ((initializerQueryFailed.status as Record<string, unknown>).task as Record<string, unknown>);
assert.equal(initializerQueryFailedTask.code, "initializer-job-query-failed");
assert.equal(initializerQueryFailedTask.exitCode, 46);
rmSync(join(fakeRoot, "fail-initializer-query"));
await projectionWithCommit("start", "runtime", "1234567890abcdef", "migration");
const migrationSucceeded = await waitFor("runtime", "start-succeeded", "migration");
assert.equal((((migrationSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).exitCode), 0);
await projectionWithCommit("start", "runtime", "1234567890abcdef", "init");
const initializerSucceeded = await waitFor("runtime", "start-succeeded", "init");
assert.equal((((initializerSucceeded.status as Record<string, unknown>).task as Record<string, unknown>).exitCode), 0);
writeFileSync(join(fakeRoot, "slow"), "1\n");
await projection("start", "runtime", true, "web");
@@ -344,8 +344,8 @@ exit 64
workerMissingSucceeded,
failed,
failureTarget,
migrationQueryFailed,
migrationSucceeded,
initializerQueryFailed,
initializerSucceeded,
stopped,
foundationSubmitted,
foundationSucceeded,
@@ -397,7 +397,7 @@ test("validation-only fixture never invokes remote capture", async () => {
secretName: "pikaoa-test-runtime",
key: "pikaoa.yaml",
mountPath: "/etc/pikaoa/pikaoa.yaml",
consumers: ["migration", "api", "worker"],
consumers: ["initializer", "api", "worker"],
valuesPrinted: false,
});
assert.equal(planPayload.valuesPrinted, false);
+199 -241
View File
@@ -10,9 +10,18 @@ import type { RenderedCliResult } from "./output";
import { CliInputError } from "./output";
import { capture, compactCapture, parseJsonOutput, readYamlRecord, sha256Fingerprint, shQuote } from "./platform-infra-ops-library";
import { resolveConfigRef } from "./ops/config-refs";
import {
pikaoaManifestForStep,
placeholderPikaoaSecrets,
renderDatabaseURL,
renderPikaoaTestRuntimeManifest,
type PikaoaTestRenderContext,
type PikaoaTestSecretMaterial,
type PikaoaTestTargetStep,
} from "./pikaoa-test-runtime-manifest";
type TestTargetAction = "plan" | "status" | "start" | "stop";
type TestTargetStep = "all" | "foundation" | "migration" | "api" | "worker" | "web";
type TestTargetStep = PikaoaTestTargetStep;
interface TestTargetOptions {
action: TestTargetAction;
@@ -41,7 +50,7 @@ interface PrometheusSelectorSpec {
valuesPrinted: false;
}
interface TestTargetSpec {
export interface TestTargetSpec {
id: string;
enabled: boolean;
validationOnly: boolean;
@@ -64,12 +73,43 @@ interface TestTargetSpec {
mode: string;
repository: string;
commitPolicy: string;
branch: string;
worktreeRemote: string;
readUrl: string;
snapshotPrefix: string;
apiImage: string;
workerImage: string;
webImage: string;
migrationImage: string;
initializerImage: string;
pullPolicy: string;
};
ci: {
lane: string;
namespace: string;
pipeline: string;
pipelineRunPrefix: string;
serviceAccountName: string;
workspaceSize: string;
pipelineTimeout: string;
toolsImage: string;
buildkitImage: string;
};
build: {
dockerfiles: { api: string; worker: string; web: string };
images: { api: string; worker: string; web: string };
networkMode: string;
};
gitops: {
readUrl: string;
writeUrl: string;
branch: string;
manifestPath: string;
credentialSecretName: string;
credentialTokenKey: string;
credentialUsername: string;
authorName: string;
authorEmail: string;
};
database: {
configRef: string;
name: string;
@@ -78,6 +118,8 @@ interface TestTargetSpec {
connection: SecretSourceSpec;
};
runtime: {
replicas: { api: number; worker: number; web: number };
resources: Record<"api" | "worker" | "web" | "initializer", Record<string, unknown>>;
secretName: string;
apiServiceName: string;
apiPort: number;
@@ -103,7 +145,8 @@ interface TestTargetSpec {
employee: { username: string; password: SecretSourceSpec };
sessionSecret: SecretSourceSpec;
};
migration: {
initializer: {
mode: "fresh-database-only";
jobName: string;
backoffLimit: number;
command: string[];
@@ -155,24 +198,8 @@ interface Blocker {
message: string;
}
interface RenderContext {
instanceId: string;
commit: string;
step: TestTargetStep;
namespace: string;
expiresAt: string;
apiImage: string;
workerImage: string;
webImage: string;
migrationImage: string;
}
interface SecretMaterial {
databaseURL: string;
sessionSecret: string;
administratorPassword: string;
employeePassword: string;
}
type RenderContext = PikaoaTestRenderContext;
type SecretMaterial = PikaoaTestSecretMaterial;
type RemoteCapture = typeof capture;
@@ -194,14 +221,14 @@ export function pikaoaTestTargetHelp(): Record<string, unknown> {
usage: [
"bun scripts/cli.ts pikaoa test-target plan [--config path] [--target id] [--instance id] [--commit sha] [--output json]",
"bun scripts/cli.ts pikaoa test-target status [--config path] [--target id] [--instance id] [--output json]",
"bun scripts/cli.ts pikaoa test-target start --target id --instance id [--commit sha] [--step foundation|migration|api|worker|web|all] --confirm",
"bun scripts/cli.ts pikaoa test-target start --target id --instance id [--commit sha] [--step foundation|init|api|worker|web|all] --confirm",
"bun scripts/cli.ts pikaoa test-target stop --target id --instance id --confirm",
],
source: "config/pikaoa.yaml#testRuntime",
guarantees: [
"未声明测试 target 时 plan/status 返回 configured=falsestart/stop 在远端调用前失败。",
"validationOnly target 只用于本地渲染,不连接 route。",
"start 可按 foundation、migration、api、worker、web 或 all 单步提交并立即返回;foundation 只创建固定 Namespace/PVC,不要求业务 Secret 已存在。",
"start 可按 foundation、init、api、worker、web 或 all 单步提交并立即返回;foundation 只创建固定 Namespace/PVC,不要求业务 Secret 已存在。",
"正式 delivery namespace、PaC、GitOps、公网暴露和生产数据库不属于该入口的写入范围。",
"Secret 只披露 sourceRef、targetKey、presence 和 fingerprint,不输出值。",
],
@@ -240,7 +267,7 @@ function parseOptions(args: string[]): TestTargetOptions {
else if (arg === "--target") targetId = simpleId(value, arg);
else if (arg === "--instance") instanceId = kubernetesName(value, arg, 30);
else if (arg === "--commit") commit = sourceCommit(value);
else if (arg === "--step") step = enumString(value, arg, ["all", "foundation", "migration", "api", "worker", "web"]);
else if (arg === "--step") step = enumString(value, arg, ["all", "foundation", "init", "api", "worker", "web"]);
else {
if (value !== "json" && value !== "text") throw inputError(`${arg} 只支持 text 或 json`, "invalid-output", value, ["text", "json"]);
output = value;
@@ -293,13 +320,22 @@ function readSelection(options: TestTargetOptions): Selection {
};
}
function parseTarget(id: string, root: Record<string, unknown>, configLabel: string): TestTargetSpec {
const path = `${configLabel}.testRuntime.targets.${id}`;
function parseTarget(id: string, root: Record<string, unknown>, configLabel: string, runtimeSection = "testRuntime"): TestTargetSpec {
const path = `${configLabel}.${runtimeSection}.targets.${id}`;
simpleId(id, "target id");
const source = record(root.source, `${path}.source`);
const images = record(source.images, `${path}.source.images`);
const ci = record(root.ci, `${path}.ci`);
const build = record(root.build, `${path}.build`);
const dockerfiles = record(build.dockerfiles, `${path}.build.dockerfiles`);
const buildImages = record(build.images, `${path}.build.images`);
const gitops = record(root.gitops, `${path}.gitops`);
const gitopsCredential = record(gitops.credential, `${path}.gitops.credential`);
const gitopsAuthor = record(gitops.author, `${path}.gitops.author`);
const database = record(root.database, `${path}.database`);
const runtime = record(root.runtime, `${path}.runtime`);
const replicas = record(runtime.replicas, `${path}.runtime.replicas`);
const resources = record(runtime.resources, `${path}.runtime.resources`);
const attachment = record(runtime.attachment, `${path}.runtime.attachment`);
const outbox = record(runtime.outbox, `${path}.runtime.outbox`);
const administrator = record(runtime.administrator, `${path}.runtime.administrator`);
@@ -313,7 +349,7 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
const workerProbes = record(probes.worker, `${path}.probes.worker`);
const webProbes = record(probes.web, `${path}.probes.web`);
const cleanup = record(root.cleanup, `${path}.cleanup`);
const migration = record(root.migration, `${path}.migration`);
const initializer = record(root.initializer, `${path}.initializer`);
const taskState = record(root.taskState, `${path}.taskState`);
if (exporterFailure.blocking !== false) throw new Error(`${path}.observability.exporterFailure.blocking 必须为 false`);
if (exporterFailure.warning !== true) throw new Error(`${path}.observability.exporterFailure.warning 必须为 true`);
@@ -341,12 +377,51 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
mode: exactString(source.mode, `${path}.source.mode`, "prebuilt-images"),
repository: nonEmpty(source.repository, `${path}.source.repository`),
commitPolicy: exactString(source.commitPolicy, `${path}.source.commitPolicy`, "cli-required"),
branch: nonEmpty(source.branch, `${path}.source.branch`),
worktreeRemote: nonEmpty(source.worktreeRemote, `${path}.source.worktreeRemote`),
readUrl: nonEmpty(source.readUrl, `${path}.source.readUrl`),
snapshotPrefix: nonEmpty(source.snapshotPrefix, `${path}.source.snapshotPrefix`),
apiImage: imageReference(images.api, `${path}.source.images.api`),
workerImage: imageReference(images.worker, `${path}.source.images.worker`),
webImage: imageReference(images.web, `${path}.source.images.web`),
migrationImage: imageReference(images.migration, `${path}.source.images.migration`),
initializerImage: imageReference(images.initializer, `${path}.source.images.initializer`),
pullPolicy: enumString(source.pullPolicy, `${path}.source.pullPolicy`, ["Always", "IfNotPresent", "Never"]),
},
ci: {
lane: simpleId(nonEmpty(ci.lane, `${path}.ci.lane`), `${path}.ci.lane`),
namespace: kubernetesName(nonEmpty(ci.namespace, `${path}.ci.namespace`), `${path}.ci.namespace`, 63),
pipeline: kubernetesName(nonEmpty(ci.pipeline, `${path}.ci.pipeline`), `${path}.ci.pipeline`, 63),
pipelineRunPrefix: kubernetesName(nonEmpty(ci.pipelineRunPrefix, `${path}.ci.pipelineRunPrefix`), `${path}.ci.pipelineRunPrefix`, 63),
serviceAccountName: kubernetesName(nonEmpty(ci.serviceAccountName, `${path}.ci.serviceAccountName`), `${path}.ci.serviceAccountName`, 63),
workspaceSize: nonEmpty(ci.workspaceSize, `${path}.ci.workspaceSize`),
pipelineTimeout: nonEmpty(ci.pipelineTimeout, `${path}.ci.pipelineTimeout`),
toolsImage: imageReference(ci.toolsImage, `${path}.ci.toolsImage`),
buildkitImage: imageReference(ci.buildkitImage, `${path}.ci.buildkitImage`),
},
build: {
dockerfiles: {
api: relativeSourcePath(dockerfiles.api, `${path}.build.dockerfiles.api`),
worker: relativeSourcePath(dockerfiles.worker, `${path}.build.dockerfiles.worker`),
web: relativeSourcePath(dockerfiles.web, `${path}.build.dockerfiles.web`),
},
images: {
api: imageRepository(buildImages.api, `${path}.build.images.api`),
worker: imageRepository(buildImages.worker, `${path}.build.images.worker`),
web: imageRepository(buildImages.web, `${path}.build.images.web`),
},
networkMode: exactString(build.networkMode, `${path}.build.networkMode`, "host"),
},
gitops: {
readUrl: nonEmpty(gitops.readUrl, `${path}.gitops.readUrl`),
writeUrl: nonEmpty(gitops.writeUrl, `${path}.gitops.writeUrl`),
branch: nonEmpty(gitops.branch, `${path}.gitops.branch`),
manifestPath: relativeSourcePath(gitops.manifestPath, `${path}.gitops.manifestPath`),
credentialSecretName: kubernetesName(nonEmpty(gitopsCredential.secretName, `${path}.gitops.credential.secretName`), `${path}.gitops.credential.secretName`, 63),
credentialTokenKey: nonEmpty(gitopsCredential.tokenKey, `${path}.gitops.credential.tokenKey`),
credentialUsername: nonEmpty(gitopsCredential.username, `${path}.gitops.credential.username`),
authorName: nonEmpty(gitopsAuthor.name, `${path}.gitops.author.name`),
authorEmail: nonEmpty(gitopsAuthor.email, `${path}.gitops.author.email`),
},
database: {
configRef: nonEmpty(database.configRef, `${path}.database.configRef`),
name: postgresIdentifier(database.name, `${path}.database.name`),
@@ -355,6 +430,17 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
connection: secretSource(database.connection, "database.connection", `${path}.database.connection`, true),
},
runtime: {
replicas: {
api: positiveInteger(replicas.api, `${path}.runtime.replicas.api`, 20),
worker: positiveInteger(replicas.worker, `${path}.runtime.replicas.worker`, 20),
web: positiveInteger(replicas.web, `${path}.runtime.replicas.web`, 20),
},
resources: {
api: record(resources.api, `${path}.runtime.resources.api`),
worker: record(resources.worker, `${path}.runtime.resources.worker`),
web: record(resources.web, `${path}.runtime.resources.web`),
initializer: record(resources.initializer, `${path}.runtime.resources.initializer`),
},
secretName: kubernetesName(nonEmpty(runtime.secretName, `${path}.runtime.secretName`), `${path}.runtime.secretName`, 63),
apiServiceName: kubernetesName(nonEmpty(runtime.apiServiceName, `${path}.runtime.apiServiceName`), `${path}.runtime.apiServiceName`, 63),
apiPort: positiveInteger(runtime.apiPort, `${path}.runtime.apiPort`, 65_535),
@@ -386,10 +472,11 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
},
sessionSecret: secretSource(runtime.sessionSecret, "runtime.sessionSecret", `${path}.runtime.sessionSecret`),
},
migration: {
jobName: kubernetesName(nonEmpty(migration.jobName, `${path}.migration.jobName`), `${path}.migration.jobName`, 63),
backoffLimit: nonNegativeInteger(migration.backoffLimit, `${path}.migration.backoffLimit`),
command: nonEmptyStringList(migration.command, `${path}.migration.command`),
initializer: {
mode: exactString(initializer.mode, `${path}.initializer.mode`, "fresh-database-only") as "fresh-database-only",
jobName: kubernetesName(nonEmpty(initializer.jobName, `${path}.initializer.jobName`), `${path}.initializer.jobName`, 63),
backoffLimit: nonNegativeInteger(initializer.backoffLimit, `${path}.initializer.backoffLimit`),
command: nonEmptyStringList(initializer.command, `${path}.initializer.command`),
},
exposure: {
serviceName: kubernetesName(nonEmpty(exposure.serviceName, `${path}.exposure.serviceName`), `${path}.exposure.serviceName`, 63),
@@ -422,6 +509,34 @@ function parseTarget(id: string, root: Record<string, unknown>, configLabel: str
return target;
}
export function pikaoaTestRuntimeConfigRef(targetId: string): string {
simpleId(targetId, "PikaOA test runtime target id");
return `config/pikaoa.yaml#testRuntime.targets.${targetId}`;
}
export function resolvePikaoaTestRuntimeTargetByConfigRef(configRef: string): TestTargetSpec {
const resolved = resolveConfigRef(configRef, "PikaOA test runtime configRef");
const match = /^testRuntime\.targets\.([A-Za-z0-9._-]+)$/u.exec(resolved.fragment);
if (resolved.file !== "config/pikaoa.yaml" || match === null) {
throw new Error("PikaOA test runtime configRef must select config/pikaoa.yaml#testRuntime.targets.<id>");
}
return parseTarget(match[1]!, record(resolved.value, configRef), resolved.file, "testRuntime");
}
export function pikaoaReleaseRuntimeConfigRef(targetId: string): string {
simpleId(targetId, "PikaOA release runtime target id");
return `config/pikaoa.yaml#releaseRuntime.targets.${targetId}`;
}
export function resolvePikaoaReleaseRuntimeTargetByConfigRef(configRef: string): TestTargetSpec {
const resolved = resolveConfigRef(configRef, "PikaOA release runtime configRef");
const match = /^releaseRuntime\.targets\.([A-Za-z0-9._-]+)$/u.exec(resolved.fragment);
if (resolved.file !== "config/pikaoa.yaml" || match === null) {
throw new Error("PikaOA release runtime configRef must select config/pikaoa.yaml#releaseRuntime.targets.<id>");
}
return parseTarget(match[1]!, record(resolved.value, configRef), resolved.file, "releaseRuntime");
}
function planPayload(options: TestTargetOptions, selection: Selection): Record<string, unknown> {
const base = basePayload(options, selection);
if (selection.target === null) return base;
@@ -486,7 +601,10 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
worker: componentSelector("worker", context.instanceId),
web: componentSelector("web", context.instanceId),
};
const structuralManifest = manifestForStep(buildManifest(target, context, placeholderSecrets()), context.step);
const structuralManifest = pikaoaManifestForStep(renderPikaoaTestRuntimeManifest(target, context, {
secretMode: "materialized",
secrets: placeholderPikaoaSecrets(),
}), context.step);
return {
namespace: {
name: context.namespace,
@@ -508,7 +626,7 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
secretName: target.runtime.secretName,
key: "pikaoa.yaml",
mountPath: "/etc/pikaoa/pikaoa.yaml",
consumers: ["migration", "api", "worker"],
consumers: ["initializer", "api", "worker"],
valuesPrinted: false,
},
probes: {
@@ -531,10 +649,11 @@ function renderedPlan(selection: Selection, context: RenderContext): Record<stri
fsGroup: target.runtime.attachment.fsGroup,
accessModes: target.runtime.attachment.accessModes,
},
migration: {
jobName: target.migration.jobName,
image: context.migrationImage,
command: target.migration.command,
initializer: {
mode: target.initializer.mode,
jobName: target.initializer.jobName,
image: context.initializerImage,
command: target.initializer.command,
applyPhase: "before-workloads",
},
step: context.step,
@@ -582,14 +701,17 @@ async function startResult(config: UniDeskConfig, options: TestTargetOptions, se
}
const material = options.step === "foundation" ? {
ok: true,
values: placeholderSecrets(),
values: placeholderPikaoaSecrets(),
sources: secretSources(selection.target).map((source) => secretSourceSummary(selection.configPath, source)),
blockers: [] as Blocker[],
} : readSecretMaterial(selection, selection.target);
if (!material.ok) {
return renderResult({ ...planned, ok: false, mutation: false, remoteQueried: false, blockers: material.blockers, secret: { sources: material.sources, valuesPrinted: false }, failure: "secret-preflight-blocked" }, options);
}
const manifest = manifestForStep(buildManifest(selection.target, context, material.values), options.step);
const manifest = pikaoaManifestForStep(renderPikaoaTestRuntimeManifest(selection.target, context, {
secretMode: "materialized",
secrets: material.values,
}), options.step);
const remote = await remoteCapture(config, selection.target.route, ["sh"], startScript(selection.target, context, manifest));
const parsed = parseJsonOutput(remote.stdout);
const mutation = parsed?.mutation === true;
@@ -647,7 +769,7 @@ function renderContext(options: TestTargetOptions, target: TestTargetSpec, requi
apiImage: renderImage(target.source.apiImage, commit),
workerImage: renderImage(target.source.workerImage, commit),
webImage: renderImage(target.source.webImage, commit),
migrationImage: renderImage(target.source.migrationImage, commit),
initializerImage: renderImage(target.source.initializerImage, commit),
};
}
@@ -674,9 +796,9 @@ function safetyBlockers(options: TestTargetOptions, selection: Selection, contex
function consistencyWarnings(target: TestTargetSpec, context: RenderContext | null): Warning[] {
const warnings: Warning[] = [];
if (!target.migration.command.includes("migrate") || !target.migration.command.includes("up")) warnings.push(warning("migration-command-unrecognized", "迁移命令未显示产品 migrate up;配置一致性只报告 warning。"));
if (![target.source.apiImage, target.source.workerImage, target.source.webImage, target.source.migrationImage].every((image) => image.includes("${commit}"))) warnings.push(warning("image-template-not-commit-scoped", "API、Worker、Web 或迁移镜像模板未包含 ${commit};版本一致性只报告 warning。"));
if (context !== null && ![context.apiImage, context.workerImage, context.webImage, context.migrationImage].every((image) => image.includes(context.commit))) warnings.push(warning("image-commit-not-visible", "渲染镜像引用未显示选中 commit;该漂移不阻塞当前 MVP。"));
if (!target.initializer.command.includes("migrate") || !target.initializer.command.includes("up")) warnings.push(warning("initializer-command-unrecognized", "空库 initializer 的当前产品命令未显示 migrate up;配置一致性只报告 warning。"));
if (![target.source.apiImage, target.source.workerImage, target.source.webImage, target.source.initializerImage].every((image) => image.includes("${commit}"))) warnings.push(warning("image-template-not-commit-scoped", "API、Worker、Web 或 initializer 镜像模板未包含 ${commit};版本一致性只报告 warning。"));
if (context !== null && ![context.apiImage, context.workerImage, context.webImage, context.initializerImage].every((image) => image.includes(context.commit))) warnings.push(warning("image-commit-not-visible", "渲染镜像引用未显示选中 commit;该漂移不阻塞当前 MVP。"));
if (target.observability.exporterWarning) warnings.push(warning("otel-exporter-warning-only", "OTel exporter 失败按 owning YAML 仅报告 warning,不阻塞业务运行。"));
if (!target.observability.prometheusScrape) warnings.push(warning("prometheus-scrape-disabled", "Prometheus scrape 已在 target YAML 中关闭。"));
return warnings;
@@ -717,178 +839,6 @@ function prometheusRuntimeStatus(items: Record<string, unknown>[], selector: Pro
};
}
function buildManifest(target: TestTargetSpec, context: RenderContext, secrets: SecretMaterial): Record<string, unknown>[] {
const labels = ownershipLabels(target, context);
const secretData = runtimeSecretData(target, context, secrets);
const apiSelector = componentSelector("api", context.instanceId);
const workerSelector = componentSelector("worker", context.instanceId);
const webSelector = componentSelector("web", context.instanceId);
const migrationSelector = componentSelector("migration", context.instanceId);
const prometheusPodLabels = target.observability.prometheusScrape ? {
[target.observability.prometheusSelector.key]: target.observability.prometheusSelector.value,
} : {};
const commonPodAnnotations = (path: string, port: number): Record<string, string> => target.observability.prometheusScrape ? {
"prometheus.io/scrape": "true",
"prometheus.io/path": path,
"prometheus.io/port": String(port),
} : {};
return [
{
apiVersion: "v1", kind: "Namespace", metadata: { name: context.namespace, labels, annotations: namespaceAnnotations(target, context) },
},
{
apiVersion: "v1", kind: "Secret", metadata: { name: target.runtime.secretName, namespace: context.namespace, labels }, type: "Opaque", stringData: secretData,
},
{
apiVersion: "v1", kind: "PersistentVolumeClaim", metadata: { name: target.runtime.attachment.claimName, namespace: context.namespace, labels },
spec: {
accessModes: target.runtime.attachment.accessModes,
resources: { requests: { storage: target.runtime.attachment.storageRequest } },
...(target.runtime.attachment.storageClassName === null ? {} : { storageClassName: target.runtime.attachment.storageClassName }),
},
},
{
apiVersion: "v1", kind: "Service", metadata: { name: target.runtime.apiServiceName, namespace: context.namespace, labels: { ...labels, ...apiSelector } },
spec: { selector: apiSelector, ports: [{ name: "http", port: target.runtime.apiPort, targetPort: target.runtime.apiPort }] },
},
{
apiVersion: "v1", kind: "Service", metadata: { name: target.exposure.serviceName, namespace: context.namespace, labels: { ...labels, ...webSelector } },
spec: {
type: target.exposure.serviceType,
selector: webSelector,
ports: [{ name: "http", port: target.runtime.webPort, targetPort: target.runtime.webPort, nodePort: target.exposure.port }],
},
},
{
apiVersion: "batch/v1", kind: "Job", metadata: { name: target.migration.jobName, namespace: context.namespace, labels: { ...labels, ...migrationSelector } },
spec: {
backoffLimit: target.migration.backoffLimit,
template: {
metadata: { labels: { ...labels, ...migrationSelector } },
spec: {
restartPolicy: "Never",
containers: [{
name: "migration", image: context.migrationImage, imagePullPolicy: target.source.pullPolicy,
command: target.migration.command,
env: [{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" }],
volumeMounts: [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }],
}],
volumes: [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }],
},
},
},
},
deployment(target, context, "api", context.apiImage, apiSelector, target.runtime.apiPort, prometheusPodLabels, commonPodAnnotations(target.observability.apiMetricsPath, target.runtime.apiPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
{ name: "PIKAOA_SESSION_SECRET", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.sessionSecret.targetKey } } },
{ name: "PIKAOA_BOOTSTRAP_ADMIN_USERNAME", value: target.runtime.administrator.username },
{ name: "PIKAOA_BOOTSTRAP_ADMIN_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.administrator.password.targetKey } } },
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_USERNAME", value: target.runtime.employee.username },
{ name: "PIKAOA_BOOTSTRAP_EMPLOYEE_PASSWORD", valueFrom: { secretKeyRef: { name: target.runtime.secretName, key: target.runtime.employee.password.targetKey } } },
], target.probes.apiLivenessPath, target.probes.apiReadinessPath),
deployment(target, context, "worker", context.workerImage, workerSelector, target.runtime.workerMetricsPort, prometheusPodLabels, commonPodAnnotations(target.observability.workerMetricsPath, target.runtime.workerMetricsPort), [
{ name: "PIKAOA_CONFIG", value: "/etc/pikaoa/pikaoa.yaml" },
], target.probes.workerLivenessPath, target.probes.workerReadinessPath),
deployment(target, context, "web", context.webImage, webSelector, target.runtime.webPort, {}, {}, [
{ name: target.runtime.webApiUpstreamEnv, value: `http://${target.runtime.apiServiceName}:${target.runtime.apiPort}` },
], target.probes.webLivenessPath, target.probes.webReadinessPath),
];
}
function manifestForStep(manifest: Record<string, unknown>[], step: TestTargetStep): Record<string, unknown>[] {
if (step === "all") return manifest;
return manifest.filter((object) => {
if (object.kind === "Namespace" || object.kind === "PersistentVolumeClaim") return true;
if (step === "foundation") return false;
if (object.kind === "Secret") return true;
if (step === "migration") return object.kind === "Job";
const metadata = optionalRecord(object.metadata, "metadata") ?? {};
const labels = optionalRecord(metadata.labels, "metadata.labels") ?? {};
return labels["app.kubernetes.io/component"] === step;
});
}
function deployment(
target: TestTargetSpec,
context: RenderContext,
component: "api" | "worker" | "web",
image: string,
selector: Record<string, string>,
port: number,
podLabels: Record<string, string>,
annotations: Record<string, string>,
env: Array<Record<string, unknown>>,
livenessPath: string,
readinessPath: string,
): Record<string, unknown> {
const labels = ownershipLabels(target, context);
return {
apiVersion: "apps/v1", kind: "Deployment", metadata: { name: `pikaoa-${component}`, namespace: context.namespace, labels: { ...labels, ...selector } },
spec: {
replicas: 1, selector: { matchLabels: selector },
template: {
metadata: { labels: { ...labels, ...selector, ...podLabels }, annotations },
spec: {
...(component === "api" ? { securityContext: { fsGroup: target.runtime.attachment.fsGroup } } : {}),
containers: [{
name: component, image, imagePullPolicy: target.source.pullPolicy, env,
ports: [{ name: component === "worker" ? "metrics" : "http", containerPort: port }],
volumeMounts: [
...(component === "web" ? [] : [{ name: "runtime", mountPath: "/etc/pikaoa", readOnly: true }]),
...(component === "api" ? [{ name: target.runtime.attachment.claimName, mountPath: target.runtime.attachment.storageRoot }] : []),
],
livenessProbe: { httpGet: { path: livenessPath, port }, periodSeconds: 5, failureThreshold: 12 },
readinessProbe: { httpGet: { path: readinessPath, port }, periodSeconds: 3, failureThreshold: 20 },
}],
volumes: [
...(component === "web" ? [] : [{ name: "runtime", secret: { secretName: target.runtime.secretName, items: [{ key: "pikaoa.yaml", path: "pikaoa.yaml" }] } }]),
...(component === "api" ? [{ name: target.runtime.attachment.claimName, persistentVolumeClaim: { claimName: target.runtime.attachment.claimName } }] : []),
],
},
},
},
};
}
function runtimeSecretData(target: TestTargetSpec, context: RenderContext, secrets: SecretMaterial): Record<string, string> {
const databaseURL = renderDatabaseURL(target, secrets.databaseURL);
return {
[target.database.connection.targetKey]: databaseURL,
[target.runtime.sessionSecret.targetKey]: secrets.sessionSecret,
[target.runtime.administrator.password.targetKey]: secrets.administratorPassword,
[target.runtime.employee.password.targetKey]: secrets.employeePassword,
"pikaoa.yaml": [
"api:",
` listen: 0.0.0.0:${target.runtime.apiPort}`,
"worker:",
` interval: ${target.runtime.workerInterval}`,
` metricsListen: 0.0.0.0:${target.runtime.workerMetricsPort}`,
" outbox:",
` batchSize: ${target.runtime.outboxBatchSize}`,
` maxAttempts: ${target.runtime.outboxMaxAttempts}`,
` retryDelay: ${target.runtime.outboxRetryDelay}`,
"database:",
` url: ${JSON.stringify(databaseURL)}`,
"identity:",
` sessionTTL: ${target.runtime.sessionTTL}`,
"attachment:",
` storageRoot: ${JSON.stringify(target.runtime.attachment.storageRoot)}`,
` maxBytes: ${target.runtime.attachment.maxBytes}`,
"observability:",
` serviceName: pikaoa-test-${context.instanceId}`,
` otlpEndpoint: ${JSON.stringify(target.observability.otlpEndpoint)}`,
`shutdownTimeout: ${target.runtime.shutdownTimeout}`,
"",
].join("\n"),
};
}
function renderDatabaseURL(target: TestTargetSpec, sourceURL: string): string {
const databaseURL = new URL(sourceURL);
databaseURL.searchParams.set("search_path", target.database.schema);
return databaseURL.toString();
}
function databasePlanSummary(target: TestTargetSpec): Record<string, unknown> {
const databaseURL = new URL(renderDatabaseURL(target, "postgresql://redacted@db.invalid/pikaoa_test?sslmode=require"));
return {
@@ -1134,7 +1084,7 @@ printf '{"ok":true,"mutation":true,"code":"start-submitted","submitted":true,"pr
}
function startRunnerScript(target: TestTargetSpec, context: RenderContext, manifest: Record<string, unknown>[], job: AsyncJobIdentity): string {
const migration = manifest.filter((object) => object.kind === "Job");
const initializer = manifest.filter((object) => object.kind === "Job");
const workload = (name: string): Record<string, unknown>[] => manifest.filter((object) => {
if (object.kind !== "Deployment" && object.kind !== "Service") return false;
const metadata = optionalRecord(object.metadata, "metadata") ?? {};
@@ -1146,7 +1096,7 @@ function startRunnerScript(target: TestTargetSpec, context: RenderContext, manif
const encode = (objects: Record<string, unknown>[]): string => Buffer.from(objects.map((object) => JSON.stringify(object)).join("\n---\n"), "utf8").toString("base64");
const foundationEncoded = encode(foundation);
const runtimeEncoded = encode(runtime);
const migrationEncoded = encode(migration);
const initializerEncoded = encode(initializer);
const apiEncoded = encode(workload("api"));
const workerEncoded = encode(workload("worker"));
const webEncoded = encode(workload("web"));
@@ -1228,7 +1178,7 @@ if kubectl get namespace "$namespace" --request-timeout=${target.taskState.statu
fi
printf '%s' ${shQuote(foundationEncoded)} | base64 -d >"$tmp/foundation.yaml"
printf '%s' ${shQuote(runtimeEncoded)} | base64 -d >"$tmp/runtime.yaml"
printf '%s' ${shQuote(migrationEncoded)} | base64 -d >"$tmp/migration.yaml"
printf '%s' ${shQuote(initializerEncoded)} | base64 -d >"$tmp/initializer.yaml"
printf '%s' ${shQuote(apiEncoded)} | base64 -d >"$tmp/api.yaml"
printf '%s' ${shQuote(workerEncoded)} | base64 -d >"$tmp/worker.yaml"
printf '%s' ${shQuote(webEncoded)} | base64 -d >"$tmp/web.yaml"
@@ -1249,22 +1199,22 @@ if ! kubectl apply --server-side --force-conflicts --field-manager=${shQuote(tar
failure_code=runtime-secret-apply-failed
exit 44
fi
if [ "$step" = all ] || [ "$step" = migration ]; then
current_phase=migration-apply
if [ "$step" = all ] || [ "$step" = init ]; then
current_phase=initializer-apply
write_status running "$current_phase" start-running null
check_canceled
kubectl -n "$namespace" delete job/${target.migration.jobName} --ignore-not-found --wait=false >/dev/null 2>&1 || true
if ! kubectl apply --server-side --force-conflicts --field-manager=${shQuote(target.fieldManager)} -f "$tmp/migration.yaml" >"$tmp/migration-apply.out" 2>"$tmp/migration-apply.err"; then failure_code=migration-apply-failed; exit 45; fi
current_phase=migration-wait
kubectl -n "$namespace" delete job/${target.initializer.jobName} --ignore-not-found --wait=false >/dev/null 2>&1 || true
if ! kubectl apply --server-side --force-conflicts --field-manager=${shQuote(target.fieldManager)} -f "$tmp/initializer.yaml" >"$tmp/initializer-apply.out" 2>"$tmp/initializer-apply.err"; then failure_code=initializer-apply-failed; exit 45; fi
current_phase=initializer-wait
write_status running "$current_phase" start-running null
migration_deadline=$(( $(date +%s) + ${target.waitTimeoutSeconds} ))
initializer_deadline=$(( $(date +%s) + ${target.waitTimeoutSeconds} ))
while :; do
check_canceled
if ! kubectl -n "$namespace" get job/${target.migration.jobName} --request-timeout=${Math.min(target.taskState.statusRequestTimeoutSeconds, 5)}s -o json >"$tmp/migration-job.json" 2>"$tmp/migration-wait.err"; then
failure_code=migration-job-query-failed
if ! kubectl -n "$namespace" get job/${target.initializer.jobName} --request-timeout=${Math.min(target.taskState.statusRequestTimeoutSeconds, 5)}s -o json >"$tmp/initializer-job.json" 2>"$tmp/initializer-wait.err"; then
failure_code=initializer-job-query-failed
exit 46
fi
if ! migration_condition="$(python3 - "$tmp/migration-job.json" <<'PY'
if ! initializer_condition="$(python3 - "$tmp/initializer-job.json" <<'PY'
import json, pathlib, sys
payload = json.loads(pathlib.Path(sys.argv[1]).read_text())
conditions = {
@@ -1282,16 +1232,16 @@ else:
print("running")
PY
)"; then
failure_code=migration-job-condition-invalid
failure_code=initializer-job-condition-invalid
exit 46
fi
case "$migration_condition" in
case "$initializer_condition" in
complete) break ;;
failure-target) failure_code=migration-job-failure-target; exit 46 ;;
failed) failure_code=migration-job-failed; exit 46 ;;
failure-target) failure_code=initializer-job-failure-target; exit 46 ;;
failed) failure_code=initializer-job-failed; exit 46 ;;
esac
if [ "$(date +%s)" -ge "$migration_deadline" ]; then
failure_code=migration-job-timeout
if [ "$(date +%s)" -ge "$initializer_deadline" ]; then
failure_code=initializer-job-timeout
exit 46
fi
sleep 1
@@ -1567,8 +1517,8 @@ function sourceSummary(target: TestTargetSpec, commit: string | null, context?:
commitPolicy: target.source.commitPolicy,
commit,
images: context === undefined
? { apiTemplate: target.source.apiImage, workerTemplate: target.source.workerImage, webTemplate: target.source.webImage, migrationTemplate: target.source.migrationImage }
: { api: context.apiImage, worker: context.workerImage, web: context.webImage, migration: context.migrationImage },
? { apiTemplate: target.source.apiImage, workerTemplate: target.source.workerImage, webTemplate: target.source.webImage, initializerTemplate: target.source.initializerImage }
: { api: context.apiImage, worker: context.workerImage, web: context.webImage, initializer: context.initializerImage },
};
}
@@ -1662,10 +1612,6 @@ function manifestFingerprint(manifest: Record<string, unknown>[], stableRequest:
return `sha256:${createHash("sha256").update(JSON.stringify(normalized)).digest("hex")}`;
}
function placeholderSecrets(): SecretMaterial {
return { databaseURL: "postgresql://redacted@db.invalid/pikaoa_test?sslmode=require", sessionSecret: "<redacted>", administratorPassword: "<redacted>", employeePassword: "<redacted>" };
}
function secretSources(target: TestTargetSpec): SecretSourceSpec[] {
return [target.database.connection, target.runtime.sessionSecret, target.runtime.administrator.password, target.runtime.employee.password];
}
@@ -1760,6 +1706,18 @@ function imageReference(value: unknown, path: string): string {
return image;
}
function imageRepository(value: unknown, path: string): string {
const image = nonEmpty(value, path);
if (/\s/u.test(image) || image.includes("@") || /:[^/]+$/u.test(image)) throw new Error(`${path} tag/digest `);
return image;
}
function relativeSourcePath(value: unknown, path: string): string {
const sourcePath = nonEmpty(value, path);
if (sourcePath.startsWith("/") || sourcePath.split(/[\\/]/u).includes("..")) throw new Error(`${path} .. `);
return sourcePath;
}
function otlpGrpcEndpoint(value: unknown, path: string): string {
const endpoint = nonEmpty(value, path);
if (endpoint.includes("://") || !/^[A-Za-z0-9.-]+:[1-9][0-9]{0,4}$/u.test(endpoint)) throw new Error(`${path} scheme OTLP gRPC host:port`);
@@ -47,6 +47,22 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
.not.toBe(githubTokenEnvNameForSecretKey(globalCredential.gitFetchCredential.secretRef.key));
});
test("keeps PikaOA master and release authorities on distinct Secret keys", () => {
const config = readGiteaConfig();
const testAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-test-nc01");
const productionAuthority = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-nc01");
const testKey = testAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
const productionKey = productionAuthority?.credentialOverride?.github.gitFetchCredential.secretRef.key;
expect(testAuthority?.upstream.branch).toBe("master");
expect(productionAuthority?.upstream.branch).toBe("release");
expect(testKey).toBe("github-token-pikainc-pikaoa");
expect(productionKey).toBe("github-token-pikainc-pikaoa-release");
expect(productionKey).not.toBe(testKey);
expect(githubTokenEnvNameForSecretKey(productionKey ?? ""))
.not.toBe(githubTokenEnvNameForSecretKey(testKey ?? ""));
});
test("normalizes Secret keys to bounded environment variable names", () => {
expect(githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia"))
.toBe("UNIDESK_GITEA_GITHUB_TOKEN_GITHUB_TOKEN_PIKAINC_SELFMEDIA");
@@ -87,6 +103,25 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.github.sourceRef);
});
test("production PikaOA status scope never reads the unselected master credential", () => {
const config = readGiteaConfig();
const production = config.sourceAuthority.repositories.find((repo) => repo.key === "pikaoa-nc01");
expect(production).toBeDefined();
const requested: string[] = [];
const result = buildGiteaMirrorBootstrapCredentialPreflight(config, [production!], (credential) => {
requested.push(credential.gitFetchCredential.secretRef.key);
return { [credential.sourceKey]: "fixture-release-token" };
});
expect(result.blockers).toEqual([]);
expect(requested).toEqual(["github-token-pikainc-pikaoa-release"]);
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).toMatchObject({
id: "github-upstream:pikaoa-nc01",
valuesPrinted: false,
});
});
test("does not inject the NC01 selfmedia token into JD01 bridge containers", () => {
const config = readGiteaConfig();
const selfmediaTokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia");
+3 -1
View File
@@ -1074,7 +1074,9 @@ rows = []
for repo in repos:
repository = repo["upstream"]["repository"]
topology_spec = next((item for item in topology_repositories if isinstance(item, dict) and item.get("repository") == repository), {})
token = os.environ[topology_spec.get("githubTokenEnv") or os.environ["UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV"]]
credential_spec = repo.get("githubCredential") or {}
token_env = credential_spec.get("tokenEnv") or topology_spec.get("githubTokenEnv") or os.environ["UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV"]
token = os.environ[token_env]
branch = repo["upstream"]["branch"]
result = request(f"/repos/{repository}/hooks")
hooks_result = parse_github_hooks_response(result.get("ok"), result.get("status"), result.get("body"))
+8 -36
View File
@@ -1182,6 +1182,9 @@ function envVars(gitea: GiteaConfig, target: GiteaTarget): string {
function remoteScript(action: "apply" | "status" | "validate" | "mirror-preflight" | "mirror-bootstrap" | "mirror-sync" | "mirror-status" | "mirror-webhook-apply" | "mirror-webhook-status", gitea: GiteaConfig, target: GiteaTarget, manifest: string, options: ApplyOptions | MirrorOptions, params: MirrorRemoteParams = {}): { script: string; payloadSummary: Record<string, unknown> } {
const frpcExposure = targetFrpcExposure(gitea, target);
const sync = targetWebhookSync(gitea, target);
const selectedRepositories = params.repos ?? repositoriesForTarget(gitea, target);
const selectedCredentials = githubCredentialsForConsumers(gitea, target, selectedRepositories, false);
const defaultCredential = selectedCredentials[0] ?? gitea.sourceAuthority.credentials.github;
const env: Record<string, string> = {
UNIDESK_GITEA_ACTION: action,
UNIDESK_GITEA_TARGET_ID: target.id,
@@ -1201,9 +1204,9 @@ function remoteScript(action: "apply" | "status" | "validate" | "mirror-prefligh
UNIDESK_GITEA_ROOT_URL: gitea.app.server.rootUrl,
UNIDESK_GITEA_GITHUB_PROXY_ENABLED: gitea.sourceAuthority.githubProxy.enabled ? "1" : "0",
UNIDESK_GITEA_GITHUB_PROXY_URL: gitea.sourceAuthority.githubProxy.url,
UNIDESK_GITEA_GITHUB_SECRET_KEY: gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key,
UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV: githubTokenEnvName(gitea.sourceAuthority.credentials.github.gitFetchCredential.secretRef.key),
UNIDESK_GITEA_GITHUB_SECRET_ENV_MAP: githubCredentialsForConsumers(gitea, target, repositoriesForTarget(gitea, target), true)
UNIDESK_GITEA_GITHUB_SECRET_KEY: defaultCredential.gitFetchCredential.secretRef.key,
UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV: githubTokenEnvName(defaultCredential.gitFetchCredential.secretRef.key),
UNIDESK_GITEA_GITHUB_SECRET_ENV_MAP: selectedCredentials
.map((credential) => `${credential.gitFetchCredential.secretRef.key}=${githubTokenEnvName(credential.gitFetchCredential.secretRef.key)}`)
.join(","),
UNIDESK_GITEA_NO_PROXY: gitea.sourceAuthority.githubProxy.noProxy.join(","),
@@ -1702,10 +1705,7 @@ function webhookSyncSummary(gitea: GiteaConfig, target: GiteaTarget): Record<str
function credentialSummaries(gitea: GiteaConfig, repositories = gitea.sourceAuthority.repositories): Array<Record<string, unknown>> {
const adminPath = credentialPath(gitea, gitea.sourceAuthority.credentials.admin.sourceRef);
const github = gitea.sourceAuthority.credentials.github;
const githubPath = credentialPath(gitea, github.sourceRef);
const adminLinePair = existsSync(adminPath) ? parseLinePairCredential(adminPath, gitea.sourceAuthority.credentials.admin) : { username: null, password: null };
const githubEnv = existsSync(githubPath) ? parseGithubCredentialFile(githubPath, github) : {};
const summaries: Array<Record<string, unknown>> = [
{
id: "gitea-admin",
@@ -1716,36 +1716,8 @@ function credentialSummaries(gitea: GiteaConfig, repositories = gitea.sourceAuth
permissionResult: { status: "not-applicable", error: null },
valuesPrinted: false,
},
{
id: "github-upstream",
sourceRef: github.sourceRef,
present: existsSync(githubPath),
requiredKeysPresent: Boolean(githubEnv[github.sourceKey]),
fingerprint: fingerprintKeys(githubEnv, [github.sourceKey]),
permissionResult: existsSync(githubPath) && Boolean(githubEnv[github.sourceKey])
? { status: "not-probed", error: null }
: { status: "blocked-missing-credential", error: "credential material is absent" },
valuesPrinted: false,
},
];
for (const repo of repositories) {
const githubOverride = repo.credentialOverride?.github;
if (githubOverride === undefined) continue;
const sourcePath = credentialPath(gitea, githubOverride.sourceRef);
const values = existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, githubOverride) : {};
summaries.push({
id: `github-upstream:${repo.key}`,
repository: repo.upstream.repository,
sourceRef: githubOverride.sourceRef,
present: existsSync(sourcePath),
requiredKeysPresent: Boolean(values[githubOverride.sourceKey]),
fingerprint: fingerprintKeys(values, [githubOverride.sourceKey]),
permissionResult: existsSync(sourcePath) && Boolean(values[githubOverride.sourceKey])
? { status: "not-probed", permissions: githubOverride.permissions, error: null }
: { status: "blocked-missing-credential", permissions: githubOverride.permissions, error: "credential material is absent" },
valuesPrinted: false,
});
}
summaries.push(...buildGiteaMirrorBootstrapCredentialPreflight(gitea, repositories).credentials);
return summaries;
}
@@ -1803,7 +1775,7 @@ function githubCredentialsForConsumers(
repositories: GiteaMirrorRepository[],
includeHookOwnerCredentials: boolean,
): GiteaGithubCredential[] {
const credentials = [gitea.sourceAuthority.credentials.github, ...repositories.flatMap((repo) => repo.credentialOverride?.github ?? [])];
const credentials = repositories.map((repo) => githubCredentialForRepo(gitea, repo));
if (includeHookOwnerCredentials
&& gitea.sourceAuthority.webhookSync.hookReconcile.ownerTargetId.toLowerCase() === target.id.toLowerCase()) {
credentials.push(...gitea.sourceAuthority.repositories.flatMap((repo) => {
@@ -38,6 +38,11 @@ test("admission contract rejects malformed required declarations instead of sile
const duplicatedRequiredConsumers = duplicatedServiceAccount.consumers.filter((item: any) => item.deliveryProvenance !== undefined);
duplicatedRequiredConsumers[1].deliveryProvenance.executionServiceAccountName = duplicatedRequiredConsumers[0].deliveryProvenance.executionServiceAccountName;
expect(() => parsePacAdmissionContract(duplicatedServiceAccount)).toThrow("executionServiceAccountName must be unique per target");
const denyingAdmission = structuredClone(source);
denyingAdmission.deliveryProvenance.admission.failurePolicy = "Fail";
denyingAdmission.deliveryProvenance.admission.validationActions = ["Deny"];
expect(() => parsePacAdmissionContract(denyingAdmission)).toThrow("validationActions must be exactly [Warn, Audit]");
});
test("admission queue transition contract rejects absent sources, unknown Tekton states, and duplicate transitions", () => {
@@ -55,7 +60,7 @@ test("admission queue transition contract rejects absent sources, unknown Tekton
expect(() => parsePacAdmissionContract(duplicate)).toThrow("deliveryProvenance.queueTransition.allowed must be unique");
});
test("versioned admission desired state protects controller proof, candidate identity, params, and execution SA", () => {
test("versioned admission desired state reports controller proof and candidate drift without denying delivery", () => {
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();
@@ -69,15 +74,15 @@ test("versioned admission desired state protects controller proof, candidate ide
toState: "started",
}]);
expect(contract.child.enabled).toBe(false);
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01"]);
expect(contract.consumers.map((item) => item.id)).toEqual(["agentrun-nc01-v02", "platform-infra-sub2rank-nc01", "selfmedia-nc01", "selfmedia-production-nc01", "pikaoa-test-nc01"]);
const items = documents(renderPacAdmissionDesiredFragment("NC01"));
expect(items.map((item) => item.kind)).toEqual(["ValidatingAdmissionPolicy", "ValidatingAdmissionPolicyBinding"]);
const policy = items[0];
const expressions = policy.spec.validations.map((item: any) => item.expression).join("\n");
const messages = policy.spec.validations.map((item: any) => item.message).join("\n");
expect(policy.spec.failurePolicy).toBe("Fail");
expect(policy.spec.failurePolicy).toBe("Ignore");
expect(policy.spec.matchConstraints.matchPolicy).toBe("Equivalent");
expect(items[1].spec.validationActions).toEqual(["Deny"]);
expect(items[1].spec.validationActions).toEqual(["Warn", "Audit"]);
expect(policy.metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("0");
expect(items[1].metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("1");
expect(expressions).toContain("request.userInfo.username");
@@ -134,8 +139,8 @@ test("versioned admission desired state protects controller proof, candidate ide
test("required consumer default RBAC has one automatic desired owner per namespace and no Tekton mutation verbs", () => {
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
expect(rbac.map((item) => item.kind)).toEqual(["Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding"]);
expect(rbac.filter((item) => item.kind === "Role").map((item) => item.metadata.namespace)).toEqual(["agentrun-ci", "devops-infra", "selfmedia-ci"]);
expect(rbac.map((item) => item.kind)).toEqual(["Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding", "Role", "RoleBinding"]);
expect(rbac.filter((item) => item.kind === "Role").map((item) => item.metadata.namespace)).toEqual(["agentrun-ci", "devops-infra", "selfmedia-ci", "pikaoa-ci"]);
for (const role of rbac.filter((item) => item.kind === "Role")) {
expect(role.metadata.annotations["argocd.argoproj.io/sync-wave"]).toBe("-1");
expect(role.metadata.annotations["unidesk.ai/effective-config-sha256"]).toBe(pacConsumerRbacDesiredIdentity("NC01").configSha256);
@@ -150,6 +155,8 @@ test("required consumer default RBAC has one automatic desired owner per namespa
"RoleBinding",
"Role",
"RoleBinding",
"Role",
"RoleBinding",
"ValidatingAdmissionPolicy",
"ValidatingAdmissionPolicyBinding",
"ServiceAccount",
@@ -158,7 +165,7 @@ test("required consumer default RBAC has one automatic desired owner per namespa
]);
});
test("live admission evaluator requires exact desired hashes, observed generation, default-SA loss, and a new resource epoch", () => {
test("live admission evaluator reports exact desired drift as non-blocking warnings", () => {
const contract = readPacAdmissionContract();
const admission = documents(renderPacAdmissionDesiredFragment("NC01"));
const rbac = documents(renderPacConsumerRbacDesiredFragment("NC01"));
@@ -174,7 +181,7 @@ test("live admission evaluator requires exact desired hashes, observed generatio
rbacConfigSha256: rbac[0].metadata.annotations["unidesk.ai/effective-config-sha256"],
};
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)).toMatchObject({ ready: true, blocking: false, warning: false, activeAfter: "2026-01-01T00:00:01Z", defaultCanMutate: false });
expect(evaluator.evaluatePacAdmissionState({
...input,
policy: {
@@ -189,7 +196,7 @@ test("live admission evaluator requires exact desired hashes, observed generatio
},
},
},
})).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-expression-warning"]) });
})).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["policy-expression-warning"]) });
const apiDefaultedPolicy = {
...policy,
spec: {
@@ -206,10 +213,10 @@ test("live admission evaluator requires exact desired hashes, observed generatio
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"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, expected: { ...expected, configSha256: `sha256:${"0".repeat(64)}` } })).toMatchObject({ ready: false, reasons: expect.arrayContaining(["policy-config-sha256-mismatch"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: true })).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["default-serviceaccount-can-mutate-tekton-runs"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, defaultCanMutate: null })).toMatchObject({ ready: false, blocking: false, warning: true, 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, blocking: false, warning: true, reasons: expect.arrayContaining(["default-role-rules-mismatch"]) });
expect(evaluator.evaluatePacAdmissionState({ ...input, expected: { ...expected, configSha256: `sha256:${"0".repeat(64)}` } })).toMatchObject({ ready: false, blocking: false, warning: true, reasons: expect.arrayContaining(["policy-config-sha256-mismatch"]) });
const mutatedPolicy = {
...policy,
spec: {
@@ -345,10 +352,10 @@ test("AgentRun rendered delivery plan and real TaskRun terminals close the statu
})).toMatchObject({ ok: true, code: "pac-ready-gitops-exact" });
});
test("provenance fixtures fail closed for pre-bootstrap, forged marker, wrong SA, and policy mismatch", () => {
test("provenance fixtures keep outer PaC delivery eligible while reporting admission warnings", () => {
const result = evaluator.runPacStatusFixtureChecks();
expect(result.ok).toBe(true);
for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "equal-revisions-ignore-owning-git-fetch-failure", "different-revisions-fetch-failure-remains-unknown", "admission-provenance-verified", "pre-bootstrap-run-fails-closed", "forged-name-candidate-fails-closed", "forged-label-candidate-fails-closed", "forged-pipeline-ref-candidate-fails-closed", "wrong-service-account-fails-closed", "policy-identity-mismatch-fails-closed"]) {
for (const id of ["gitops-only-registry-not-configured", "image-registry-missing-fails-closed", "equal-revisions-ignore-owning-git-fetch-failure", "different-revisions-fetch-failure-remains-unknown", "admission-provenance-verified", "pre-bootstrap-run-warning-only", "marker-mismatch-warning-only", "candidate-label-warning-only", "candidate-pipeline-ref-warning-only", "service-account-mismatch-warning-only", "policy-identity-mismatch-warning-only"]) {
expect(result.checks.find((item) => item.id === id)?.ok).toBe(true);
}
});
+7 -5
View File
@@ -33,8 +33,8 @@ export interface PacAdmissionContract {
readonly allowedQueueTransitions: readonly PacAdmissionQueueTransitionContract[];
readonly policyName: string;
readonly bindingName: string;
readonly failurePolicy: "Fail";
readonly validationActions: readonly ["Deny"];
readonly failurePolicy: "Ignore";
readonly validationActions: readonly ["Warn", "Audit"];
readonly child: {
readonly enabled: false;
readonly parentNameAnnotation: string;
@@ -83,7 +83,9 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
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]");
if (validationActions.length !== 2 || validationActions[0] !== "Warn" || validationActions[1] !== "Audit") {
throw new Error("deliveryProvenance.admission.validationActions must be exactly [Warn, Audit]");
}
const consumers = recordArray(root.consumers, "consumers").flatMap((consumer, index) => {
if (consumer.deliveryProvenance === undefined) return [];
const value = record(consumer.deliveryProvenance, `consumers[${index}].deliveryProvenance`);
@@ -127,8 +129,8 @@ export function parsePacAdmissionContract(source: Record<string, unknown>): PacA
allowedQueueTransitions,
policyName,
bindingName,
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Fail"),
validationActions: ["Deny"],
failurePolicy: literal(admission.failurePolicy, "deliveryProvenance.admission.failurePolicy", "Ignore"),
validationActions: ["Warn", "Audit"],
child: {
enabled: literal(child.enabled, "deliveryProvenance.child.enabled", false),
parentNameAnnotation: required(child.parentNameAnnotation, "deliveryProvenance.child.parentNameAnnotation"),
@@ -120,19 +120,19 @@ test("unselected parse and repository errors remain warnings while selected obje
expect(() => parsePacConfigDocument(invalidConsumerDocument, { consumerId: "selfmedia-production-nc01" })).toThrow("namespace");
const invalidRepositoryDocument = pacConfigDocument();
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-nc01");
const invalidRepository = (invalidRepositoryDocument.repositories as Array<Record<string, unknown>>).find((item) => item.id === "pikaoa-test-nc01");
if (invalidRepository === undefined) throw new Error("pikaoa repository fixture is required");
invalidRepository.cloneUrl = "https://gitea.example.invalid/mirrors/pikaoa.git";
const isolated = parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "selfmedia-nc01" });
expect(isolated.validationWarnings).toContainEqual({
code: "pac-unselected-repository-invalid",
object: { kind: "repository", id: "pikaoa-nc01" },
object: { kind: "repository", id: "pikaoa-test-nc01" },
consumer: null,
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-nc01",
configPath: "config/platform-infra/pipelines-as-code.yaml#repositories.pikaoa-test-nc01",
blocking: false,
evidence: expect.stringMatching(/^type=string,length=\d+,sha256=[0-9a-f]{64}$/u),
});
expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-nc01")).toBe(false);
expect(validPacConsumers(isolated).some((consumer) => consumer.repositoryRef === "pikaoa-test-nc01")).toBe(false);
expect(() => parsePacConfigDocument(invalidRepositoryDocument, { consumerId: "pikaoa-nc01" })).toThrow("cloneUrl");
const duplicateSelectedRepository = pacConfigDocument();
@@ -166,11 +166,23 @@ test("PaC apply checks the Gitea repository before dry-run return and cluster mu
const preflight = remote.indexOf("if ! repository_preflight", action);
const dryRun = remote.indexOf('if [ "$UNIDESK_PAC_DRY_RUN" = "1" ]', action);
const release = remote.indexOf(" apply_release", action);
const admission = remote.indexOf(" apply_admission_manifest", release);
const rbac = remote.indexOf(" rbac_tmp=$(mktemp)", admission);
expect(action).toBeGreaterThanOrEqual(0);
expect(preflight).toBeGreaterThan(action);
expect(dryRun).toBeGreaterThan(preflight);
expect(release).toBeGreaterThan(dryRun);
expect(admission).toBeGreaterThan(release);
expect(rbac).toBeGreaterThan(admission);
expect(remote).toContain('"admission":{"applied":%s,"ready":%s}');
expect(remote).toContain('"admissionProvenance":%s');
expect(remote).toContain('"error":"gitea-repository-missing"');
const statusAction = remote.slice(remote.indexOf("status_action() {"), remote.indexOf("history_action() {"));
expect(statusAction).toContain('[ "$bootstrap_ready" = "true" ]');
expect(statusAction).toContain("output.code = 'pac-consumer-bootstrap-not-ready'");
expect(statusAction).toContain("output.ok = false");
expect(statusAction).toContain("code: 'pac-admission-provenance-not-ready'");
expect(statusAction).toContain("blocking: false");
});
test("platform bootstrap help advertises the composite entry before low-level apply", async () => {
@@ -197,13 +209,42 @@ test("PaC bootstrap projects typed stages and compact JSON", () => {
const projection = projectPacBootstrapResult(result);
expect(projection.ok).toBe(true);
expect(JSON.stringify(projection)).not.toContain("pipelinesAsCodeApply");
expect((projection.stages as Record<string, unknown>[]).map((item) => item.id)).toEqual(["gitea-init", "pac-controller", "tekton-consumer", "gitops-argo"]);
expect((projection.stages as Record<string, unknown>[]).map((item) => item.id)).toEqual(["gitea-init", "pac-controller", "pac-admission", "tekton-consumer", "gitops-argo"]);
const rendered = renderPacBootstrap(result).renderedText;
expect(rendered).toContain("PRECONDITIONS");
expect(rendered).toContain("yaml-repository-exact-match");
expect(rendered).toContain("confirm:");
});
test("PaC bootstrap keeps admission drift as a non-blocking warning", () => {
const result = {
ok: true,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: true,
mode: "confirmed",
target: { id: "NC01" },
consumer: { id: "widgets", node: "NC01", lane: "v01" },
repository: { id: "widgets", namespace: "ci" },
mirrorRepository: { key: mirror.key, upstreamRepository: mirror.upstream.repository, upstreamBranch: mirror.upstream.branch, owner: mirror.gitea.owner, repo: mirror.gitea.name },
githubPreflight: { ok: true, mutation: false, repositories: [{ key: mirror.key, code: "github-upstream-available", ok: true }] },
giteaBootstrap: { ok: true, mutation: false, mode: "skipped", blockers: [] },
pipelinesAsCodeApply: {
ok: true,
mutation: true,
mode: "confirmed",
remote: {
ok: true,
admission: { applied: true, ready: false },
admissionProvenance: { policyName: "unidesk-pac-delivery-provenance-v2", reasons: ["policy-generation-not-observed"] },
},
},
};
const projection = projectPacBootstrapResult(result);
expect(projection.ok).toBe(true);
expect((projection.stages as Record<string, unknown>[]).find((item) => item.id === "pac-admission")).toMatchObject({ ok: true, state: "warning", blocking: false });
expect(projection.warnings).toEqual([expect.objectContaining({ code: "pac-admission-provenance-not-ready", blocking: false })]);
});
test("PaC bootstrap returns fix-yaml next for zero YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: no exact match"));
expect(projectPacBootstrapResult(result)).toBe(result);
@@ -237,6 +278,6 @@ test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stage
});
expect((projection.blockers as Record<string, unknown>[])).toEqual([{ code: "github-repository-not-found-or-no-access", stage: "github-upstream", reason: "repository unavailable" }]);
expect((projection.preconditions as Record<string, unknown>[])[2]).toMatchObject({ ok: null, code: "gitea-bootstrap-skipped" });
expect((projection.stages as Record<string, unknown>[]).map((item) => item.state)).toEqual(["skipped", "skipped", "skipped", "skipped"]);
expect((projection.stages as Record<string, unknown>[]).map((item) => item.state)).toEqual(["skipped", "skipped", "skipped", "skipped", "skipped"]);
expect((projection.next as Record<string, unknown>).kind).toBe("read-only-status");
});
@@ -69,6 +69,8 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
const gitea = record(result.giteaBootstrap);
const apply = record(result.pipelinesAsCodeApply);
const remote = record(apply.remote);
const admission = record(remote.admission);
const admissionProvenance = record(remote.admissionProvenance);
const blockers = bootstrapBlockers(githubPreflight, gitea, apply, remote);
const dryRun = result.mode === "dry-run";
const repositoryMissing = remote.error === "gitea-repository-missing";
@@ -77,6 +79,16 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
const giteaSkipped = gitea.mode === "skipped";
const applySkipped = apply.mode === "skipped";
const applyReady = apply.ok === true || (dryRun && repositoryMissing);
const warnings = [
...records(result.warnings),
...(applySkipped || dryRun || repositoryMissing || admission.ready === true ? [] : [{
object: { kind: "ValidatingAdmissionPolicy", id: stringValue(admissionProvenance.policyName, "PaC admission") },
blocking: false,
code: "pac-admission-provenance-not-ready",
configPath: "config/platform-infra/pipelines-as-code.yaml#deliveryProvenance.admission",
reasons: Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons : [],
}]),
];
const next = bootstrapNext(result, blockers);
return {
ok: blockers.length === 0 && giteaReady && applyReady,
@@ -101,11 +113,12 @@ export function projectPacBootstrapResult(result: Record<string, unknown>): Reco
stages: [
{ id: "gitea-init", ok: giteaSkipped ? null : giteaReady, state: giteaSkipped ? "skipped" : giteaReady ? (dryRun ? "planned" : "ready") : "failed", mutation: gitea.mutation === true },
{ id: "pac-controller", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
{ id: "pac-admission", ok: applySkipped ? null : true, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : dryRun ? "planned" : admission.ready === true ? "ready" : "warning", blocking: false, mutation: admission.applied === true },
{ id: "tekton-consumer", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
{ id: "gitops-argo", ok: applySkipped ? null : applyReady, state: applySkipped ? "skipped" : repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
],
repository: { id: repository.id, namespace: repository.namespace },
warnings: result.warnings,
warnings,
blockers,
next,
valuesPrinted: false,
@@ -284,6 +284,13 @@ apply_release() {
rm -f "$tmp"
}
apply_admission_manifest() {
tmp=$(mktemp)
printf '%s' "$UNIDESK_PAC_ADMISSION_MANIFEST_B64" | base64 -d > "$tmp"
kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-admission-bootstrap" -f "$tmp" >/dev/null
rm -f "$tmp"
}
wait_ready() {
timeout="${UNIDESK_PAC_WAIT_TIMEOUT_SECONDS:-55}"
end=$(( $(now_epoch) + timeout ))
@@ -313,6 +320,11 @@ apply_action() {
return
fi
apply_release
admission_applied=false
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
apply_admission_manifest
admission_applied=true
fi
kubectl create ns "$UNIDESK_PAC_TARGET_NAMESPACE" --dry-run=client -o yaml | kubectl apply -f - >/dev/null
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
rbac_tmp=$(mktemp)
@@ -333,7 +345,11 @@ apply_action() {
{ printf '%s' "$token"; printf '\0'; printf '%s' "$UNIDESK_PAC_WEBHOOK_SECRET"; } \
| pac_repository_secret_manifest \
| kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER-repository-secret" -f - >/dev/null
repository_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
if [ "${UNIDESK_PAC_REPOSITORY_APPLY:-1}" = "1" ]; then
repository_manifest | kubectl apply --server-side --force-conflicts --field-manager="$UNIDESK_PAC_FIELD_MANAGER" -f - >/dev/null
else
kubectl -n "$UNIDESK_PAC_TARGET_NAMESPACE" get repository.pipelinesascode.tekton.dev "$UNIDESK_PAC_REPOSITORY_NAME" >/dev/null
fi
argo_bootstrap=false
if [ -n "${UNIDESK_PAC_ARGO_BOOTSTRAP_MANIFEST_B64:-}" ]; then
if [ -n "${UNIDESK_PAC_ARGO_REPOSITORY_SECRET_NAME:-}" ]; then
@@ -351,7 +367,14 @@ apply_action() {
hook_id=$(ensure_webhook)
ready=false
if wait_ready; then ready=true; fi
printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ready" "$ready" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
prepare_admission_provenance_state
admission_ready=true
if [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" = "1" ]; then
admission_ready=$(printf '%s' "$UNIDESK_PAC_ADMISSION_STATE_JSON" | node -e 'let input="";process.stdin.on("data",chunk=>input+=chunk);process.stdin.on("end",()=>process.stdout.write(JSON.parse(input).ready===true?"true":"false"));')
fi
ok=false
if [ "$ready" = true ]; then ok=true; fi
printf '{"ok":%s,"mode":"confirmed","mutation":true,"controllerReady":%s,"admission":{"applied":%s,"ready":%s},"admissionProvenance":%s,"argoBootstrap":%s,"preflight":{"repositoryExists":true,"httpStatus":"2xx","repository":"%s/%s","valuesPrinted":false},"webhook":{"present":true,"id":"%s","url":"%s"},"repository":"%s","namespace":"%s","valuesPrinted":false}\n' "$ok" "$ready" "$admission_applied" "$admission_ready" "$UNIDESK_PAC_ADMISSION_STATE_JSON" "$argo_bootstrap" "$(json_string "$UNIDESK_PAC_GITEA_OWNER")" "$(json_string "$UNIDESK_PAC_GITEA_REPO")" "$(json_string "$hook_id")" "$(json_string "$UNIDESK_PAC_WEBHOOK_URL")" "$(json_string "$UNIDESK_PAC_REPOSITORY_NAME")" "$(json_string "$UNIDESK_PAC_TARGET_NAMESPACE")"
}
condition_status() {
@@ -590,7 +613,7 @@ function defaultCanMutate(namespace) {
function admissionStateForConsumer(consumer) {
const expected = consumer.deliveryProvenance;
if (!expected || expected.required !== true) return { configured: false, required: false, ready: null, reasons: ['consumer-admission-provenance-not-required'], valuesPrinted: false };
if (!expected || expected.required !== true) return { configured: false, required: false, ready: null, blocking: false, warning: false, warnings: [], reasons: ['consumer-admission-provenance-not-required'], valuesPrinted: false };
admissionPolicy ||= kubectlJson(['get', 'validatingadmissionpolicy', expected.policyName, '-o', 'json'], {}, 'admission-policy');
admissionBinding ||= kubectlJson(['get', 'validatingadmissionpolicybinding', expected.bindingName, '-o', 'json'], {}, 'admission-binding');
if (!admissionRbac.has(consumer.namespace)) {
@@ -1527,6 +1550,9 @@ if (!passwordPresent) reasons.push('argocd-repository-secret-password-missing');
process.stdout.write(JSON.stringify({
configured: runnerConfigured || argoConfigured,
ready: reasons.length === 0,
blocking: false,
warning: reasons.length > 0,
warnings: reasons,
runner: {
configured: runnerConfigured,
serviceAccount: runnerName || null,
@@ -1563,7 +1589,6 @@ status_action() {
repository_condition=$(condition_status "$UNIDESK_PAC_TARGET_NAMESPACE" repository "$UNIDESK_PAC_REPOSITORY_NAME")
prepare_admission_provenance_state
consumer_bootstrap=$(consumer_bootstrap_state)
bootstrap_ready=$(UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" node -e 'const s=JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_STATE||"{}"); process.stdout.write(s.configured===true&&s.ready!==true?"false":"true")')
pipelines=$(pipeline_rows)
latest=$(printf '%s' "$pipelines" | node -e 'const fs=require("fs"); const a=JSON.parse(fs.readFileSync(0,"utf8")||"[]"); process.stdout.write(a[0]?.name||"")')
export UNIDESK_PAC_TARGET_PIPELINERUN="$latest"
@@ -1587,39 +1612,30 @@ NODE
)
runtime=$(json_normalize "$(runtime_summary)")
diagnostics=$(cicd_diagnostics "$pipelines" "$artifact" "$argo" "$runtime")
admission_ready=$(UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" node -e 'const s=JSON.parse(process.env.UNIDESK_PAC_STATE||"{}"); process.stdout.write(s.ready===true?"true":"false")')
diagnostics=$(UNIDESK_PAC_DIAGNOSTICS="$diagnostics" UNIDESK_PAC_STATE="$UNIDESK_PAC_ADMISSION_STATE_JSON" UNIDESK_PAC_BOOTSTRAP_STATE="$consumer_bootstrap" node <<'NODE'
const diagnostics = JSON.parse(process.env.UNIDESK_PAC_DIAGNOSTICS || '{}');
const admissionProvenance = JSON.parse(process.env.UNIDESK_PAC_STATE || '{}');
const consumerBootstrap = JSON.parse(process.env.UNIDESK_PAC_BOOTSTRAP_STATE || '{}');
const output = { ...diagnostics, admissionProvenance, consumerBootstrap, valuesPrinted: false };
if (process.env.UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED === '1' && admissionProvenance.ready !== true) {
process.stdout.write(JSON.stringify({
...diagnostics,
ok: false,
code: 'pac-admission-provenance-not-ready',
phase: 'admission-provenance-not-ready',
hint: 'required PaC admission policy, binding, desired RBAC, or live default-SA mutation gate is not ready',
admissionProvenance,
valuesPrinted: false,
}));
} else if (consumerBootstrap.configured === true && consumerBootstrap.ready !== true) {
process.stdout.write(JSON.stringify({
...diagnostics,
ok: false,
code: 'pac-consumer-bootstrap-not-ready',
phase: 'consumer-bootstrap-not-ready',
hint: 'declared runner ServiceAccount/RoleBinding or Argo repository credential is missing or drifted',
admissionProvenance,
consumerBootstrap,
valuesPrinted: false,
}));
} else {
process.stdout.write(JSON.stringify({ ...diagnostics, admissionProvenance, consumerBootstrap, valuesPrinted: false }));
output.warnings = [...(Array.isArray(output.warnings) ? output.warnings : []), {
code: 'pac-admission-provenance-not-ready',
blocking: false,
hint: 'PaC admission policy, binding, desired RBAC, or live default-SA observation is drifted; delivery continues',
reasons: Array.isArray(admissionProvenance.reasons) ? admissionProvenance.reasons : [],
}];
}
if (consumerBootstrap.configured === true && consumerBootstrap.ready !== true) {
output.ok = false;
output.code = 'pac-consumer-bootstrap-not-ready';
output.phase = 'consumer-bootstrap-not-ready';
output.hint = 'declared runner ServiceAccount/RoleBinding or Argo repository credential is missing or drifted';
}
process.stdout.write(JSON.stringify(output));
NODE
)
printf '{"ok":%s,"crdPresent":%s,"controllerReady":"%s","admissionProvenance":%s,"consumerBootstrap":%s,"repository":{"name":"%s","repo":"%s/%s","url":"%s","condition":"%s"},"repositoryCondition":"%s","webhooks":%s,"pipelineRuns":%s,"taskRuns":%s,"artifact":%s,"argo":%s,"runtime":%s,"diagnostics":%s,"valuesPrinted":false}\n' \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && [ "$bootstrap_ready" = "true" ] && { [ "${UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED:-0}" != "1" ] || [ "$admission_ready" = "true" ]; } && echo true || echo false )" \
"$( [ -n "$crd" ] && [ "$controller_ready" != "0/0" ] && [ "$bootstrap_ready" = "true" ] && echo true || echo false )" \
"$( [ -n "$crd" ] && echo true || echo false )" \
"$(json_string "$controller_ready")" \
"$UNIDESK_PAC_ADMISSION_STATE_JSON" \
@@ -17,6 +17,9 @@ import {
canonicalizePacPipelineSpec,
firstPacSourceArtifactDrift,
pacSourceArtifactSafeError,
pacSourceArtifactProvenanceFromManifest,
pipelineRunLabels,
pipelineRunWorkspaces,
pacValueEvidence,
pacSourceArtifactYaml,
optionalPipelineRunWorkspaces,
@@ -96,6 +99,46 @@ function runtimeInput(commit: string | null = sourceCommit): Record<string, unkn
}
describe("PaC source artifact CLI contract", () => {
test("accepts PikaOA test runtime provenance", () => {
const expected = {
configRef: "config/pikaoa.yaml#testRuntime.targets.NC01",
effectiveConfigSha256: `sha256:${"a".repeat(64)}`,
renderer: "pikaoa-test-runtime",
mode: "embedded-pipeline-spec",
} as const;
const manifest = {
metadata: { annotations: pipelineProvenanceAnnotations(expected) },
};
expect(pacSourceArtifactProvenanceFromManifest(manifest)).toEqual(expected);
});
test("uses PikaOA-specific PipelineRun labels", () => {
const binding = { consumer: { id: "pikaoa-test-nc01", node: "NC01" } } as Parameters<typeof pipelineRunLabels>[0];
expect(pipelineRunLabels(binding, "pikaoa")).toEqual({
"app.kubernetes.io/name": "pikaoa-test-nc01-pac",
"app.kubernetes.io/part-of": "pikaoa",
"unidesk.ai/source-commit": "{{ revision }}",
"pikaoa.unidesk.io/source-commit": "{{ revision }}",
"pikaoa.unidesk.io/trigger": "pipelines-as-code",
});
});
test("maps the PikaOA shared workspace to the declared RWO PVC size", () => {
const binding = {
consumer: { sourceArtifact: { renderer: "pikaoa-test-runtime" } },
repository: { params: { workspace_pvc_size: "8Gi" } },
} as Parameters<typeof pipelineRunWorkspaces>[0];
expect(pipelineRunWorkspaces(binding, { workspaces: [{ name: "workspace" }] })).toEqual([{
name: "workspace",
volumeClaimTemplate: {
spec: {
accessModes: ["ReadWriteOnce"],
resources: { requests: { storage: "8Gi" } },
},
},
}]);
});
test("omits empty PipelineRun workspaces and preserves declared bindings", () => {
expect(optionalPipelineRunWorkspaces([])).toEqual({});
const workspaces = [{ name: "source", emptyDir: {} }];
@@ -19,9 +19,10 @@ import { stableJsonSha256, stableJsonValue } from "./stable-json";
import { pipelineProvenanceAnnotations, pipelineProvenanceFromManifest, withPipelineProvenanceAnnotations } from "./pipeline-provenance";
import { renderSub2RankDesiredPipeline, sub2RankOwningSourceRemote } from "./platform-infra-sub2rank-pipeline";
import { renderSelfMediaDesiredPipeline, selfMediaSourceWorktreeRemote } from "./selfmedia-delivery-renderer";
import { renderPikaoaTestDesiredPipeline, pikaoaReleaseSourceWorktreeRemote, pikaoaTestSourceWorktreeRemote } from "./pikaoa-test-delivery-renderer";
export type PacSourceArtifactMode = "embedded-pipeline-spec" | "remote-pipeline-annotation";
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime";
export type PacSourceArtifactRenderer = "agentrun-control-plane" | "hwlab-runtime-lane" | "sub2rank-platform-service" | "selfmedia-runtime" | "pikaoa-test-runtime" | "pikaoa-release-runtime";
export type PacSourceArtifactAction = "plan" | "check" | "write" | "status" | "verify-runtime";
export type PacSourceArtifactRuntimeAlignment = "not-requested" | "aligned" | "drifted" | "missing" | "unavailable";
@@ -499,11 +500,13 @@ function renderDesiredArtifact(binding: PacSourceArtifactBinding, sourceWorktree
? renderHwlabDesiredPipeline(binding, sourceWorktree)
: sourceArtifact.renderer === "sub2rank-platform-service"
? renderSub2RankDesiredPipeline(binding)
: renderSelfMediaPipeline(binding, sourceWorktree);
: sourceArtifact.renderer === "selfmedia-runtime"
? renderSelfMediaPipeline(binding, sourceWorktree)
: renderPikaoaTestPipeline(binding, sourceWorktree);
const pipeline = sourceArtifact.renderer === "hwlab-runtime-lane"
? rendered.pipeline
: withPipelineProvenanceAnnotations(rendered.pipeline, rendered.provenance);
if (!provenanceEquals(provenanceFromManifest(pipeline), rendered.provenance)) {
if (!provenanceEquals(pacSourceArtifactProvenanceFromManifest(pipeline), rendered.provenance)) {
throw new Error(`${sourceArtifact.renderer} did not render the declared Pipeline provenance contract`);
}
const desiredSpec = requiredRecord(pipeline.spec, "rendered Pipeline.spec");
@@ -583,6 +586,19 @@ function renderSelfMediaPipeline(binding: PacSourceArtifactBinding, sourceWorktr
};
}
function renderPikaoaTestPipeline(binding: PacSourceArtifactBinding, sourceWorktree: string): { pipeline: Record<string, unknown>; provenance: PacSourceArtifactProvenance } {
const rendered = renderPikaoaTestDesiredPipeline(binding, sourceWorktree);
return {
pipeline: rendered.pipeline,
provenance: {
configRef: rendered.configRef,
effectiveConfigSha256: rendered.effectiveConfigSha256,
renderer: binding.consumer.sourceArtifact.renderer,
mode: "embedded-pipeline-spec",
},
};
}
function renderHwlabPipelineFromOwningYaml(spec: HwlabRuntimeLaneSpec, sourceWorktree: string): Record<string, unknown> {
const temporaryRoot = mkdtempSync(resolve(tmpdir(), "unidesk-pac-source-artifact-"));
const temporarySource = resolve(temporaryRoot, "source");
@@ -659,7 +675,9 @@ function embeddedPipelineRun(binding: PacSourceArtifactBinding, desiredSpec: Rec
? "sub2rank"
: provenance.renderer === "selfmedia-runtime"
? "selfmedia"
: "agentrun",
: provenance.renderer === "pikaoa-test-runtime"
? "pikaoa"
: "agentrun",
),
},
spec: {
@@ -732,7 +750,7 @@ function pipelineRunAnnotations(binding: PacSourceArtifactBinding, provenance: P
};
}
function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun" | "hwlab" | "sub2rank" | "selfmedia"): Record<string, string> {
export function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun" | "hwlab" | "sub2rank" | "selfmedia" | "pikaoa"): Record<string, string> {
if (partOf === "agentrun") {
return {
"app.kubernetes.io/part-of": "agentrun",
@@ -751,6 +769,15 @@ function pipelineRunLabels(binding: PacSourceArtifactBinding, partOf: "agentrun"
"selfmedia.pikapython.com/trigger": "pipelines-as-code",
};
}
if (partOf === "pikaoa") {
return {
"app.kubernetes.io/name": `${binding.consumer.id}-pac`,
"app.kubernetes.io/part-of": "pikaoa",
"unidesk.ai/source-commit": "{{ revision }}",
"pikaoa.unidesk.io/source-commit": "{{ revision }}",
"pikaoa.unidesk.io/trigger": "pipelines-as-code",
};
}
if (partOf === "sub2rank") {
return {
"app.kubernetes.io/name": "sub2rank",
@@ -825,7 +852,7 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
if (name === "source-stage-ref") return { name, value: "{{ source_snapshot_prefix }}/{{ revision }}" };
if (name === "git-url") return { name, value: "{{ repo_url }}" };
const repositoryParam = name.replaceAll("-", "_");
if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" && Object.prototype.hasOwnProperty.call(param, "default")) {
if ((binding.consumer.sourceArtifact.renderer === "selfmedia-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") && Object.prototype.hasOwnProperty.call(param, "default")) {
return { name, value: param.default };
}
if (Object.prototype.hasOwnProperty.call(binding.repository.params, repositoryParam)) {
@@ -836,11 +863,11 @@ function pipelineRunParams(binding: PacSourceArtifactBinding, desiredSpec: Recor
});
}
function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
export function pipelineRunWorkspaces(binding: PacSourceArtifactBinding, desiredSpec: Record<string, unknown>): Record<string, unknown>[] {
if (desiredSpec.workspaces === undefined) return [];
return arrayRecords(desiredSpec.workspaces, "Pipeline.spec.workspaces").map((workspace) => {
const name = requiredString(workspace.name, "Pipeline.spec.workspaces[].name");
if (name === "source") {
if (name === "source" || (name === "workspace" && (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime" || binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime"))) {
return {
name,
volumeClaimTemplate: {
@@ -875,9 +902,9 @@ function inspectSourceArtifact(sourceWorktree: string, binding: PacSourceArtifac
const pipelineDrift = firstPacSourceArtifactDrift(desired.pipeline, actualPipeline);
const pipelineRunDrift = firstPacSourceArtifactDrift(desired.pipelineRun, actualPipelineRun);
const actualProvenance = sourceArtifact.mode === "remote-pipeline-annotation"
? provenanceFromManifest(actualPipeline)
: provenanceFromManifest(actualPipelineRun);
const wrapperProvenance = provenanceFromManifest(actualPipelineRun);
? pacSourceArtifactProvenanceFromManifest(actualPipeline)
: pacSourceArtifactProvenanceFromManifest(actualPipelineRun);
const wrapperProvenance = pacSourceArtifactProvenanceFromManifest(actualPipelineRun);
const provenanceAligned = provenanceEquals(actualProvenance, desired.provenance) && provenanceEquals(wrapperProvenance, desired.provenance);
const aligned = pipelineDrift === null && pipelineRunDrift === null && provenanceAligned;
return {
@@ -918,6 +945,8 @@ function owningSourceRemote(binding: PacSourceArtifactBinding): string {
}
if (binding.consumer.sourceArtifact.renderer === "sub2rank-platform-service") return sub2RankOwningSourceRemote();
if (binding.consumer.sourceArtifact.renderer === "selfmedia-runtime") return selfMediaSourceWorktreeRemote(binding.consumer.node);
if (binding.consumer.sourceArtifact.renderer === "pikaoa-test-runtime") return pikaoaTestSourceWorktreeRemote(binding.consumer.node);
if (binding.consumer.sourceArtifact.renderer === "pikaoa-release-runtime") return pikaoaReleaseSourceWorktreeRemote(binding.consumer.node);
if (!isHwlabRuntimeLane(binding.consumer.lane)) throw new PacSourceArtifactError("owning-source-lane-unresolved");
return hwlabRuntimeLaneSpecForNode(binding.consumer.lane, binding.consumer.node).gitUrl;
}
@@ -1030,13 +1059,15 @@ function assertPipelineIdentity(pipeline: Record<string, unknown>, binding: PacS
if (metadata.namespace !== binding.consumer.namespace) throw new Error(`rendered Pipeline namespace ${String(metadata.namespace)} does not match consumer ${binding.consumer.namespace}`);
}
function provenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
export function pacSourceArtifactProvenanceFromManifest(manifest: Record<string, unknown>): PacSourceArtifactProvenance | null {
const provenance = pipelineProvenanceFromManifest(manifest);
if (provenance === null) return null;
if (provenance.renderer !== "agentrun-control-plane"
&& provenance.renderer !== "hwlab-runtime-lane"
&& provenance.renderer !== "sub2rank-platform-service"
&& provenance.renderer !== "selfmedia-runtime") return null;
&& provenance.renderer !== "selfmedia-runtime"
&& provenance.renderer !== "pikaoa-test-runtime"
&& provenance.renderer !== "pikaoa-release-runtime") return null;
if (provenance.mode !== "embedded-pipeline-spec" && provenance.mode !== "remote-pipeline-annotation") return null;
return provenance as PacSourceArtifactProvenance;
}
@@ -28,7 +28,7 @@ import {
type PacSourceArtifactRuntimeObservation,
type PacSourceArtifactSpec,
} from "./platform-infra-pipelines-as-code-source-artifact";
import { pacAdmissionDesiredIdentity } from "./platform-infra-pac-provenance";
import { pacAdmissionDesiredIdentity, renderPacAdmissionDesiredFragment } from "./platform-infra-pac-provenance";
import { pacConsumerRbacDesiredIdentity, renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac";
import { runGiteaMirrorBootstrapPreflight, runPlatformInfraGiteaCommand } from "./platform-infra-gitea";
import { readGiteaConfig } from "./platform-infra-gitea-config";
@@ -801,7 +801,7 @@ function parseConsumerDeliveryProvenance(value: Record<string, unknown>, path: s
function parseSourceArtifact(value: Record<string, unknown>, path: string): PacSourceArtifactSpec {
const mode = y.enumField(value, "mode", path, ["embedded-pipeline-spec", "remote-pipeline-annotation"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime"] as const);
const renderer = y.enumField(value, "renderer", path, ["agentrun-control-plane", "hwlab-runtime-lane", "sub2rank-platform-service", "selfmedia-runtime", "pikaoa-test-runtime", "pikaoa-release-runtime"] as const);
const pipelineRunPath = sourceArtifactPath(y.stringField(value, "pipelineRunPath", path), `${path}.pipelineRunPath`);
const pipelinePath = value.pipelinePath === undefined ? null : sourceArtifactPath(y.stringField(value, "pipelinePath", path), `${path}.pipelinePath`);
const taskRunTemplate = y.objectField(value, "taskRunTemplate", path);
@@ -811,6 +811,8 @@ function parseSourceArtifact(value: Record<string, unknown>, path: string): PacS
if (renderer === "hwlab-runtime-lane" && mode !== "remote-pipeline-annotation") throw new Error(`${path}.renderer hwlab-runtime-lane requires remote-pipeline-annotation`);
if (renderer === "sub2rank-platform-service" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer sub2rank-platform-service requires embedded-pipeline-spec`);
if (renderer === "selfmedia-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer selfmedia-runtime requires embedded-pipeline-spec`);
if (renderer === "pikaoa-test-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-test-runtime requires embedded-pipeline-spec`);
if (renderer === "pikaoa-release-runtime" && mode !== "embedded-pipeline-spec") throw new Error(`${path}.renderer pikaoa-release-runtime requires embedded-pipeline-spec`);
return {
mode,
renderer,
@@ -1103,10 +1105,10 @@ function validateConsumerConfig(
if (consumer.sourceArtifact?.renderer === "sub2rank-platform-service" && consumer.runnerServiceAccount === null) {
throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for sub2rank-platform-service`);
}
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime") {
if (consumer.runnerServiceAccount === null) throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for selfmedia-runtime`);
if (consumer.sourceArtifact?.renderer === "selfmedia-runtime" || consumer.sourceArtifact?.renderer === "pikaoa-test-runtime") {
if (consumer.runnerServiceAccount === null) throw new Error(`${configLabel}.consumers.${consumer.id}.runnerServiceAccount is required for private GitOps runtime renderers`);
if (consumer.argoBootstrap?.repositoryCredential === null || consumer.argoBootstrap?.repositoryCredential === undefined) {
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private selfmedia repository`);
throw new Error(`${configLabel}.consumers.${consumer.id}.argoBootstrap.repositoryCredential is required for the private repository`);
}
if (consumer.closeoutGitOpsMirrorFlush) throw new Error(`${configLabel}.consumers.${consumer.id}.closeoutGitOpsMirrorFlush must be false for Gitea writeback GitOps`);
if (params.gitops_secret_name !== repository.secretName) throw new Error(`${configLabel}.consumers.${consumer.id}.effective params.gitops_secret_name must match repository secretName`);
@@ -1720,6 +1722,7 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
const admissionIdentity = pacAdmissionDesiredIdentity(target.id);
const rbacIdentity = pacConsumerRbacDesiredIdentity(target.id);
const consumerConfig = historyConsumerConfig(pac, consumer);
const repositoryOwner = pac.consumers.find((item) => item.repositoryRef === repository.id);
const env: Record<string, string> = {
UNIDESK_PAC_ACTION: action,
UNIDESK_PAC_EVALUATOR_B64: Buffer.from(readFileSync(evaluatorFile, "utf8"), "utf8").toString("base64"),
@@ -1741,6 +1744,7 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
UNIDESK_PAC_WEBHOOK_URL: webhookUrl,
UNIDESK_PAC_WEBHOOK_SECRET: secrets.webhookSecret,
UNIDESK_PAC_REPOSITORY_NAME: repository.name,
UNIDESK_PAC_REPOSITORY_APPLY: repositoryOwner?.id === consumer.id ? "1" : "0",
UNIDESK_PAC_REPOSITORY_URL: repository.url,
UNIDESK_PAC_SECRET_NAME: repository.secretName,
UNIDESK_PAC_TOKEN_KEY: repository.tokenKey,
@@ -1753,6 +1757,7 @@ function remoteScript(action: "apply" | "status" | "history" | "debug-step", pac
UNIDESK_PAC_PIPELINE_RUN_PREFIX: consumer.pipelineRunPrefix,
UNIDESK_PAC_CONSUMER_CONFIG_B64: Buffer.from(JSON.stringify(consumerConfig), "utf8").toString("base64"),
UNIDESK_PAC_DELIVERY_PROVENANCE_REQUIRED: consumer.deliveryProvenance?.required === true ? "1" : "0",
UNIDESK_PAC_ADMISSION_MANIFEST_B64: Buffer.from(renderPacAdmissionDesiredFragment(target.id), "utf8").toString("base64"),
UNIDESK_PAC_CONSUMER_RBAC_MANIFEST_B64: Buffer.from(renderPacConsumerRbacDesiredFragment(target.id), "utf8").toString("base64"),
UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_MANIFEST_B64: Buffer.from(runnerServiceAccountManifest(consumer), "utf8").toString("base64"),
UNIDESK_PAC_RUNNER_SERVICE_ACCOUNT_NAME: consumer.runnerServiceAccount?.name ?? "",