Files
pikasTech-unidesk/scripts/src/ci/entry.ts
T
2026-06-28 03:56:44 +00:00

188 lines
9.4 KiB
TypeScript

// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. entry 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:3410-3600 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 { deploySshIdentityPlan, 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 { d601K3sGuardShellLines, d601NativeKubeconfig } from "../d601-k3s-guard";
import { runSshCommandCapture } from "../ssh";
import { cleanupFailedPods, cleanupRuns, status } from "./cleanup";
import { makeRunId, resolveDeployDevManifest, runDevE2E } from "./dev-e2e";
import { ciHelp, requireRunId } from "./help";
import { install, installAsync, installStatus } from "./install";
import { logs } from "./logs";
import { blockedArtifactResult, blockedReason, boolFlag, ciCleanupFailedPodsOptions, ciCleanupRunsOptions, ciLogsOptions, isHelpArg, numberOption, publishTransportOption, requireDesiredRef, requireFullCommit, requireRepoRelativePath, requireRevision, requireServiceId, resolveCatalogArtifact, stringOption, userServicePublishBoundaryBlock } from "./options";
import { backendCoreArtifactSourceHostPath, userServiceArtifactSourceHostPath } from "./pipelinerun";
import { publishBackendCoreArtifact, publishUserServiceArtifact, run } from "./publish";
import { ciTarget, ciTargetSourceSummary, providerIdOption } from "./types";
export async function runCiCommand(config: UniDeskConfig, args: string[]): Promise<Record<string, unknown>> {
const [action = "status", nameArg] = args;
if (isHelpArg(action) || args.slice(1).some(isHelpArg)) return ciHelp();
if (action === "install") {
const options = {
target: ciTarget(providerIdOption(args)),
skipPrewarm: boolFlag(args, "--skip-prewarm"),
skipTektonInstall: boolFlag(args, "--skip-tekton-install"),
wait: boolFlag(args, "--wait"),
};
return options.wait ? install(config, options) : installAsync(options);
}
if (action === "install-status") return installStatus(nameArg ?? "latest");
if (action === "plan") {
const target = ciTarget(providerIdOption(args));
return {
ok: true,
action: "ci-plan",
mutation: false,
target: ciTargetSourceSummary(target),
configTruth: target.configSource.configPath,
next: {
status: `bun scripts/cli.ts ci status --target ${target.targetId}`,
installDryRun: `bun scripts/cli.ts ci install --target ${target.targetId} --skip-prewarm --skip-tekton-install`,
run: `bun scripts/cli.ts ci run --target ${target.targetId} --revision <commit>`,
},
};
}
if (action === "status") return status(ciTarget(providerIdOption(args)));
if (action === "github-ssh-identity") {
const subAction = nameArg ?? "plan";
const target = providerIdOption(args);
const identityId = stringOption(args, "--identity") ?? stringOption(args, "--identity-id");
if (subAction === "plan") return deploySshIdentityPlan(target, identityId);
if (subAction === "ensure") {
if (!boolFlag(args, "--confirm")) {
const confirmArgs = [
"bun scripts/cli.ts ci github-ssh-identity ensure",
target === null ? "" : `--target ${target}`,
identityId === null ? "" : `--identity ${identityId}`,
"--confirm",
].filter((item) => item.length > 0).join(" ");
return {
ok: false,
action: "deploy-ssh-identity-ensure",
mutation: false,
mode: "confirm-required",
plan: deploySshIdentityPlan(target, identityId),
next: {
confirm: confirmArgs,
},
};
}
return await ensureGithubSshIdentityForProvider(config, target ?? "", identityId);
}
throw new Error("ci github-ssh-identity must be one of: plan, ensure");
}
if (action === "run") {
const target = ciTarget(providerIdOption(args));
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
const revision = requireRevision(stringOption(args, "--revision") ?? stringOption(args, "--commit"));
const waitMs = numberOption(args, "--wait-ms", 0);
return run({ repoUrl, revision, waitMs, target });
}
if (action === "publish-backend-core") {
if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
throw new Error("ci publish-backend-core reads source repo from CI.json; edit CI.json instead of using --repo");
}
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
const waitMs = numberOption(args, "--wait-ms", 0);
const dryRun = boolFlag(args, "--dry-run");
const artifact = resolveCatalogArtifact("backend-core");
if (artifact.kind !== "source-build") throw new Error("backend-core must be modeled as a source-build artifact in CI.json");
if (artifact.status === "blocked") return blockedArtifactResult(artifact, commit, blockedReason(artifact));
return publishBackendCoreArtifact(config, {
repoUrl: artifact.source.repo,
commit,
waitMs,
sourceHostPath: backendCoreArtifactSourceHostPath(commit),
dockerfile: artifact.source.dockerfile,
imageRepository: artifact.image.repository,
dryRun,
});
}
if (action === "publish-user-service") {
const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
const waitMs = numberOption(args, "--wait-ms", 0);
const dryRun = boolFlag(args, "--dry-run");
const transport = publishTransportOption(stringOption(args, "--transport"));
if (stringOption(args, "--repo") !== null || stringOption(args, "--repo-url") !== null) {
throw new Error("ci publish-user-service reads source repo from CI.json; edit CI.json instead of using --repo");
}
const artifact = resolveCatalogArtifact(serviceId);
if (artifact.kind === "source-build" && artifact.serviceId === "backend-core") {
throw new Error("backend-core uses ci publish-backend-core; publish-user-service is for registered user services");
}
if (artifact.kind === "upstream-image") {
return blockedArtifactResult(artifact, commit, artifact.blockedReason);
}
if (artifact.status === "blocked") {
return blockedArtifactResult(artifact, commit, blockedReason(artifact));
}
const repoUrl = artifact.source.repo;
const dockerfile = requireRepoRelativePath(artifact.source.dockerfile, `CI.json.artifacts.${serviceId}.source.dockerfile`);
const boundaryBlock = userServicePublishBoundaryBlock(config, serviceId, commit, artifact);
if (boundaryBlock !== null) return boundaryBlock;
return publishUserServiceArtifact(config, {
repoUrl,
commit,
waitMs,
serviceId,
dockerfile,
imageRepository: artifact.image.repository,
sourceHostPath: userServiceArtifactSourceHostPath(serviceId, commit),
dryRun,
transport,
});
}
if (action === "run-dev-e2e") {
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
const desiredRef = requireDesiredRef(stringOption(args, "--desired-ref") ?? stringOption(args, "--deploy-branch"));
const manifest = resolveDeployDevManifest(desiredRef);
const waitMs = numberOption(args, "--wait-ms", 0);
const runId = requireRunId(stringOption(args, "--run-id") ?? makeRunId(manifest.deployCommit));
return runDevE2E({
repoUrl,
desiredRef,
deployCommit: manifest.deployCommit,
environment: manifest.environment,
scriptRepo: manifest.ci.repo,
scriptPath: manifest.ci.scriptPath,
scriptTimeoutMs: manifest.ci.timeoutMs,
services: manifest.services,
runId,
keepNamespace: boolFlag(args, "--keep-namespace"),
waitMs,
});
}
if (action === "logs") return logs(config, nameArg ?? "", ciTarget(providerIdOption(args)), ciLogsOptions(args));
if (action === "cleanup-runs") return cleanupRuns(config, ciCleanupRunsOptions(args));
if (action === "cleanup-failed-pods") return cleanupFailedPods(config, ciCleanupFailedPodsOptions(args));
throw new Error("ci command must be one of: install, plan, status, run, publish-backend-core, publish-user-service, run-dev-e2e, logs, cleanup-runs, cleanup-failed-pods");
}
export function startCiInstallJob(targetId?: string): Record<string, unknown> {
return installAsync({
target: ciTarget(targetId ?? null),
skipPrewarm: false,
skipTektonInstall: false,
wait: false,
});
}