// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. install module for scripts/src/ci.ts. // SPEC: PJ2026-01060308 cicd-yaml-targets draft-2026-06-25-cicd-yaml-targets. // Moved mechanically from scripts/src/ci.ts:1429-1559 for #903. import { randomUUID } from "node:crypto"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join, posix as posixPath } from "node:path"; import { blockedCatalogArtifactIds, catalogSummary, findCiCatalogArtifact, loadCiCatalog, supportedSourceBuildArtifactIds, type CiCatalogArtifact, type CiSourceBuildCatalogArtifact, type CiUpstreamImageCatalogArtifact } from "../ci-catalog"; import { runCommand } from "../command"; import { type UniDeskConfig, repoRoot, rootPath } from "../config"; import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "../deploy-ssh-identity"; import { jobWithTail, listJobs, readJob, startJob } from "../jobs"; import { coreInternalFetch } from "../microservices"; import { artifactRegistryReadonlyResultFromCommand, buildArtifactRegistryReadonlyProbe, parseArtifactRegistryOptions, type ArtifactRegistryReadonlyProbe, } from "../artifact-registry"; import { k3sGuardShellLines, defaultNativeKubeconfig } from "../k3s-target-guard"; import { runSshCommandCapture } from "../ssh"; import type { CiInstallOptions } from "./types"; import { status } from "./cleanup"; import { ciTargetGuardShellLines, shellQuote } from "./options"; import { emitCiInstallProgress } from "./preflight"; import { prewarmCiRuntimeImages, remoteApplyManifest, runRemoteBackground } from "./remote"; import { tektonPipelineReleaseUrl, tektonTriggersInterceptorsUrl, tektonTriggersReleaseUrl } from "./types"; export async function install(config: UniDeskConfig, options: CiInstallOptions): Promise> { if (!existsSync(rootPath(options.target.pipelineManifest))) { throw new Error("CI manifests are missing"); } emitCiInstallProgress("install", "started", { providerId: options.target.providerId, skipPrewarm: options.skipPrewarm, skipTektonInstall: options.skipTektonInstall, }); try { if (options.skipPrewarm) { emitCiInstallProgress("prewarm", "skipped", { providerId: options.target.providerId }); } else { emitCiInstallProgress("prewarm", "started", { providerId: options.target.providerId }); await prewarmCiRuntimeImages(options.target); emitCiInstallProgress("prewarm", "succeeded", { providerId: options.target.providerId }); } if (options.skipTektonInstall) { emitCiInstallProgress("install-tekton", "skipped", { providerId: options.target.providerId }); } else { emitCiInstallProgress("install-tekton", "started", { providerId: options.target.providerId }); const installTektonScript = [ "set -euo pipefail", ...ciTargetGuardShellLines(options.target), `kubectl apply -f ${shellQuote(tektonPipelineReleaseUrl)}`, "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s", `kubectl apply -f ${shellQuote(tektonTriggersReleaseUrl)}`, `kubectl apply -f ${shellQuote(tektonTriggersInterceptorsUrl)}`, "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines --timeout=900s", "kubectl wait --for=condition=Available deployment --all -n tekton-pipelines-resolvers --timeout=900s", ].join("\n"); const installTekton = await runRemoteBackground("install-tekton", installTektonScript, 1_200_000, options.target); if (!installTekton.ok) throw new Error(`Tekton install failed: ${installTekton.stderr || installTekton.stdout}`); emitCiInstallProgress("install-tekton", "succeeded", { providerId: options.target.providerId }); } for (const manifest of [ "src/components/microservices/k3sctl-adapter/k3s/ci/tekton-install.yaml", options.target.pipelineManifest, "src/components/microservices/k3sctl-adapter/k3s/ci/unidesk-ci.triggers.yaml", ]) { emitCiInstallProgress("apply-manifest", "started", { providerId: options.target.providerId, manifest }); await remoteApplyManifest(config, manifest, options.target); emitCiInstallProgress("apply-manifest", "succeeded", { providerId: options.target.providerId, manifest }); } emitCiInstallProgress("status", "started", { providerId: options.target.providerId }); const summary = await status(options.target); emitCiInstallProgress("status", "succeeded", { providerId: options.target.providerId }); emitCiInstallProgress("install", "succeeded", { providerId: options.target.providerId }); return { ...summary, install: { providerId: options.target.providerId, prewarm: options.skipPrewarm ? "skipped" : "completed", tektonInstall: options.skipTektonInstall ? "skipped" : "completed", }, }; } catch (error) { emitCiInstallProgress("install", "failed", { providerId: options.target.providerId, errorTail: (error instanceof Error ? error.message : String(error)).slice(-1200), }); throw error; } } export function ciInstallCommand(options: CiInstallOptions): string[] { return [ "bun", "scripts/cli.ts", "ci", "install", "--target", options.target.targetId, "--wait", ...(options.skipPrewarm ? ["--skip-prewarm"] : []), ...(options.skipTektonInstall ? ["--skip-tekton-install"] : []), ]; } export function installAsync(options: CiInstallOptions): Record { const command = ciInstallCommand(options); const job = startJob( "ci_install", command, `Install/refresh Tekton CI on ${options.target.providerId} native k3s`, ); return { ok: true, mode: "async", providerId: options.target.providerId, skipped: { prewarm: options.skipPrewarm, tektonInstall: options.skipTektonInstall, }, job: { id: job.id, status: job.status, command: job.command, stdoutFile: job.stdoutFile, stderrFile: job.stderrFile, note: job.note, }, next: [ `bun scripts/cli.ts ci install-status ${job.id}`, `bun scripts/cli.ts job status ${job.id} --tail-bytes 12000`, ], boundary: "ci install is fire-and-forget by default; use --wait only for explicit synchronous debugging.", }; } export function latestCiInstallJobId(): string { const job = listJobs().find((item) => item.name === "ci_install"); if (job === undefined) throw new Error("no ci_install job found"); return job.id; } export function installStatus(id: string): Record { const jobId = id === "latest" || id.length === 0 ? latestCiInstallJobId() : id; const job = readJob(jobId); if (job.name !== "ci_install") { throw new Error(`job ${jobId} is ${job.name}, not ci_install`); } return { ok: job.status !== "failed" && job.status !== "canceled", job: jobWithTail(job, 12_000), next: job.status === "running" || job.status === "queued" ? [`bun scripts/cli.ts ci install-status ${job.id}`] : ["bun scripts/cli.ts ci status"], }; }