220 lines
10 KiB
TypeScript
220 lines
10 KiB
TypeScript
// SPEC: PJ2026-01060307 控制面模块化 draft-2026-06-25-p0. direct-docker module for scripts/src/ci.ts.
|
|
|
|
// Moved mechanically from scripts/src/ci.ts:2125-2316 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 { ArtifactSummary, ArtifactSummaryContext, CiPublishUserServiceArtifactOptions } from "./types";
|
|
import { artifactSummaryDefaults, assertCommandOk, directDockerRegistryCurl, dockerArtifactDigest, registryManifestDigest } from "./artifact-summary";
|
|
import { assertArtifactSummaryComplete } from "./publish-preflight";
|
|
import { codeQueueDirectDockerBaseImage, providerGatewayWsEgressProxyUrl } from "./types";
|
|
|
|
export function buildContextForService(serviceId: string, dockerfile: string): string {
|
|
return serviceId === "claudeqq" ? posixPath.dirname(dockerfile) : ".";
|
|
}
|
|
|
|
export function directDockerBuildNetworkArgs(serviceId: string): string[] {
|
|
if (serviceId !== "code-queue") return [];
|
|
const noProxy = [
|
|
"localhost",
|
|
"127.0.0.1",
|
|
"::1",
|
|
"host.docker.internal",
|
|
"d601-tcp-egress-gateway",
|
|
"d601-tcp-egress-gateway.unidesk",
|
|
"d601-tcp-egress-gateway.unidesk.svc",
|
|
"d601-tcp-egress-gateway.unidesk.svc.cluster.local",
|
|
"backend-core",
|
|
"oa-event-flow",
|
|
"database",
|
|
].join(",");
|
|
return [
|
|
"--network",
|
|
"host",
|
|
"--build-arg",
|
|
`HTTP_PROXY=${providerGatewayWsEgressProxyUrl}`,
|
|
"--build-arg",
|
|
`HTTPS_PROXY=${providerGatewayWsEgressProxyUrl}`,
|
|
"--build-arg",
|
|
`http_proxy=${providerGatewayWsEgressProxyUrl}`,
|
|
"--build-arg",
|
|
`https_proxy=${providerGatewayWsEgressProxyUrl}`,
|
|
"--build-arg",
|
|
`NO_PROXY=${noProxy}`,
|
|
"--build-arg",
|
|
`no_proxy=${noProxy}`,
|
|
];
|
|
}
|
|
|
|
export function directDockerSourceGitMode(repoUrl: string): "local-unidesk-worktree" | "git-archive" {
|
|
return repoUrl === "https://github.com/pikasTech/unidesk" ? "local-unidesk-worktree" : "git-archive";
|
|
}
|
|
|
|
export async function prepareDirectDockerUserServiceSource(options: CiPublishUserServiceArtifactOptions): Promise<{ path: string; cleanup: () => void; summary: Record<string, unknown> }> {
|
|
const tempRoot = mkdtempSync(join(tmpdir(), `unidesk-ci-${options.serviceId}-${options.commit.slice(0, 8)}-`));
|
|
const sourcePath = join(tempRoot, "source");
|
|
const gitMode = directDockerSourceGitMode(options.repoUrl);
|
|
if (gitMode !== "local-unidesk-worktree") {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
throw new Error("direct-docker publish currently supports only UniDesk repo-owned source-build artifacts; use --transport tekton for external repositories");
|
|
}
|
|
const resolved = runCommand(["git", "rev-parse", "--verify", `${options.commit}^{commit}`], repoRoot, { timeoutMs: 30_000 });
|
|
assertCommandOk(resolved, "resolve source commit");
|
|
if (resolved.stdout.trim() !== options.commit) {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
throw new Error(`direct-docker source commit mismatch: resolved ${resolved.stdout.trim()} expected ${options.commit}`);
|
|
}
|
|
const dockerfileExists = runCommand(["git", "cat-file", "-e", `${options.commit}:${options.dockerfile}`], repoRoot, { timeoutMs: 30_000 });
|
|
assertCommandOk(dockerfileExists, "verify source dockerfile");
|
|
const worktree = runCommand(["git", "worktree", "add", "--detach", sourcePath, options.commit], repoRoot, { timeoutMs: 120_000 });
|
|
if (worktree.exitCode !== 0) {
|
|
rmSync(tempRoot, { recursive: true, force: true });
|
|
throw new Error(`prepare source worktree failed: ${worktree.stderr || worktree.stdout}`);
|
|
}
|
|
return {
|
|
path: sourcePath,
|
|
cleanup: () => {
|
|
const removed = runCommand(["git", "worktree", "remove", "--force", sourcePath], repoRoot, { timeoutMs: 60_000 });
|
|
if (removed.exitCode !== 0) rmSync(tempRoot, { recursive: true, force: true });
|
|
else rmSync(tempRoot, { recursive: true, force: true });
|
|
},
|
|
summary: {
|
|
ok: true,
|
|
mode: gitMode,
|
|
providerId: "local",
|
|
repoUrl: options.repoUrl,
|
|
commit: options.commit,
|
|
serviceId: options.serviceId,
|
|
dockerfile: options.dockerfile,
|
|
sourceHostPath: sourcePath,
|
|
valuesPrinted: false,
|
|
},
|
|
};
|
|
}
|
|
|
|
export async function publishUserServiceArtifactDirectDocker(options: CiPublishUserServiceArtifactOptions, context: ArtifactSummaryContext): Promise<Record<string, unknown>> {
|
|
const source = await prepareDirectDockerUserServiceSource(options);
|
|
const planned = artifactSummaryDefaults(context);
|
|
const localImage = `${options.imageRepository}:${options.commit}`;
|
|
const buildContext = buildContextForService(options.serviceId, options.dockerfile);
|
|
const networkArgs = directDockerBuildNetworkArgs(options.serviceId);
|
|
const baseArgs = options.serviceId === "code-queue" && runCommand(["docker", "image", "inspect", codeQueueDirectDockerBaseImage], repoRoot, { timeoutMs: 30_000 }).exitCode === 0
|
|
? ["--build-arg", `CODE_QUEUE_BASE_IMAGE=${codeQueueDirectDockerBaseImage}`]
|
|
: [];
|
|
try {
|
|
if (options.serviceId === "code-queue" && baseArgs.length === 0) {
|
|
return {
|
|
ok: false,
|
|
runnerDisposition: "infra-blocked",
|
|
failureClassification: "ci-runner-not-ready",
|
|
failureKind: "code-queue-base-image-missing",
|
|
serviceId: options.serviceId,
|
|
commit: options.commit,
|
|
artifactSummary: planned,
|
|
source: source.summary,
|
|
artifact: planned.imageRef,
|
|
transport: "direct-docker",
|
|
controlledPublish: {
|
|
environment: "DEV-local-artifact",
|
|
command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --transport direct-docker`,
|
|
noRollout: true,
|
|
noRuntimeMutation: true,
|
|
},
|
|
boundary: "direct-docker CI artifact publish builds and pushes a commit-pinned image only; no deploy apply, no rollout, no Code Queue restart, no active task mutation",
|
|
next: [
|
|
"Restore or warm local unidesk-code-queue:d601 base image, then rerun the same direct-docker publish command.",
|
|
],
|
|
};
|
|
}
|
|
const build = runCommand([
|
|
"docker",
|
|
"build",
|
|
"--label",
|
|
`unidesk.ai/service-id=${options.serviceId}`,
|
|
"--label",
|
|
`unidesk.ai/source-repo=${options.repoUrl}`,
|
|
"--label",
|
|
`unidesk.ai/source-commit=${options.commit}`,
|
|
"--label",
|
|
`unidesk.ai/dockerfile=${options.dockerfile}`,
|
|
...networkArgs,
|
|
...baseArgs,
|
|
"-t",
|
|
localImage,
|
|
"-t",
|
|
planned.imageRef,
|
|
"-f",
|
|
options.dockerfile,
|
|
buildContext,
|
|
], source.path, { timeoutMs: Math.max(options.waitMs, 20 * 60_000) });
|
|
assertCommandOk(build, "direct docker build");
|
|
const inspectLabels = runCommand(["docker", "image", "inspect", planned.imageRef, "--format", "{{ index .Config.Labels \"unidesk.ai/service-id\" }} {{ index .Config.Labels \"unidesk.ai/source-commit\" }}"], repoRoot, { timeoutMs: 30_000 });
|
|
assertCommandOk(inspectLabels, "inspect built image labels");
|
|
if (!inspectLabels.stdout.includes(`${options.serviceId} ${options.commit}`)) throw new Error(`direct docker image labels did not match ${options.serviceId}/${options.commit}`);
|
|
const registryCheck = directDockerRegistryCurl(["-fsS", "http://127.0.0.1:5000/v2/"], 15_000);
|
|
assertCommandOk(registryCheck, "D601 loopback artifact registry health");
|
|
const push = runCommand(["docker", "push", planned.imageRef], repoRoot, { timeoutMs: Math.max(options.waitMs, 10 * 60_000) });
|
|
assertCommandOk(push, "direct docker push");
|
|
const pull = runCommand(["docker", "pull", planned.imageRef], repoRoot, { timeoutMs: 120_000 });
|
|
assertCommandOk(pull, "direct docker pull verification");
|
|
const digest = dockerArtifactDigest(planned.repository, planned.imageRef) ?? registryManifestDigest(planned.repository, planned.tag);
|
|
const artifact: ArtifactSummary = {
|
|
...planned,
|
|
digest,
|
|
digestRef: digest === null ? null : `${planned.repository}@${digest}`,
|
|
};
|
|
assertArtifactSummaryComplete(artifact, "direct-docker");
|
|
return {
|
|
ok: true,
|
|
transport: "direct-docker",
|
|
pipelineRun: null,
|
|
namespace: null,
|
|
repoUrl: options.repoUrl,
|
|
commit: options.commit,
|
|
serviceId: options.serviceId,
|
|
source: source.summary,
|
|
artifact: artifact.imageRef,
|
|
artifactSummary: artifact,
|
|
controlledPublish: {
|
|
environment: "DEV-local-artifact",
|
|
command: `bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --transport direct-docker`,
|
|
noRollout: true,
|
|
noRuntimeMutation: true,
|
|
dependsOnLocalUnideskDatabase: false,
|
|
},
|
|
boundary: "direct-docker CI artifact publish builds and pushes a commit-pinned image only; no deploy apply, no rollout, no Code Queue restart, no active task mutation",
|
|
wait: {
|
|
ok: true,
|
|
dispatchOk: null,
|
|
dispatchStatus: null,
|
|
dispatchExitCode: null,
|
|
stdoutTail: push.stdout.slice(-6000),
|
|
stderrTail: push.stderr.slice(-6000),
|
|
},
|
|
condition: null,
|
|
next: [
|
|
"use artifactSummary.imageRef or artifactSummary.digestRef as later dev artifact consumer input",
|
|
],
|
|
};
|
|
} finally {
|
|
source.cleanup();
|
|
}
|
|
}
|