Files
pikasTech-unidesk/scripts/src/ci/install.ts
T
2026-07-09 10:46:55 +02:00

162 lines
7.0 KiB
TypeScript

// 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<Record<string, unknown>> {
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<string, unknown> {
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<string, unknown> {
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"],
};
}