344 lines
18 KiB
TypeScript
344 lines
18 KiB
TypeScript
import type { RenderedCliResult } from "./output";
|
|
|
|
export function renderMirrorPlan(result: Record<string, unknown>): RenderedCliResult {
|
|
const target = record(result.target);
|
|
const source = record(result.sourceAuthority);
|
|
const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.upstreamRepository), stringValue(repo.upstreamBranch), stringValue(repo.readUrl), stringValue(repo.legacyDisposition)]);
|
|
const responsibilities = arrayRecords(result.responsibilities).map((item) => [stringValue(item.name), stringValue(item.current), stringValue(item.target), stringValue(item.disposition)]);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea mirror plan", [
|
|
"PLATFORM-INFRA GITEA MIRROR PLAN",
|
|
...table(["TARGET", "STAGE", "AUTHORITY", "REPOS"], [[stringValue(target.id), stringValue(source.stage), stringValue(source.statusAuthority), stringValue(source.repositoryCount)]]),
|
|
"",
|
|
"REPOSITORIES",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "UPSTREAM", "BRANCH", "GITEA_READ_URL", "LEGACY"], repos)),
|
|
"",
|
|
"RESPONSIBILITIES",
|
|
...(responsibilities.length === 0 ? ["-"] : table(["NAME", "CURRENT", "TARGET", "DISPOSITION"], responsibilities)),
|
|
"",
|
|
"NEXT",
|
|
` status: ${stringValue(next.status)}`,
|
|
` full: ${stringValue(next.statusFull)}`,
|
|
` webhook-status: ${stringValue(next.webhookStatus)}`,
|
|
` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`,
|
|
"",
|
|
"Disclosure: credentials are summarized by presence/fingerprint only.",
|
|
]);
|
|
}
|
|
|
|
export function renderMirrorBootstrap(result: Record<string, unknown>): RenderedCliResult {
|
|
const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.owner), stringValue(repo.name), boolText(record(repo.create).ok)]);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea mirror bootstrap", [
|
|
"PLATFORM-INFRA GITEA MIRROR BOOTSTRAP",
|
|
...table(["OK", "MUTATION", "TARGET"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id)]]),
|
|
"",
|
|
"REPOSITORIES",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "OWNER", "REPO", "API_OK"], repos)),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT ${stringValue(next.webhookStatus)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderMirrorSync(result: Record<string, unknown>): RenderedCliResult {
|
|
const bundles = arrayRecords(result.sourceBundles).map((bundle) => [stringValue(bundle.key), stringValue(bundle.sourceCommit).slice(0, 12), stringValue(bundle.snapshotRef), stringValue(bundle.readUrl)]);
|
|
const repos = arrayRecords(result.repositories).map((repo) => [
|
|
stringValue(repo.key),
|
|
boolText(repo.syncOk),
|
|
stringValue(repo.fetchRc),
|
|
stringValue(repo.pushRc),
|
|
stringValue(repo.gitopsPushRc),
|
|
stringValue(repo.sourceCommit).slice(0, 12),
|
|
compactTail(stringValue(repo.fetchTail) || stringValue(repo.pushTail) || stringValue(repo.revparseTail)),
|
|
]);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea mirror sync", [
|
|
"PLATFORM-INFRA GITEA MIRROR SYNC",
|
|
...table(["OK", "MUTATION", "TARGET"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id)]]),
|
|
"",
|
|
"REPOSITORIES",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "SYNC", "FETCH", "PUSH", "GITOPS", "COMMIT", "ERROR"], repos)),
|
|
"",
|
|
"SOURCE BUNDLES",
|
|
...(bundles.length === 0 ? ["-"] : table(["KEY", "COMMIT", "SNAPSHOT_REF", "READ_URL"], bundles)),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT ${stringValue(next.status)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderMirrorStatus(result: Record<string, unknown>): RenderedCliResult {
|
|
const source = record(result.sourceAuthority);
|
|
const service = record(result.serviceHealth);
|
|
const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.mirrorState), stringValue(repo.branchCommit).slice(0, 12), stringValue(repo.snapshotCommit).slice(0, 12), stringValue(repo.snapshotRef)]);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea mirror status", [
|
|
"PLATFORM-INFRA GITEA MIRROR STATUS",
|
|
...table(["TARGET", "SERVICE", "MIRROR_READY", "AUTHORITY"], [[stringValue(record(result.target).id), boolText(service.ok), boolText(source.mirrorReady), stringValue(source.statusAuthority)]]),
|
|
"",
|
|
"REPOSITORIES",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "STATE", "BRANCH", "SNAPSHOT", "SNAPSHOT_REF"], repos)),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT ${stringValue(next.webhookStatus)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderMirrorWebhookApply(result: Record<string, unknown>): RenderedCliResult {
|
|
const repos = arrayRecords(result.repositories).map((repo) => [stringValue(repo.key), stringValue(repo.repository), boolText(repo.hookReady), stringValue(repo.hookId), compactTail(stringValue(repo.error))]);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea mirror webhook apply", [
|
|
"PLATFORM-INFRA GITEA MIRROR WEBHOOK APPLY",
|
|
...table(["OK", "MUTATION", "TARGET", "URL"], [[boolText(result.ok), boolText(result.mutation), stringValue(record(result.target).id), stringValue(record(result.webhook).publicUrl)]]),
|
|
"",
|
|
"GITHUB HOOKS",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "REPOSITORY", "READY", "HOOK_ID", "ERROR"], repos)),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT ${stringValue(next.webhookStatus)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderMirrorWebhookStatus(result: Record<string, unknown>, disclosure: Record<string, unknown>): RenderedCliResult {
|
|
const repos = arrayRecords(result.repositories).map((repo) => {
|
|
const latestPush = record(repo.latestPushDelivery);
|
|
const latest = record(repo.latestDelivery);
|
|
const delivery = latestPush.event !== "" ? latestPush : latest;
|
|
const inbox = record(repo.durableInboxRecord);
|
|
const inboxResult = record(inbox.result);
|
|
const inboxState = stringValue(repo.durableInboxState, "missing");
|
|
const durableProof = repo.preDurableBootstrap === true
|
|
? "pre-durable-bootstrap"
|
|
: repo.durableInboxCommitted === true
|
|
? `${stringValue(inboxResult.disposition)}:${stringValue(inboxResult.sourceCommit).slice(0, 12)}`
|
|
: "-";
|
|
return [
|
|
stringValue(repo.key),
|
|
boolText(repo.hookReady),
|
|
stringValue(repo.githubHead).slice(0, 12),
|
|
stringValue(repo.branchCommit).slice(0, 12),
|
|
stringValue(repo.snapshotCommit).slice(0, 12),
|
|
boolText(repo.authorityMatches),
|
|
boolText(repo.bridgeEventStale),
|
|
`${stringValue(repo.deliveryId)}:${stringValue(delivery.statusCode)}:${stringValue(delivery.status)}`,
|
|
inboxState,
|
|
durableProof,
|
|
compactTail(stringValue(repo.error)),
|
|
];
|
|
});
|
|
const bridge = record(result.bridge);
|
|
const bridgeLogs = record(result.bridgeLogs);
|
|
const retry = record(record(record(result.webhook).bridge).retry);
|
|
const retryText = `${stringValue(retry.maxAttempts)}x/${stringValue(retry.initialDelayMs)}-${stringValue(retry.maxDelayMs)}ms`;
|
|
const selection = record(disclosure.selection);
|
|
const selectedRepoKey = stringValue(selection.selectedRepoKey, "");
|
|
const delivery = arrayRecords(disclosure.deliveries)[0];
|
|
const deliveryResult = record(delivery?.result);
|
|
const retryDetail = record(disclosure.retry);
|
|
const logs = record(disclosure.logs);
|
|
const logRows = arrayRecords(logs.events).map((event) => [
|
|
stringValue(event.syncAttempt),
|
|
boolText(event.ok),
|
|
boolText(event.terminal),
|
|
stringValue(event.disposition),
|
|
stringValue(event.elapsedMs),
|
|
compactLine(stringValue(event.error)),
|
|
]);
|
|
const firstError = record(record(disclosure.diagnostics).firstError);
|
|
const repositoryCommands = arrayRecords(record(disclosure.next).repositories).map((item) => [
|
|
stringValue(item.key),
|
|
stringValue(item.deliveryId),
|
|
stringValue(item.command),
|
|
]);
|
|
const drillDown = selectedRepoKey === ""
|
|
? [
|
|
"",
|
|
"DRILL-DOWN",
|
|
...(repositoryCommands.length === 0 ? ["-"] : table(["REPOSITORY", "DELIVERY", "COMMAND"], repositoryCommands)),
|
|
]
|
|
: [
|
|
"",
|
|
"DELIVERY",
|
|
...(delivery === undefined ? ["-"] : table(["ID", "REPOSITORY", "STATE", "ATTEMPTS", "NEXT", "COMMIT", "DISPOSITION"], [[
|
|
stringValue(delivery.deliveryId),
|
|
stringValue(delivery.repo),
|
|
stringValue(delivery.state),
|
|
`${stringValue(retryDetail.cycleAttempt)}/${stringValue(retryDetail.totalAttempts)}`,
|
|
stringValue(retryDetail.nextAttemptAt),
|
|
stringValue(deliveryResult.sourceCommit).slice(0, 12),
|
|
stringValue(deliveryResult.disposition),
|
|
]])),
|
|
"",
|
|
"BRIDGE EVENTS",
|
|
...(logRows.length === 0 ? ["-"] : table(["ATTEMPT", "OK", "TERMINAL", "DISPOSITION", "ELAPSED_MS", "ERROR"], logRows)),
|
|
...(firstError.message === undefined ? [] : ["", `FIRST ERROR ${stringValue(firstError.domain)}: ${stringValue(firstError.message)}`]),
|
|
];
|
|
return rendered(disclosure, "platform-infra gitea mirror webhook status", [
|
|
"PLATFORM-INFRA GITEA MIRROR WEBHOOK STATUS",
|
|
...table(["TARGET", "BRIDGE", "READY", "RETRY", "URL", "LOGS"], [[stringValue(record(result.target).id), stringValue(bridge.deployment), boolText(bridge.ready), retryText, stringValue(record(result.webhook).publicUrl), `rc=${stringValue(bridgeLogs.exitCode)}`]]),
|
|
"",
|
|
"GITHUB HOOKS",
|
|
...(repos.length === 0 ? ["-"] : table(["KEY", "HOOK", "GH_HEAD", "GITEA", "SNAPSHOT", "MATCH", "STALE", "DELIVERY", "DURABLE", "PROOF", "ERROR"], repos)),
|
|
...drillDown,
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT ${stringValue(record(disclosure.next).full)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderPlan(result: Record<string, unknown>): RenderedCliResult {
|
|
const config = record(result.config);
|
|
const target = record(config.target);
|
|
const app = record(config.app);
|
|
const migration = record(config.migration);
|
|
const policy = arrayRecords(result.policy);
|
|
const failed = policy.filter((item) => item.ok === false);
|
|
const next = record(result.next);
|
|
return rendered(result, "platform-infra gitea plan", [
|
|
"PLATFORM-INFRA GITEA PLAN",
|
|
...table(["FIELD", "VALUE", "DETAIL", "VALUE"], [
|
|
["TARGET", stringValue(target.id), "route", stringValue(target.route)],
|
|
["NAMESPACE", stringValue(target.namespace), "role", stringValue(target.role)],
|
|
["IMAGE", stringValue(app.image), "replicas", stringValue(app.replicas)],
|
|
["SERVICE", `${stringValue(app.serviceName)}:${stringValue(record(app.service).httpPort)}`, "dns", serviceDnsFromObjects(app, target)],
|
|
["WEBUI", stringValue(record(app.publicExposure).publicBaseUrl), "internalGit", serviceDnsFromObjects(app, target)],
|
|
["ACTIONS", boolText(app.actionsEnabled), "registrationDisabled", boolText(app.registrationDisabled)],
|
|
["MIGRATION", stringValue(migration.role), "replaces", stringValue(migration.replaces)],
|
|
["POLICY", failed.length === 0 ? "ok" : `failed=${failed.length}`, "valuesPrinted", "false"],
|
|
]),
|
|
"",
|
|
"NEXT",
|
|
` status: ${stringValue(next.status)}`,
|
|
` validate: ${stringValue(next.validate)}`,
|
|
` mirror-status: ${stringValue(next.mirrorStatus)}`,
|
|
` webhook-status: ${stringValue(next.webhookStatus)}`,
|
|
` fix-automatic-delivery: ${stringValue(next.fixAutomaticDelivery)}`,
|
|
"",
|
|
"Boundary: Gitea is internal ClusterIP source authority for GH-1560; CI trigger authority is Pipelines-as-Code, not Gitea Actions/act_runner.",
|
|
"Disclosure: Secret values are not printed; this stage does not create runner credentials.",
|
|
]);
|
|
}
|
|
|
|
export function renderApply(result: Record<string, unknown>): RenderedCliResult {
|
|
const target = record(result.target);
|
|
const payload = record(result.payloadTransport);
|
|
const remote = record(result.remote);
|
|
const steps = record(remote.steps);
|
|
const serverDryRunStep = record(steps.serverDryRun);
|
|
const frpcStep = record(steps.frpcSecret);
|
|
const frpcCleanupStep = record(steps.frpcCleanup);
|
|
const webhookSecretStep = record(steps.webhookSyncSecret);
|
|
const applyStep = record(steps.apply);
|
|
const rolloutStep = record(steps.rollout);
|
|
const frpcRolloutStep = record(steps.frpcRollout);
|
|
const webhookRolloutStep = record(steps.webhookSyncRollout);
|
|
const caddy = record(result.pk01Caddy);
|
|
return rendered(result, "platform-infra gitea apply", [
|
|
"PLATFORM-INFRA GITEA APPLY",
|
|
...table(["TARGET", "NAMESPACE", "MODE", "OK"], [[stringValue(target.id), stringValue(target.namespace), stringValue(result.mode), boolText(result.ok)]]),
|
|
...table(["TRANSPORT", "BYTES", "MATERIALIZATION", "VALUES"], [[stringValue(payload.kind), stringValue(payload.totalBytes), stringValue(payload.materialization), stringValue(payload.valuesPrinted)]]),
|
|
"",
|
|
"STEPS",
|
|
...table(["STEP", "EXIT", "DETAIL"], [
|
|
["serverDryRun", stringValue(serverDryRunStep.exitCode), compactLine(stringValue(serverDryRunStep.stderrTail, stringValue(serverDryRunStep.stdoutTail)))],
|
|
["frpc-secret", stringValue(frpcStep.exitCode), compactLine(stringValue(frpcStep.stderrTail, stringValue(frpcStep.stdoutTail)))],
|
|
["frpc-cleanup", stringValue(frpcCleanupStep.exitCode), compactLine(stringValue(frpcCleanupStep.stderrTail, stringValue(frpcCleanupStep.stdoutTail)))],
|
|
["webhook-secret", stringValue(webhookSecretStep.exitCode), compactLine(stringValue(webhookSecretStep.stderrTail, stringValue(webhookSecretStep.stdoutTail)))],
|
|
["apply", stringValue(applyStep.exitCode), compactLine(stringValue(applyStep.stderrTail, stringValue(applyStep.stdoutTail)))],
|
|
["rollout", stringValue(rolloutStep.exitCode), compactLine(stringValue(rolloutStep.stderrTail, stringValue(rolloutStep.stdoutTail)))],
|
|
["frpc-rollout", stringValue(frpcRolloutStep.exitCode), compactLine(stringValue(frpcRolloutStep.stderrTail, stringValue(frpcRolloutStep.stdoutTail)))],
|
|
["webhook-rollout", stringValue(webhookRolloutStep.exitCode), compactLine(stringValue(webhookRolloutStep.stderrTail, stringValue(webhookRolloutStep.stdoutTail)))],
|
|
["pk01-caddy", stringValue(caddy.exitCode ?? (caddy.ok === false ? 1 : 0)), compactLine(stringValue(caddy.error, stringValue(caddy.status, stringValue(caddy.reason))))],
|
|
]),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT bun scripts/cli.ts platform-infra gitea status --target ${stringValue(target.id)}`,
|
|
]);
|
|
}
|
|
|
|
export function renderStatus(result: Record<string, unknown>): RenderedCliResult {
|
|
const summary = record(result.summary);
|
|
const statefulSet = record(summary.statefulSet);
|
|
const service = record(summary.service);
|
|
const networkPolicy = record(summary.networkPolicy);
|
|
const pods = arrayRecords(summary.pods).map((pod) => [stringValue(pod.name), stringValue(pod.phase), boolText(pod.ready), stringValue(pod.restarts)]);
|
|
const pvcs = arrayRecords(summary.pvcs).map((pvc) => [stringValue(pvc.name), stringValue(pvc.phase), stringValue(pvc.capacity)]);
|
|
return rendered(result, "platform-infra gitea status", [
|
|
"PLATFORM-INFRA GITEA STATUS",
|
|
...table(["TARGET", "ROUTE", "NAMESPACE", "READY"], [[stringValue(summary.target), stringValue(summary.route), stringValue(summary.namespace), boolText(summary.ready)]]),
|
|
"",
|
|
"CONTROL",
|
|
...table(["CHECK", "VALUE", "DETAIL"], [
|
|
["statefulSet", `${stringValue(statefulSet.readyReplicas)}/${stringValue(statefulSet.desired)}`, stringValue(statefulSet.name)],
|
|
["service", stringValue(service.type), stringValue(summary.serviceDns)],
|
|
["endpoints", boolText(summary.endpointsReady), stringValue(service.clusterIP)],
|
|
["allow-all", boolText(networkPolicy.allowAllPresent), "NetworkPolicy"],
|
|
]),
|
|
"",
|
|
"PODS",
|
|
...(pods.length === 0 ? ["-"] : table(["POD", "PHASE", "READY", "RESTARTS"], pods)),
|
|
"",
|
|
"PVCS",
|
|
...(pvcs.length === 0 ? ["-"] : table(["PVC", "PHASE", "CAPACITY"], pvcs)),
|
|
...remoteErrorLines(result),
|
|
"",
|
|
`NEXT bun scripts/cli.ts platform-infra gitea validate --target ${stringValue(summary.target)}`,
|
|
]);
|
|
}
|
|
|
|
function remoteErrorLines(result: Record<string, unknown>): string[] {
|
|
if (result.ok !== false) return [];
|
|
const remote = record(result.remote);
|
|
const exitCode = stringValue(remote.exitCode);
|
|
const stderr = compactLine(stringValue(remote.stderrTail));
|
|
const stdout = compactLine(stringValue(remote.stdoutTail));
|
|
const detail = stderr !== "-" ? stderr : stdout;
|
|
return ["", "ERROR", ...table(["EXIT", "DETAIL"], [[exitCode, detail]])];
|
|
}
|
|
|
|
function compactTail(value: string): string {
|
|
const line = value.split(/\r?\n/u).map((item) => item.trim()).filter(Boolean).at(-1) ?? "";
|
|
return line.length > 96 ? `${line.slice(0, 93)}...` : line;
|
|
}
|
|
|
|
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 serviceDnsFromObjects(app: Record<string, unknown>, target: Record<string, unknown>): string {
|
|
const service = record(app.service);
|
|
return `${stringValue(app.serviceName)}.${stringValue(target.namespace)}.svc.cluster.local:${stringValue(service.httpPort)}`;
|
|
}
|
|
|
|
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 compactLine(value: string): string {
|
|
const trimmed = value.replace(/\s+/gu, " ").trim();
|
|
return trimmed.length > 0 ? trimmed.slice(0, 220) : "-";
|
|
}
|
|
|
|
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)];
|
|
}
|