188 lines
9.4 KiB
TypeScript
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,
|
|
});
|
|
}
|