Files
pikasTech-unidesk/scripts/src/platform-infra-pipelines-as-code.ts
T
2026-07-05 12:11:18 +00:00

747 lines
32 KiB
TypeScript

import { createHash, randomBytes } from "node:crypto";
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import type { UniDeskConfig } from "./config";
import { rootPath } from "./config";
import type { RenderedCliResult } from "./output";
import {
capture,
compactCapture,
createYamlFieldReader,
parseJsonOutput,
readYamlRecord,
shQuote,
sha256Fingerprint,
} from "./platform-infra-ops-library";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
const remoteScriptFile = rootPath("scripts", "src", "platform-infra-pipelines-as-code-remote.sh");
const fieldManager = "unidesk-platform-infra-pipelines-as-code";
const y = createYamlFieldReader(configLabel);
interface PacTarget {
id: string;
route: string;
namespace: string;
role: string;
enabled: boolean;
}
interface PacConfig {
version: number;
kind: "platform-infra-pipelines-as-code";
metadata: {
id: string;
owner: string;
spec: string;
relatedIssues: number[];
};
defaults: {
targetId: string;
};
release: {
version: string;
manifestUrl: string;
namespace: string;
controllerServiceName: string;
controllerServicePort: number;
waitTimeoutSeconds: number;
};
targets: PacTarget[];
gitea: {
configRef: string;
internalBaseUrl: string;
admin: {
sourceRoot: string;
sourceRef: string;
format: "line-pair";
usernameLine: number;
passwordLine: number;
apiUsername: string;
};
webhook: {
secretRoot: string;
secretSourceRef: string;
events: string[];
branch: string;
};
};
repository: {
name: string;
namespace: string;
providerType: "gitea";
url: string;
cloneUrl: string;
owner: string;
repo: string;
secretName: string;
tokenKey: string;
webhookSecretKey: string;
concurrencyLimit: number;
params: Record<string, string>;
};
consumer: {
node: string;
lane: string;
namespace: string;
pipeline: string;
pipelineRunPrefix: string;
argoNamespace: string;
argoApplication: string;
};
}
interface CommonOptions {
targetId: string | null;
full: boolean;
raw: boolean;
}
interface ApplyOptions extends CommonOptions {
confirm: boolean;
dryRun: boolean;
wait: boolean;
}
interface WebhookTestOptions extends CommonOptions {
confirm: boolean;
}
interface SecretMaterial {
adminUsername: string;
adminPassword: string;
adminFingerprint: string;
webhookSecret: string;
webhookFingerprint: string;
webhookPath: string;
}
export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown> | RenderedCliResult> {
const [action = "plan"] = args;
if (action === "help" || action === "--help") return help();
if (action === "plan") {
const options = parseCommonOptions(args.slice(1));
const result = plan(options);
return options.full || options.raw ? result : renderPlan(result);
}
if (action === "apply") {
const options = parseApplyOptions(args.slice(1));
const result = await apply(config, options);
return options.full || options.raw ? result : renderApply(result);
}
if (action === "status") {
const options = parseCommonOptions(args.slice(1));
const result = await status(config, options);
return options.full || options.raw ? result : renderStatus(result);
}
if (action === "webhook-test") {
const options = parseWebhookTestOptions(args.slice(1));
const result = await webhookTest(config, options);
return options.full || options.raw ? result : renderWebhookTest(result);
}
return { ok: false, error: "unsupported-platform-infra-pipelines-as-code-command", args, help: help() };
}
function help(): Record<string, unknown> {
return {
command: "platform-infra pipelines-as-code plan|apply|status|webhook-test",
configTruth: configLabel,
usage: [
"bun scripts/cli.ts platform-infra pipelines-as-code plan --target JD01",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --dry-run",
"bun scripts/cli.ts platform-infra pipelines-as-code apply --target JD01 --confirm",
"bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--full|--raw]",
"bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target JD01 --confirm",
],
boundary: "Sole CI trigger path for GH-1552: Gitea webhook -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime.",
};
}
function readPacConfig(): PacConfig {
const root = readYamlRecord<Record<string, unknown>>(configFile, "platform-infra-pipelines-as-code");
const release = y.objectField(root, "release", "");
const gitea = y.objectField(root, "gitea", "");
const admin = y.objectField(gitea, "admin", "gitea");
const webhook = y.objectField(gitea, "webhook", "gitea");
const repository = y.objectField(root, "repository", "");
const consumer = y.objectField(root, "consumer", "");
const parsed: PacConfig = {
version: y.integerField(root, "version", ""),
kind: "platform-infra-pipelines-as-code",
metadata: {
id: y.stringField(y.objectField(root, "metadata", ""), "id", "metadata"),
owner: y.stringField(y.objectField(root, "metadata", ""), "owner", "metadata"),
spec: y.stringField(y.objectField(root, "metadata", ""), "spec", "metadata"),
relatedIssues: y.numberArrayField(y.objectField(root, "metadata", ""), "relatedIssues", "metadata"),
},
defaults: {
targetId: y.stringField(y.objectField(root, "defaults", ""), "targetId", "defaults"),
},
release: {
version: y.stringField(release, "version", "release"),
manifestUrl: urlField(release, "manifestUrl", "release"),
namespace: y.kubernetesNameField(release, "namespace", "release"),
controllerServiceName: y.kubernetesNameField(release, "controllerServiceName", "release"),
controllerServicePort: y.portField(release, "controllerServicePort", "release"),
waitTimeoutSeconds: positiveInteger(release, "waitTimeoutSeconds", "release"),
},
targets: y.arrayOfRecords(root.targets, "targets").map(parseTarget),
gitea: {
configRef: y.stringField(gitea, "configRef", "gitea"),
internalBaseUrl: urlField(gitea, "internalBaseUrl", "gitea"),
admin: {
sourceRoot: y.absolutePathField(admin, "sourceRoot", "gitea.admin"),
sourceRef: y.sourceRefField(admin, "sourceRef", "gitea.admin"),
format: y.enumField(admin, "format", "gitea.admin", ["line-pair"] as const),
usernameLine: positiveInteger(admin, "usernameLine", "gitea.admin"),
passwordLine: positiveInteger(admin, "passwordLine", "gitea.admin"),
apiUsername: y.stringField(admin, "apiUsername", "gitea.admin"),
},
webhook: {
secretRoot: y.absolutePathField(webhook, "secretRoot", "gitea.webhook"),
secretSourceRef: y.sourceRefField(webhook, "secretSourceRef", "gitea.webhook"),
events: y.stringArrayField(webhook, "events", "gitea.webhook"),
branch: y.stringField(webhook, "branch", "gitea.webhook"),
},
},
repository: {
name: y.kubernetesNameField(repository, "name", "repository"),
namespace: y.kubernetesNameField(repository, "namespace", "repository"),
providerType: y.enumField(repository, "providerType", "repository", ["gitea"] as const),
url: urlField(repository, "url", "repository"),
cloneUrl: urlField(repository, "cloneUrl", "repository"),
owner: y.stringField(repository, "owner", "repository"),
repo: y.stringField(repository, "repo", "repository"),
secretName: y.kubernetesNameField(repository, "secretName", "repository"),
tokenKey: y.stringField(repository, "tokenKey", "repository"),
webhookSecretKey: y.stringField(repository, "webhookSecretKey", "repository"),
concurrencyLimit: positiveInteger(repository, "concurrencyLimit", "repository"),
params: stringRecord(y.objectField(repository, "params", "repository"), "repository.params"),
},
consumer: {
node: y.stringField(consumer, "node", "consumer"),
lane: y.stringField(consumer, "lane", "consumer"),
namespace: y.kubernetesNameField(consumer, "namespace", "consumer"),
pipeline: y.kubernetesNameField(consumer, "pipeline", "consumer"),
pipelineRunPrefix: y.stringField(consumer, "pipelineRunPrefix", "consumer"),
argoNamespace: y.kubernetesNameField(consumer, "argoNamespace", "consumer"),
argoApplication: y.kubernetesNameField(consumer, "argoApplication", "consumer"),
},
};
validateConfig(parsed);
return parsed;
}
function parseTarget(record: Record<string, unknown>, index: number): PacTarget {
const path = `targets[${index}]`;
return {
id: y.stringField(record, "id", path),
route: y.stringField(record, "route", path),
namespace: y.kubernetesNameField(record, "namespace", path),
role: y.stringField(record, "role", path),
enabled: y.booleanField(record, "enabled", path),
};
}
function validateConfig(config: PacConfig): void {
if (config.version !== 1) throw new Error(`${configLabel}.version must be 1`);
resolveTarget(config, config.defaults.targetId);
if (config.repository.providerType !== "gitea") throw new Error(`${configLabel}.repository.providerType must be gitea`);
if (config.release.waitTimeoutSeconds > 55) throw new Error(`${configLabel}.release.waitTimeoutSeconds must fit the 60s trans budget`);
if (config.repository.namespace !== config.consumer.namespace) throw new Error(`${configLabel}.repository.namespace must match consumer.namespace`);
if (new URL(config.repository.cloneUrl).origin !== new URL(config.gitea.internalBaseUrl).origin) throw new Error(`${configLabel}.repository.cloneUrl must use the configured internal Gitea base URL`);
if (!config.gitea.webhook.events.includes("push")) throw new Error(`${configLabel}.gitea.webhook.events must include push`);
}
function resolveTarget(config: PacConfig, targetId: string | null): PacTarget {
const id = targetId ?? config.defaults.targetId;
const target = config.targets.find((item) => item.id.toLowerCase() === id.toLowerCase());
if (target === undefined) throw new Error(`unknown Pipelines-as-Code target ${id}; known targets: ${config.targets.map((item) => item.id).join(", ")}`);
if (!target.enabled) throw new Error(`Pipelines-as-Code target ${target.id} is disabled in ${configLabel}`);
return target;
}
function plan(options: CommonOptions): Record<string, unknown> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const secrets = secretSummaries(pac);
return {
ok: true,
action: "platform-infra-pipelines-as-code-plan",
mutation: false,
target: targetSummary(target),
config: configSummary(pac),
release: pac.release,
repository: repositorySummary(pac),
secrets,
policy: policyChecks(pac),
next: nextCommands(target.id),
};
}
async function apply(config: UniDeskConfig, options: ApplyOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const secrets = ensureSecrets(pac, !options.dryRun, options.dryRun);
const releaseManifest = options.dryRun ? "" : await fetchReleaseManifest(pac);
const result = await capture(config, target.route, ["sh"], remoteScript("apply", pac, target, options, secrets, releaseManifest));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-pipelines-as-code-apply",
mutation: !options.dryRun,
mode: options.dryRun ? "dry-run" : "confirmed",
target: targetSummary(target),
config: compactConfigSummary(pac),
release: { version: pac.release.version, manifestUrl: pac.release.manifestUrl },
repository: repositorySummary(pac),
secrets: secretSummaries(pac),
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id),
};
}
async function status(config: UniDeskConfig, options: CommonOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
const secrets = ensureSecrets(pac, false);
const result = await capture(config, target.route, ["sh"], remoteScript("status", pac, target, { ...options, confirm: false, dryRun: true, wait: false }, secrets, ""));
const parsed = parseJsonOutput(result.stdout);
const summary = parsed === null ? null : statusSummary(parsed);
return {
ok: result.exitCode === 0 && summary?.ready === true,
action: "platform-infra-pipelines-as-code-status",
mutation: false,
target: targetSummary(target),
config: compactConfigSummary(pac),
summary,
remote: parsed === null ? compactCapture(result, { full: true }) : options.raw || options.full ? parsed : summary,
next: nextCommands(target.id),
};
}
async function webhookTest(config: UniDeskConfig, options: WebhookTestOptions): Promise<Record<string, unknown>> {
const pac = readPacConfig();
const target = resolveTarget(pac, options.targetId);
if (!options.confirm) return { ok: false, action: "platform-infra-pipelines-as-code-webhook-test", mutation: false, mode: "missing-confirm", error: "webhook-test requires --confirm" };
const secrets = ensureSecrets(pac, false);
const result = await capture(config, target.route, ["sh"], remoteScript("webhook-test", pac, target, { ...options, dryRun: false, wait: false }, secrets, ""));
const parsed = parseJsonOutput(result.stdout);
return {
ok: result.exitCode === 0 && parsed?.ok === true,
action: "platform-infra-pipelines-as-code-webhook-test",
mutation: true,
target: targetSummary(target),
remote: parsed ?? compactCapture(result, { full: true }),
next: nextCommands(target.id),
};
}
async function fetchReleaseManifest(pac: PacConfig): Promise<string> {
const response = await fetch(pac.release.manifestUrl);
if (!response.ok) throw new Error(`failed to fetch ${pac.release.manifestUrl}: HTTP ${response.status}`);
const text = await response.text();
if (!text.includes("repositories.pipelinesascode.tekton.dev")) throw new Error(`${pac.release.manifestUrl} did not contain the Repository CRD`);
return text;
}
function remoteScript(action: "apply" | "status" | "webhook-test", pac: PacConfig, target: PacTarget, options: ApplyOptions | WebhookTestOptions, secrets: SecretMaterial, releaseManifest: string): string {
const webhookUrl = `${pac.gitea.internalBaseUrl.replace(/\/+$/u, "").replace(/gitea-http\.[^.]+\.svc\.cluster\.local:3000/u, `${pac.release.controllerServiceName}.${pac.release.namespace}.svc.cluster.local:${pac.release.controllerServicePort}`)}`;
const env: Record<string, string> = {
UNIDESK_PAC_ACTION: action,
UNIDESK_PAC_FIELD_MANAGER: fieldManager,
UNIDESK_PAC_TARGET_ID: target.id,
UNIDESK_PAC_TARGET_NAMESPACE: target.namespace,
UNIDESK_PAC_RELEASE_NAMESPACE: pac.release.namespace,
UNIDESK_PAC_RELEASE_MANIFEST_B64: Buffer.from(releaseManifest, "utf8").toString("base64"),
UNIDESK_PAC_CONTROLLER_SERVICE_NAME: pac.release.controllerServiceName,
UNIDESK_PAC_WAIT_TIMEOUT_SECONDS: String(pac.release.waitTimeoutSeconds),
UNIDESK_PAC_DRY_RUN: "dryRun" in options && options.dryRun ? "1" : "0",
UNIDESK_PAC_GITEA_BASE_URL: pac.gitea.internalBaseUrl.replace(/\/+$/u, ""),
UNIDESK_PAC_GITEA_ADMIN_USERNAME: secrets.adminUsername,
UNIDESK_PAC_GITEA_ADMIN_PASSWORD: secrets.adminPassword,
UNIDESK_PAC_GITEA_API_USERNAME: pac.gitea.admin.apiUsername,
UNIDESK_PAC_GITEA_OWNER: pac.repository.owner,
UNIDESK_PAC_GITEA_REPO: pac.repository.repo,
UNIDESK_PAC_WEBHOOK_URL: webhookUrl,
UNIDESK_PAC_WEBHOOK_SECRET: secrets.webhookSecret,
UNIDESK_PAC_REPOSITORY_NAME: pac.repository.name,
UNIDESK_PAC_REPOSITORY_URL: pac.repository.url,
UNIDESK_PAC_SECRET_NAME: pac.repository.secretName,
UNIDESK_PAC_TOKEN_KEY: pac.repository.tokenKey,
UNIDESK_PAC_WEBHOOK_SECRET_KEY: pac.repository.webhookSecretKey,
UNIDESK_PAC_CONCURRENCY_LIMIT: String(pac.repository.concurrencyLimit),
UNIDESK_PAC_PARAMS_JSON: JSON.stringify(pac.repository.params),
UNIDESK_PAC_PIPELINE_RUN_PREFIX: pac.consumer.pipelineRunPrefix,
UNIDESK_PAC_ARGO_NAMESPACE: pac.consumer.argoNamespace,
UNIDESK_PAC_ARGO_APPLICATION: pac.consumer.argoApplication,
};
const exports = Object.entries(env).map(([key, value]) => `export ${key}=${shQuote(value)}`).join("\n");
return `${exports}\n${readFileSync(remoteScriptFile, "utf8")}`;
}
function credentialPath(root: string, sourceRef: string): string {
return sourceRef.startsWith("/") ? sourceRef : join(root, sourceRef);
}
function parseLinePair(path: string, usernameLine: number, passwordLine: number): { username: string; password: string } {
const lines = readFileSync(path, "utf8").split(/\r?\n/u);
const username = lines[usernameLine - 1]?.trim() ?? "";
const password = lines[passwordLine - 1]?.trim() ?? "";
if (username.length === 0 || password.length === 0) throw new Error(`${path} must contain username on line ${usernameLine} and password on line ${passwordLine}`);
return { username, password };
}
function ensureSecrets(pac: PacConfig, createWebhookSecret: boolean, allowMissingWebhookSecret = false): SecretMaterial {
const adminPath = credentialPath(pac.gitea.admin.sourceRoot, pac.gitea.admin.sourceRef);
if (!existsSync(adminPath)) throw new Error(`${pac.gitea.admin.sourceRef} is missing`);
const admin = parseLinePair(adminPath, pac.gitea.admin.usernameLine, pac.gitea.admin.passwordLine);
const webhookPath = credentialPath(pac.gitea.webhook.secretRoot, pac.gitea.webhook.secretSourceRef);
if (!existsSync(webhookPath)) {
if (allowMissingWebhookSecret) {
const placeholder = "dry-run-webhook-secret-placeholder";
return {
adminUsername: admin.username,
adminPassword: admin.password,
adminFingerprint: secretFingerprint(`${admin.username}\0${admin.password}`),
webhookSecret: placeholder,
webhookFingerprint: secretFingerprint(placeholder),
webhookPath,
};
}
if (!createWebhookSecret) throw new Error(`${pac.gitea.webhook.secretSourceRef} is missing; run apply --confirm to create it`);
mkdirSync(dirname(webhookPath), { recursive: true });
writeFileSync(webhookPath, `${randomBytes(32).toString("hex")}\n`, { encoding: "utf8", mode: 0o600 });
chmodSync(webhookPath, 0o600);
}
const webhookSecret = readFileSync(webhookPath, "utf8").trim();
if (webhookSecret.length < 32) throw new Error(`${pac.gitea.webhook.secretSourceRef} must contain at least 32 chars`);
return {
adminUsername: admin.username,
adminPassword: admin.password,
adminFingerprint: secretFingerprint(`${admin.username}\0${admin.password}`),
webhookSecret,
webhookFingerprint: secretFingerprint(webhookSecret),
webhookPath,
};
}
function secretSummaries(pac: PacConfig): Array<Record<string, unknown>> {
const adminPath = credentialPath(pac.gitea.admin.sourceRoot, pac.gitea.admin.sourceRef);
const webhookPath = credentialPath(pac.gitea.webhook.secretRoot, pac.gitea.webhook.secretSourceRef);
const admin = existsSync(adminPath) ? parseLinePair(adminPath, pac.gitea.admin.usernameLine, pac.gitea.admin.passwordLine) : null;
const webhookSecret = existsSync(webhookPath) ? readFileSync(webhookPath, "utf8").trim() : "";
return [
{
id: "gitea-admin",
sourceRef: pac.gitea.admin.sourceRef,
sourcePath: adminPath,
present: admin !== null,
fingerprint: admin === null ? null : secretFingerprint(`${admin.username}\0${admin.password}`),
valuesPrinted: false,
},
{
id: "pac-webhook",
sourceRef: pac.gitea.webhook.secretSourceRef,
sourcePath: webhookPath,
present: webhookSecret.length > 0,
fingerprint: webhookSecret.length > 0 ? secretFingerprint(webhookSecret) : null,
valuesPrinted: false,
},
];
}
function statusSummary(payload: Record<string, unknown>): Record<string, unknown> {
const pipelineRuns = arrayRecords(payload.pipelineRuns);
const latest = pipelineRuns[0] ?? {};
const taskRuns = arrayRecords(payload.taskRuns);
return {
ready: payload.crdPresent === true && String(payload.controllerReady ?? "0/0") !== "0/0",
crdPresent: payload.crdPresent === true,
controllerReady: payload.controllerReady,
repositoryCondition: payload.repositoryCondition,
webhookCount: arrayRecords(payload.webhooks).length,
latestPipelineRun: latest,
taskRuns,
artifact: record(payload.artifact),
argo: record(payload.argo),
valuesPrinted: false,
};
}
function policyChecks(pac: PacConfig): Array<Record<string, unknown>> {
return [
{ name: "single-path", ok: true, detail: "Gitea webhook -> Pipelines-as-Code -> Tekton -> Argo/k8s runtime; no Gitea Actions/act_runner/branch-follower fallback." },
{ name: "yaml-source-of-truth", ok: true, detail: `${configLabel} owns PaC release, Repository CR, Gitea webhook and AgentRun JD01 v0.2 params.` },
{ name: "gitea-internal-source", ok: pac.repository.cloneUrl.includes(".svc.cluster.local"), detail: "Tekton source clone uses the internal k3s Gitea service URL." },
{ name: "runtime-zero-docker", ok: true, detail: "Runtime starts at k8s pulling already-built images; CI build may use Tekton/BuildKit." },
];
}
function targetSummary(target: PacTarget): Record<string, unknown> {
return { id: target.id, route: target.route, namespace: target.namespace, role: target.role };
}
function configSummary(pac: PacConfig): Record<string, unknown> {
return {
path: configLabel,
metadata: pac.metadata,
release: pac.release,
gitea: { configRef: pac.gitea.configRef, internalBaseUrl: pac.gitea.internalBaseUrl, webhookBranch: pac.gitea.webhook.branch },
repository: repositorySummary(pac),
consumer: pac.consumer,
valuesPrinted: false,
};
}
function compactConfigSummary(pac: PacConfig): Record<string, unknown> {
return {
path: configLabel,
releaseVersion: pac.release.version,
repository: pac.repository.name,
providerType: pac.repository.providerType,
sourceUrl: pac.repository.cloneUrl,
consumer: `${pac.consumer.node}/${pac.consumer.lane}`,
pipeline: pac.consumer.pipeline,
valuesPrinted: false,
};
}
function repositorySummary(pac: PacConfig): Record<string, unknown> {
return {
name: pac.repository.name,
namespace: pac.repository.namespace,
providerType: pac.repository.providerType,
url: pac.repository.url,
cloneUrl: pac.repository.cloneUrl,
owner: pac.repository.owner,
repo: pac.repository.repo,
secretName: pac.repository.secretName,
concurrencyLimit: pac.repository.concurrencyLimit,
params: Object.keys(pac.repository.params).sort(),
};
}
function nextCommands(targetId: string): Record<string, string> {
return {
apply: `bun scripts/cli.ts platform-infra pipelines-as-code apply --target ${targetId} --confirm`,
status: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${targetId}`,
webhookTest: `bun scripts/cli.ts platform-infra pipelines-as-code webhook-test --target ${targetId} --confirm`,
};
}
function renderPlan(result: Record<string, unknown>): RenderedCliResult {
const target = record(result.target);
const config = record(result.config);
const repository = record(result.repository);
const secrets = arrayRecords(result.secrets);
const policy = arrayRecords(result.policy);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE PLAN",
...table(["TARGET", "NAMESPACE", "RELEASE", "REPOSITORY"], [[stringValue(target.id), stringValue(target.namespace), stringValue(record(config.release).version), stringValue(repository.name)]]),
"",
"PATH",
...table(["STAGE", "AUTHORITY", "DETAIL"], [
["source", "Gitea", stringValue(repository.cloneUrl)],
["trigger", "Pipelines-as-Code", "Gitea push webhook"],
["ci", "Tekton", stringValue(record(config.consumer).pipeline)],
["cd", "Argo", stringValue(record(config.consumer).argoApplication)],
]),
"",
"SECRETS",
...table(["ID", "PRESENT", "FINGERPRINT", "VALUES"], secrets.map((item) => [stringValue(item.id), boolText(item.present), stringValue(item.fingerprint), "false"])),
"",
"POLICY",
...table(["NAME", "OK", "DETAIL"], policy.map((item) => [stringValue(item.name), boolText(item.ok), stringValue(item.detail)])),
"",
"NEXT",
` apply: ${stringValue(record(result.next).apply)}`,
` status: ${stringValue(record(result.next).status)}`,
];
return rendered(result, "platform-infra pipelines-as-code plan", lines);
}
function renderApply(result: Record<string, unknown>): RenderedCliResult {
const target = record(result.target);
const remote = record(result.remote);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE APPLY",
...table(["TARGET", "MODE", "MUTATION", "OK"], [[stringValue(target.id), stringValue(result.mode), boolText(result.mutation), boolText(result.ok)]]),
"",
"REMOTE",
...table(["CONTROLLER", "WEBHOOK", "REPOSITORY", "VALUES"], [[stringValue(remote.controllerReady), stringValue(record(remote.webhook).present), stringValue(remote.repository), "false"]]),
"",
"NEXT",
` status: ${stringValue(record(result.next).status)}`,
];
return rendered(result, "platform-infra pipelines-as-code apply", lines);
}
function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const latest = record(summary.latestPipelineRun);
const taskRuns = arrayRecords(summary.taskRuns);
const artifact = record(summary.artifact);
const argo = record(summary.argo);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE STATUS",
...table(["READY", "CRD", "CONTROLLER", "WEBHOOKS", "REPOSITORY"], [[boolText(summary.ready), boolText(summary.crdPresent), stringValue(summary.controllerReady), stringValue(summary.webhookCount), compactLine(stringValue(summary.repositoryCondition))]]),
"",
"LATEST PIPELINERUN",
...table(["NAME", "STATUS", "REASON", "DURATION_S", "SOURCE"], [[stringValue(latest.name), stringValue(latest.status), stringValue(latest.reason), stringValue(latest.durationSeconds), short(stringValue(latest.sourceCommit))]]),
"",
"TASKRUN DURATIONS",
...(taskRuns.length === 0 ? ["-"] : table(["TASKRUN", "STATUS", "REASON", "DURATION_S"], taskRuns.map((item) => [short(stringValue(item.name), 56), stringValue(item.status), stringValue(item.reason), stringValue(item.durationSeconds)]))),
"",
"IMAGE / GITOPS",
...table(["IMAGE_STATUS", "ENV_ID", "DIGEST", "GITOPS"], [[stringValue(artifact.imageStatus), stringValue(artifact.envIdentity), short(stringValue(artifact.digest), 18), short(stringValue(artifact.gitopsCommit))]]),
"",
"ARGO",
...table(["SYNC", "HEALTH", "REVISION"], [[stringValue(argo.sync), stringValue(argo.health), short(stringValue(argo.revision))]]),
"",
"NEXT",
` full: bun scripts/cli.ts platform-infra pipelines-as-code status --target ${stringValue(record(result.target).id)} --full`,
];
return rendered(result, "platform-infra pipelines-as-code status", lines);
}
function renderWebhookTest(result: Record<string, unknown>): RenderedCliResult {
const remote = record(result.remote);
const lines = [
"PLATFORM-INFRA PIPELINES-AS-CODE WEBHOOK TEST",
...table(["OK", "MUTATION", "HOOK", "SENT"], [[boolText(result.ok), boolText(result.mutation), stringValue(remote.hookId), boolText(remote.webhookTestSent)]]),
"",
"NEXT",
` status: ${stringValue(record(result.next).status)}`,
];
return rendered(result, "platform-infra pipelines-as-code webhook-test", lines);
}
function parseApplyOptions(args: string[]): ApplyOptions {
const commonArgs: string[] = [];
let confirm = false;
let dryRun = false;
let wait = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm") confirm = true;
else if (arg === "--dry-run") dryRun = true;
else if (arg === "--wait") wait = true;
else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
if (confirm && dryRun) throw new Error("pipelines-as-code apply accepts only one of --confirm or --dry-run");
return { ...parseCommonOptions(commonArgs), confirm, dryRun: dryRun || !confirm, wait };
}
function parseWebhookTestOptions(args: string[]): WebhookTestOptions {
const commonArgs: string[] = [];
let confirm = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--confirm") confirm = true;
else {
commonArgs.push(arg);
if (arg === "--target" || arg === "--node") {
commonArgs.push(args[index + 1] ?? "");
index += 1;
}
}
}
return { ...parseCommonOptions(commonArgs), confirm };
}
function parseCommonOptions(args: string[]): CommonOptions {
let targetId: string | null = null;
let full = false;
let raw = false;
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--target" || arg === "--node") {
const value = args[index + 1];
if (value === undefined || value.startsWith("--")) throw new Error(`${arg} requires a value`);
if (!/^[A-Za-z0-9._-]+$/u.test(value)) throw new Error(`${arg} must be a simple target id`);
targetId = value;
index += 1;
} else if (arg === "--full") {
full = true;
} else if (arg === "--raw") {
raw = true;
full = true;
} else {
throw new Error(`unsupported pipelines-as-code option: ${arg}`);
}
}
return { targetId, full, raw };
}
function positiveInteger(obj: Record<string, unknown>, key: string, path: string): number {
const value = y.integerField(obj, key, path);
if (value < 1) throw new Error(`${configLabel}.${path}.${key} must be positive`);
return value;
}
function urlField(obj: Record<string, unknown>, key: string, path: string): string {
const value = y.stringField(obj, key, path);
const parsed = new URL(value);
if (!["http:", "https:"].includes(parsed.protocol) || parsed.search || parsed.hash) throw new Error(`${configLabel}.${path}.${key} must be an http(s) URL without query or hash`);
return value.replace(/\/+$/u, "");
}
function stringRecord(obj: Record<string, unknown>, path: string): Record<string, string> {
const out: Record<string, string> = {};
for (const [key, value] of Object.entries(obj)) {
if (typeof value !== "string" || value.length === 0) throw new Error(`${configLabel}.${path}.${key} must be a non-empty string`);
out[key] = value;
}
return out;
}
function secretFingerprint(value: string): string {
return sha256Fingerprint(value).slice(0, 23);
}
function rendered(result: Record<string, unknown>, command: string, lines: string[]): RenderedCliResult {
return { ok: result.ok !== false, command, renderedText: lines.join("\n"), contentType: "text/plain" };
}
function record(value: unknown): Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function arrayRecords(value: unknown): Record<string, unknown>[] {
return Array.isArray(value) ? value.filter((item) => typeof item === "object" && item !== null && !Array.isArray(item)) as Record<string, unknown>[] : [];
}
function stringValue(value: unknown, fallback = "-"): string {
if (typeof value === "string" && value.length > 0) return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return fallback;
}
function boolText(value: unknown): string {
return value === true ? "true" : "false";
}
function short(value: string, length = 12): string {
if (value === "-" || value.length <= length) return value;
return value.slice(0, length);
}
function compactLine(value: string): string {
const trimmed = value.replace(/\s+/gu, " ").trim();
return trimmed.length > 0 ? trimmed.slice(0, 120) : "-";
}
function table(headers: string[], rows: string[][]): string[] {
const widths = headers.map((header, index) => Math.max(header.length, ...rows.map((row) => (row[index] ?? "").length)));
const format = (row: string[]) => row.map((cell, index) => cell.padEnd(widths[index])).join(" ");
return [format(headers), format(headers.map((header, index) => "-".repeat(Math.max(header.length, widths[index])))), ...rows.map(format)];
}