// 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> { 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 `, }, }; } 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 { return installAsync({ target: ciTarget(targetId ?? null), skipPrewarm: false, skipTektonInstall: false, wait: false, }); }