merge: 吸收 PaC bootstrap 易用性改进

This commit is contained in:
Codex
2026-07-13 21:11:42 +02:00
7 changed files with 382 additions and 46 deletions
@@ -64,6 +64,10 @@ bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target <NODE> --
- 安装或更新 PaC controller、consumer RBAC、Repository CR、Argo bootstrap 与 Gitea webhook
- 不同步 source branch,不创建 snapshot,不触发业务 PipelineRun。
- 预检与失败边界:
- 默认 human 与显式 `--json` 使用同一份紧凑 typed projection,按 `yaml-exact-match``github-upstream``gitea-repository` 前置条件,以及 `gitea-init``pac-controller``tekton-consumer``gitops-argo` 阶段展示结果;只有显式 `--full``--raw` 才披露底层组合结果;
- YAML 精确匹配必须显示匹配数量,并区分 `yaml-repository-no-match``yaml-repository-multiple-matches`
- GitHub 上游或凭据不可用、Gitea 初始化失败与 PaC apply 失败必须使用不同 blocker code;不得折叠为同一条未知错误;
- 失败输出只给 owning YAML 修正提示或 consumer 只读 `status` 下钻;不得给 mirror sync、人工 PipelineRun、Argo refresh 或其他 mutation 恢复命令;
- PaC `apply` 在任何 Kubernetes 写入前读取 Gitea repository
- repository 缺失时返回 `gitea-repository-missing`,不留下半套 Tekton/RBAC/Argo 状态;
- YAML 零匹配或多匹配时在本地拒绝,不猜测 repository
@@ -4,7 +4,7 @@ import {
readGiteaConfig,
resolveTarget,
} from "./platform-infra-gitea-config";
import { buildMirrorWebhookCredentialBlockedResult, renderManifest } from "./platform-infra-gitea";
import { buildGiteaMirrorBootstrapCredentialPreflight, buildMirrorWebhookCredentialBlockedResult, renderManifest } from "./platform-infra-gitea";
import { renderMirrorWebhookCredentialBlocked } from "./platform-infra-gitea-render";
function syntheticMissingSelfmediaCredential(): Record<string, unknown> {
@@ -65,6 +65,28 @@ describe("platform-infra Gitea repository GitHub credentials", () => {
expect(nc01Overrides.map((credential) => credential.sourceRef)).toContain("pikainc-selfmedia-gh-token.txt");
});
test("bootstrap preflight only requires the selected repository effective credential", () => {
const config = readGiteaConfig();
const repository = config.sourceAuthority.repositories.find((repo) => repo.key === "selfmedia-nc01");
expect(repository).toBeDefined();
const result = buildGiteaMirrorBootstrapCredentialPreflight(config, [repository!], (credential) => (
credential.sourceRef === "pikainc-selfmedia-gh-token.txt"
? { [credential.sourceKey]: "fixture-token" }
: null
));
expect(result.blockers).toEqual([]);
expect(result.credentials).toHaveLength(1);
expect(result.credentials[0]).toMatchObject({
id: "github-upstream:selfmedia-nc01",
sourceRef: "pikainc-selfmedia-gh-token.txt",
present: true,
requiredKeysPresent: true,
});
expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.admin.sourceRef);
expect(JSON.stringify(result)).not.toContain(config.sourceAuthority.credentials.github.sourceRef);
});
test("does not inject the NC01 selfmedia token into JD01 bridge containers", () => {
const config = readGiteaConfig();
const selfmediaTokenEnv = githubTokenEnvNameForSecretKey("github-token-pikainc-selfmedia");
@@ -221,11 +221,45 @@ payload = {
},
"valuesPrinted": False,
}
print(json.dumps(payload, ensure_ascii=False, indent=2))
sys.exit(0 if payload["ok"] else 1)
PY
}
run_mirror_preflight() {
python3 - "$tmp/repos.json" <<'PY'
import base64, json, os, subprocess, sys
repos = json.load(open(sys.argv[1], encoding="utf-8"))
rows = []
timeout = int(os.environ.get("UNIDESK_GITEA_WAIT_TIMEOUT_SECONDS", "20"))
for repo in repos:
credential = repo.get("githubCredential") or {}
token_env = credential.get("tokenEnv") or os.environ.get("UNIDESK_GITEA_GITHUB_DEFAULT_TOKEN_ENV", "")
token = os.environ.get(token_env, "")
clone_url = repo["upstream"]["cloneUrl"]
branch = repo["upstream"]["branch"]
if not token:
rows.append({"key": repo["key"], "repository": repo["upstream"]["repository"], "branch": branch, "ok": False, "code": "github-credential-unavailable", "valuesPrinted": False})
continue
basic = base64.b64encode(("x-access-token:" + token).encode()).decode()
try:
completed = subprocess.run(
["git", "-c", "http.extraHeader=Authorization: Basic " + basic, "ls-remote", "--exit-code", clone_url, "refs/heads/" + branch],
capture_output=True,
text=True,
timeout=timeout,
)
error = " ".join(completed.stderr.split())[-800:]
rows.append({"key": repo["key"], "repository": repo["upstream"]["repository"], "branch": branch, "ok": completed.returncode == 0, "code": "github-upstream-available" if completed.returncode == 0 else "github-repository-not-found-or-no-access", "exitCode": completed.returncode, "errorTail": error, "valuesPrinted": False})
except subprocess.TimeoutExpired:
rows.append({"key": repo["key"], "repository": repo["upstream"]["repository"], "branch": branch, "ok": False, "code": "github-upstream-timeout", "valuesPrinted": False})
payload = {"ok": bool(rows) and all(row["ok"] for row in rows), "mutation": False, "repositories": rows, "valuesPrinted": False}
print(json.dumps(payload, ensure_ascii=False))
sys.exit(0 if payload["ok"] else 1)
PY
}
run_status() {
selector="app.kubernetes.io/name=$UNIDESK_GITEA_APP_NAME,app.kubernetes.io/component=gitea"
capture_json namespace kubectl get namespace "$UNIDESK_GITEA_NAMESPACE"
@@ -1125,6 +1159,7 @@ case "$UNIDESK_GITEA_ACTION" in
apply) run_apply ;;
status) run_status ;;
validate) run_validate ;;
mirror-preflight) run_mirror_preflight ;;
mirror-bootstrap) run_mirror_bootstrap ;;
mirror-sync) run_mirror_sync ;;
mirror-status) run_mirror_status ;;
+64 -1
View File
@@ -123,6 +123,69 @@ export async function runPlatformInfraGiteaCommand(config: UniDeskConfig, args:
};
}
export async function runGiteaMirrorBootstrapPreflight(
config: UniDeskConfig,
targetId: string,
repoKey: string,
): Promise<Record<string, unknown>> {
const gitea = readGiteaConfig();
const target = resolveCommandTarget(gitea, targetId);
const repos = selectedRepositories(gitea, target, repoKey);
const { credentials, blockers } = buildGiteaMirrorBootstrapCredentialPreflight(gitea, repos);
if (blockers.length > 0) {
return { ok: false, mutation: false, code: "github-credential-unavailable", blockers, repositories: repos.map((repo) => repositorySummary(gitea, repo)), valuesPrinted: false };
}
const secrets = mirrorPreflightSecrets(gitea, repos);
const result = await capture(config, target.route, ["sh"], remoteScript("mirror-preflight", gitea, target, "", { targetId, repoKey, confirm: false, dryRun: true, wait: false, full: false, raw: false }, { repos, secrets }).script);
const parsed = parseJsonOutput(result.stdout);
return parsed ?? { ok: false, mutation: false, code: "github-upstream-preflight-failed", remote: compactCapture(result, { full: true }), valuesPrinted: false };
}
export function buildGiteaMirrorBootstrapCredentialPreflight(
gitea: GiteaConfig,
repositories: GiteaMirrorRepository[],
readCredential: (credential: GiteaGithubCredential) => Record<string, string> | null = (credential) => readGithubCredential(gitea, credential),
): { credentials: Array<Record<string, unknown>>; blockers: string[] } {
const credentials = repositories.map((repo) => {
const github = githubCredentialForRepo(gitea, repo);
const values = readCredential(github);
const present = values !== null;
const requiredKeysPresent = Boolean(values?.[github.sourceKey]);
return {
id: `github-upstream:${repo.key}`,
repository: repo.upstream.repository,
sourceRef: github.sourceRef,
present,
requiredKeysPresent,
fingerprint: present ? fingerprintKeys(values, [github.sourceKey]) : null,
permissionResult: present && requiredKeysPresent
? { status: "not-probed", permissions: github.permissions, error: null }
: { status: "blocked-missing-credential", permissions: github.permissions, error: "credential material is absent" },
valuesPrinted: false,
};
});
return { credentials, blockers: credentialBlockers(credentials) };
}
function readGithubCredential(gitea: GiteaConfig, github: GiteaGithubCredential): Record<string, string> | null {
const sourcePath = credentialPath(gitea, github.sourceRef);
return existsSync(sourcePath) ? parseGithubCredentialFile(sourcePath, github) : null;
}
function mirrorPreflightSecrets(gitea: GiteaConfig, repositories: GiteaMirrorRepository[]): MirrorSecrets {
const githubTokens: Record<string, string> = {};
for (const repo of repositories) {
const github = githubCredentialForRepo(gitea, repo);
const githubPath = credentialPath(gitea, github.sourceRef);
if (!existsSync(githubPath)) throw new Error(`${github.sourceRef} is missing; cannot probe GitHub upstream`);
const githubEnv = parseGithubCredentialFile(githubPath, github);
const githubToken = githubEnv[github.sourceKey];
if (!githubToken) throw new Error(`${github.sourceRef} must contain ${github.sourceKey}`);
githubTokens[github.gitFetchCredential.secretRef.key] = githubToken;
}
return { adminUsername: "", adminPassword: "", githubTokens, webhookSecret: "" };
}
export function giteaHelp(scope?: string): Record<string, unknown> {
if (scope === "platform-bootstrap") {
return {
@@ -1116,7 +1179,7 @@ function envVars(gitea: GiteaConfig, target: GiteaTarget): string {
value: ${yamlQuote(value)}`).join("\n");
}
function remoteScript(action: "apply" | "status" | "validate" | "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> } {
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 env: Record<string, string> = {
@@ -3,7 +3,7 @@ import { readFileSync } from "node:fs";
import { resolve } from "node:path";
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
import { renderMirrorBootstrap } from "./platform-infra-gitea-render";
import { resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { pacBootstrapYamlMatchFailure, projectPacBootstrapResult, renderPacBootstrap, resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import { runPlatformInfraPipelinesAsCodeCommand } from "./platform-infra-pipelines-as-code";
const mirror: GiteaMirrorRepository = {
@@ -78,3 +78,62 @@ test("platform bootstrap help advertises the composite entry before low-level ap
expect(usage[0]).toContain("pipelines-as-code bootstrap");
expect(usage).toContain("bun scripts/cli.ts platform-infra pipelines-as-code bootstrap --target <NODE> --consumer <consumer> --confirm");
});
test("PaC bootstrap projects typed stages and compact JSON", () => {
const result = {
ok: true,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: false,
mode: "dry-run",
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: "dry-run", blockers: [] },
pipelinesAsCodeApply: { ok: true, mutation: false, mode: "dry-run", remote: { ok: true, preflight: { repositoryExists: true } } },
};
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"]);
const rendered = renderPacBootstrap(result).renderedText;
expect(rendered).toContain("PRECONDITIONS");
expect(rendered).toContain("yaml-repository-exact-match");
expect(rendered).toContain("confirm:");
});
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);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-no-match");
expect(result.next).toEqual({
kind: "fix-yaml",
command: "检查 config/platform-infra/gitea.yaml 与 config/platform-infra/pipelines-as-code.yaml 的 target、owner/name 和 read URL 精确匹配",
});
});
test("PaC bootstrap returns fix-yaml next for multiple YAML matches", () => {
const result = pacBootstrapYamlMatchFailure("NC01", "widgets", new Error("cannot resolve repository: 2 exact matches"));
expect(result.mutation).toBe(false);
expect((result.blockers as Record<string, unknown>[])[0]?.code).toBe("yaml-repository-multiple-matches");
expect((result.next as Record<string, unknown>).kind).toBe("fix-yaml");
});
test("PaC bootstrap distinguishes GitHub access failure from Gitea and PaC stages", () => {
const projection = projectPacBootstrapResult({
ok: false,
action: "platform-infra-pipelines-as-code-bootstrap",
mutation: false,
mode: "dry-run",
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: false, mutation: false, repositories: [{ key: mirror.key, code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] },
giteaBootstrap: { ok: false, mutation: false, mode: "skipped", githubPreflight: { ok: false, repositories: [{ code: "github-repository-not-found-or-no-access", errorTail: "repository unavailable" }] } },
pipelinesAsCodeApply: { ok: false, mutation: false, mode: "skipped" },
});
expect((projection.blockers as Record<string, unknown>[])).toEqual([{ code: "github-repository-not-found-or-no-access", stage: "github-upstream", reason: "repository unavailable" }]);
expect((projection.next as Record<string, unknown>).kind).toBe("read-only-status");
});
@@ -1,4 +1,5 @@
import type { GiteaConfig, GiteaMirrorRepository } from "./platform-infra-gitea-config";
import type { RenderedCliResult } from "./output";
export interface PacBootstrapRepositoryIdentity {
readonly id: string;
@@ -28,6 +29,179 @@ export function resolvePacBootstrapMirrorRepository(
return candidates[0];
}
export function pacBootstrapYamlMatchFailure(
targetId: string,
consumerId: string,
error: unknown,
): Record<string, unknown> {
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<string, unknown>): Record<string, unknown> {
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 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: giteaReady, code: giteaReady ? (dryRun ? "gitea-bootstrap-planned" : "gitea-bootstrap-ready") : "gitea-bootstrap-failed" },
],
stages: [
{ id: "gitea-init", ok: giteaReady, state: giteaReady ? (dryRun ? "planned" : "ready") : "failed", mutation: gitea.mutation === true },
{ id: "pac-controller", ok: applyReady, state: repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
{ id: "tekton-consumer", ok: applyReady, state: repositoryMissing ? "waiting-for-gitea-confirm" : applyReady ? (dryRun ? "planned" : "ready") : "failed", mutation: apply.mutation === true },
{ id: "gitops-argo", ok: applyReady, state: 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<string, unknown>): 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<string, unknown>, gitea: Record<string, unknown>, apply: Record<string, unknown>, remote: Record<string, unknown>): Record<string, unknown>[] {
const blockers: Record<string, unknown>[] = [];
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<string, unknown>, blockers: Record<string, unknown>[]): Record<string, unknown> {
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, unknown>[]): 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<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record<string, unknown> : {};
}
function records(value: unknown): Record<string, unknown>[] {
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, "");
+22 -43
View File
@@ -28,9 +28,14 @@ import {
} from "./platform-infra-pipelines-as-code-source-artifact";
import { pacAdmissionDesiredIdentity } from "./platform-infra-pac-provenance";
import { pacConsumerRbacDesiredIdentity, renderPacConsumerRbacDesiredFragment } from "./platform-infra-pac-consumer-rbac";
import { runPlatformInfraGiteaCommand } from "./platform-infra-gitea";
import { runGiteaMirrorBootstrapPreflight, runPlatformInfraGiteaCommand } from "./platform-infra-gitea";
import { readGiteaConfig } from "./platform-infra-gitea-config";
import { resolvePacBootstrapMirrorRepository } from "./platform-infra-pipelines-as-code-bootstrap";
import {
pacBootstrapYamlMatchFailure,
projectPacBootstrapResult,
renderPacBootstrap,
resolvePacBootstrapMirrorRepository,
} from "./platform-infra-pipelines-as-code-bootstrap";
const configFile = rootPath("config", "platform-infra", "pipelines-as-code.yaml");
const configLabel = "config/platform-infra/pipelines-as-code.yaml";
@@ -255,7 +260,7 @@ export async function runPlatformInfraPipelinesAsCodeCommand(config: UniDeskConf
if (action === "bootstrap") {
const options = parseApplyOptions(args.slice(1));
const result = await bootstrap(config, options);
return options.json || options.full || options.raw ? result : renderBootstrap(result);
return options.full || options.raw ? result : options.json ? projectPacBootstrapResult(result) : renderPacBootstrap(result);
}
if (action === "status") {
const options = parseCommonOptions(args.slice(1));
@@ -974,11 +979,20 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
throw new Error(`Pipelines-as-Code consumer ${consumer.id} belongs to ${consumer.node}, not target ${target.id}`);
}
const repository = resolveRepository(pac, consumer.repositoryRef);
const mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, repository);
let mirror: ReturnType<typeof resolvePacBootstrapMirrorRepository>;
try {
mirror = resolvePacBootstrapMirrorRepository(readGiteaConfig(), target.id, repository);
} catch (error) {
return pacBootstrapYamlMatchFailure(target.id, consumer.id, error);
}
const githubPreflight = record(await runGiteaMirrorBootstrapPreflight(config, target.id, mirror.key));
if (githubPreflight.ok !== true) {
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, { ok: false, mutation: false, mode: "skipped", githubPreflight, valuesPrinted: false }, { ok: false, mutation: false, mode: "skipped", valuesPrinted: false });
}
const giteaArgs = ["mirror", "bootstrap", "--target", target.id, "--repo", mirror.key, ...(options.confirm ? ["--confirm"] : []), "--full"];
const giteaBootstrap = record(await runPlatformInfraGiteaCommand(config, giteaArgs));
if (options.confirm && giteaBootstrap.ok !== true) {
return bootstrapResult(target, consumer, repository, mirror, options, giteaBootstrap, {
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, {
ok: false,
mutation: false,
mode: "skipped",
@@ -987,7 +1001,7 @@ async function bootstrap(config: UniDeskConfig, options: ApplyOptions): Promise<
});
}
const pacApply = await apply(config, options);
return bootstrapResult(target, consumer, repository, mirror, options, giteaBootstrap, pacApply);
return bootstrapResult(target, consumer, repository, mirror, options, githubPreflight, giteaBootstrap, pacApply);
}
function bootstrapResult(
@@ -996,6 +1010,7 @@ function bootstrapResult(
repository: PacRepository,
mirror: ReturnType<typeof resolvePacBootstrapMirrorRepository>,
options: ApplyOptions,
githubPreflight: Record<string, unknown>,
giteaBootstrap: Record<string, unknown>,
pacApply: Record<string, unknown>,
): Record<string, unknown> {
@@ -1020,6 +1035,7 @@ function bootstrapResult(
repo: mirror.gitea.name,
valuesPrinted: false,
},
githubPreflight,
steps: [
{ id: "gitea-repository", ok: giteaBootstrap.ok === true, mutation: giteaBootstrap.mutation === true, mode: giteaBootstrap.mode, valuesPrinted: false },
{ id: "pac-tekton-gitops", ok: pacReady, mutation: pacApply.mutation === true, mode: pacApply.mode, repositoryMissing, valuesPrinted: false },
@@ -2207,43 +2223,6 @@ function renderApply(result: Record<string, unknown>): RenderedCliResult {
return rendered(result, "platform-infra pipelines-as-code apply", lines);
}
function renderBootstrap(result: Record<string, unknown>): RenderedCliResult {
const target = record(result.target);
const consumer = record(result.consumer);
const mirror = record(result.mirrorRepository);
const steps = arrayRecords(result.steps);
const pacApply = record(result.pipelinesAsCodeApply);
const pacRemote = record(pacApply.remote);
const giteaBootstrap = record(result.giteaBootstrap);
const next = record(result.next);
const errorValue = [pacRemote.error, giteaBootstrap.error, pacApply.error]
.find((value): value is string => typeof value === "string" && value.length > 0) ?? "";
const error = errorValue.length === 0 ? "" : compactLine(errorValue);
const lines = [
"PLATFORM-INFRA PAC / TEKTON / GITOPS BOOTSTRAP",
...table(["TARGET", "CONSUMER", "MODE", "MUTATION", "OK"], [[stringValue(target.id), stringValue(consumer.id), stringValue(result.mode), boolText(result.mutation), boolText(result.ok)]]),
"",
"SOURCE AUTHORITY",
...table(["GITEA_KEY", "GITEA_REPOSITORY", "UPSTREAM", "BRANCH"], [[stringValue(mirror.key), `${stringValue(mirror.owner)}/${stringValue(mirror.repo)}`, stringValue(mirror.upstreamRepository), stringValue(mirror.upstreamBranch)]]),
"",
"STEPS",
...table(["STEP", "OK", "MODE", "MUTATION", "DETAIL"], steps.map((step) => [
stringValue(step.id),
boolText(step.ok),
stringValue(step.mode),
boolText(step.mutation),
step.repositoryMissing === true ? "repository-will-be-created-on-confirm" : "ready",
])),
...(error.length === 0 ? [] : ["", `ERROR ${error}`]),
"",
"NEXT",
...(result.mode === "dry-run"
? [` confirm: ${stringValue(next.confirm)}`, ` boundary: ${stringValue(next.boundary)}`]
: [` delivery: ${stringValue(next.deliveryTrigger)}`, ` investigate: ${stringValue(next.investigate)}`]),
];
return rendered(result, "platform-infra pipelines-as-code bootstrap", lines);
}
export function renderStatus(result: Record<string, unknown>): RenderedCliResult {
const summary = record(result.summary);
const consumer = record(result.consumer);