import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config"; import type { RenderedCliResult } from "./output"; export interface PacBootstrapRepositoryIdentity { readonly id: string; readonly owner: string; readonly repo: string; readonly cloneUrl: string; } export function resolvePacBootstrapMirrorRepository( gitea: GiteaConfig, targetId: string, repository: PacBootstrapRepositoryIdentity, ): GiteaMirrorRepository { const candidates = gitea.sourceAuthority.repositories.filter((item) => ( item.targetId.toLowerCase() === targetId.toLowerCase() && item.gitea.owner === repository.owner && item.gitea.name === repository.repo && repositoryUrlIdentity(item.gitea.readUrl) === repositoryUrlIdentity(repository.cloneUrl) )); if (candidates.length !== 1) { const disposition = candidates.length === 0 ? "no exact match" : `${candidates.length} exact matches`; throw new Error( `cannot resolve the YAML-owned Gitea bootstrap repository for PaC repository ${repository.id} on ${targetId}: ${disposition}; ` + "match targetId, gitea owner/name, and read URL in config/platform-infra/gitea.yaml", ); } return candidates[0]; } export function pacBootstrapYamlMatchFailure( targetId: string, consumerId: string, error: unknown, ): Record { const reason = error instanceof Error ? error.message : String(error); const matchCount = reason.includes("no exact match") ? 0 : Number.parseInt(reason.match(/(\d+) exact matches/u)?.[1] ?? "-1", 10); return { ok: false, action: "platform-infra-pipelines-as-code-bootstrap", mutation: false, mode: "precondition-failed", target: { id: targetId }, consumer: { id: consumerId }, preconditions: [ { id: "yaml-exact-match", ok: false, code: matchCount === 0 ? "yaml-repository-no-match" : "yaml-repository-multiple-matches", matchCount, detail: reason }, ], stages: [], blockers: [{ code: matchCount === 0 ? "yaml-repository-no-match" : "yaml-repository-multiple-matches", stage: "yaml-exact-match", reason }], next: { kind: "fix-yaml", command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配", }, valuesPrinted: false, }; } export function projectPacBootstrapResult(result: Record): Record { if (Array.isArray(result.preconditions) && Array.isArray(result.stages) && Array.isArray(result.blockers)) return result; const target = record(result.target); const consumer = record(result.consumer); const repository = record(result.repository); const mirror = record(result.mirrorRepository); const githubPreflight = record(result.githubPreflight); const githubRepository = records(githubPreflight.repositories)[0] ?? {}; const gitea = record(result.giteaBootstrap); const apply = record(result.pipelinesAsCodeApply); const remote = record(apply.remote); const blockers = bootstrapBlockers(githubPreflight, gitea, apply, remote); const dryRun = result.mode === "dry-run"; const repositoryMissing = remote.error === "gitea-repository-missing"; const githubReady = githubPreflight.ok === true; const giteaReady = gitea.ok === true; const giteaSkipped = gitea.mode === "skipped"; const applySkipped = apply.mode === "skipped"; const applyReady = apply.ok === true || (dryRun && repositoryMissing); const next = bootstrapNext(result, blockers); return { ok: blockers.length === 0 && giteaReady && applyReady, action: result.action, mutation: result.mutation === true, mode: result.mode, target: { id: target.id }, consumer: { id: consumer.id, node: consumer.node, lane: consumer.lane }, sourceAuthority: { upstreamRepository: mirror.upstreamRepository, upstreamBranch: mirror.upstreamBranch, github: { ok: githubReady, code: stringValue(githubRepository.code, githubReady ? "github-upstream-available" : "github-upstream-preflight-failed") }, giteaRepository: `${stringValue(mirror.owner)}/${stringValue(mirror.repo)}`, giteaKey: mirror.key, valuesPrinted: false, }, preconditions: [ { id: "yaml-exact-match", ok: true, code: "yaml-repository-exact-match", matchCount: 1 }, { id: "github-upstream", ok: githubReady, code: stringValue(githubRepository.code, githubReady ? "github-upstream-available" : "github-upstream-preflight-failed") }, { id: "gitea-repository", ok: giteaSkipped ? null : giteaReady, code: giteaSkipped ? "gitea-bootstrap-skipped" : giteaReady ? (dryRun ? "gitea-bootstrap-planned" : "gitea-bootstrap-ready") : "gitea-bootstrap-failed" }, ], 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: "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 }, blockers, next, valuesPrinted: false, }; } export function renderPacBootstrap(result: Record): RenderedCliResult { const projection = projectPacBootstrapResult(result); const target = record(projection.target); const consumer = record(projection.consumer); const source = record(projection.sourceAuthority); const preconditions = records(projection.preconditions); const stages = records(projection.stages); const blockers = records(projection.blockers); const next = record(projection.next); const lines = [ "PLATFORM-INFRA PAC / TEKTON / GITOPS BOOTSTRAP", ...table(["TARGET", "CONSUMER", "MODE", "MUTATION", "OK"], [[stringValue(target.id), stringValue(consumer.id), stringValue(projection.mode), boolText(projection.mutation), boolText(projection.ok)]]), "", "SOURCE AUTHORITY", ...table(["UPSTREAM", "BRANCH", "GITEA_KEY", "GITEA_REPOSITORY"], [[stringValue(source.upstreamRepository), stringValue(source.upstreamBranch), stringValue(source.giteaKey), stringValue(source.giteaRepository)]]), "", "PRECONDITIONS", ...table(["PRECONDITION", "OK", "CODE"], preconditions.map((item) => [stringValue(item.id), boolText(item.ok), stringValue(item.code)])), "", "STAGES", ...table(["STAGE", "OK", "STATE", "MUTATION"], stages.map((item) => [stringValue(item.id), boolText(item.ok), stringValue(item.state), boolText(item.mutation)])), ...(blockers.length === 0 ? [] : ["", "BLOCKERS", ...blockers.map((item) => ` ${stringValue(item.code)}: ${compactLine(stringValue(item.reason))}`)]), "", "NEXT", ` ${stringValue(next.kind)}: ${stringValue(next.command)}`, ]; return { ok: projection.ok === true, command: "platform-infra pipelines-as-code bootstrap", renderedText: lines.join("\n"), contentType: "text/plain", projection }; } function bootstrapBlockers(githubPreflight: Record, gitea: Record, apply: Record, remote: Record): Record[] { const blockers: Record[] = []; const githubRepository = records(githubPreflight.repositories)[0] ?? {}; if (githubPreflight.ok === false) { blockers.push({ code: stringValue(githubRepository.code, stringValue(githubPreflight.code, "github-upstream-preflight-failed")), stage: "github-upstream", reason: stringValue(githubRepository.errorTail, errorText(githubPreflight)) }); return blockers; } for (const code of Array.isArray(gitea.blockers) ? gitea.blockers.map(String) : []) { blockers.push({ code: code.includes("credential") ? "source-credential-unavailable" : "github-upstream-unavailable", stage: "github-upstream", reason: code }); } if (gitea.ok !== true && blockers.length === 0) blockers.push({ code: "gitea-bootstrap-failed", stage: "gitea-init", reason: errorText(gitea) }); if (apply.ok !== true && remote.error !== "gitea-repository-missing") { blockers.push({ code: remote.error === "gitea-credential-unavailable" ? "gitea-credential-unavailable" : "pac-apply-failed", stage: "pac-controller", reason: errorText(remote, apply) }); } return blockers; } function bootstrapNext(result: Record, blockers: Record[]): Record { const target = record(result.target); const consumer = record(result.consumer); const source = record(result.mirrorRepository); if (blockers.length > 0) { const yamlFailure = blockers.some((item) => String(item.code).startsWith("yaml-")); return yamlFailure ? { kind: "fix-yaml", command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配" } : { kind: "read-only-status", command: `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${target.id} --consumer ${consumer.id}` }; } if (result.mode === "dry-run") return { kind: "confirm", command: `bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target ${target.id} --consumer ${consumer.id} --confirm` }; return { kind: "source-pr-merge", command: `合并 ${source.upstreamRepository}@${source.upstreamBranch} 的 GitHub PR,由自动链继续交付` }; } function errorText(...values: Record[]): string { for (const value of values) { for (const candidate of [value.error, value.stderrTail, record(value.remote).error, record(value.remote).stderrTail]) { if (typeof candidate === "string" && candidate.length > 0) return candidate; } } return "unknown-bootstrap-failure"; } function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } function records(value: unknown): Record[] { return Array.isArray(value) ? value.map(record) : []; } function stringValue(value: unknown, fallback = "-"): string { return typeof value === "string" && value.length > 0 ? value : value === undefined || value === null ? fallback : String(value); } function boolText(value: unknown): string { return value === true ? "true" : value === false ? "false" : "-"; } function compactLine(value: string): string { return value.replace(/\s+/gu, " ").trim().slice(0, 240) || "-"; } 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(" ").trimEnd(); return [format(headers), format(widths.map((width) => "-".repeat(width))), ...rows.map(format)]; } function repositoryUrlIdentity(value: string): string { const parsed = new URL(value); const path = parsed.pathname.replace(/\/+$/u, ""); return `${parsed.protocol}//${parsed.host}${path}`; }