feat: standardize user service artifact producer
This commit is contained in:
+341
-5
@@ -1,7 +1,8 @@
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { existsSync, readFileSync } from "node:fs";
|
||||
import { posix as posixPath } from "node:path";
|
||||
import { runCommand } from "./command";
|
||||
import { type UniDeskConfig, repoRoot, rootPath } from "./config";
|
||||
import { type UniDeskConfig, type UniDeskMicroserviceConfig, repoRoot, rootPath } from "./config";
|
||||
import { ensureGithubSshIdentityForProvider, gitSshHttpConnectProxySource } from "./deploy-ssh-identity";
|
||||
import { startJob } from "./jobs";
|
||||
import { coreInternalFetch } from "./microservices";
|
||||
@@ -42,6 +43,16 @@ interface CiPublishBackendCoreOptions {
|
||||
sourceHostPath: string;
|
||||
}
|
||||
|
||||
interface CiPublishUserServiceArtifactOptions {
|
||||
repoUrl: string;
|
||||
commit: string;
|
||||
waitMs: number;
|
||||
sourceHostPath: string;
|
||||
serviceId: string;
|
||||
dockerfile: string;
|
||||
dryRun: boolean;
|
||||
}
|
||||
|
||||
interface CiDevE2EOptions {
|
||||
repoUrl: string;
|
||||
desiredRef: string;
|
||||
@@ -92,6 +103,19 @@ interface DeployDevManifestSummary {
|
||||
services: Array<{ id: string; commitId: string; repo: string }>;
|
||||
}
|
||||
|
||||
interface ArtifactSummary {
|
||||
serviceId: string;
|
||||
sourceCommit: string;
|
||||
sourceRepo: string;
|
||||
dockerfile: string;
|
||||
registry: string;
|
||||
repository: string;
|
||||
tag: string;
|
||||
imageRef: string;
|
||||
digest: string | null;
|
||||
digestRef: string | null;
|
||||
}
|
||||
|
||||
function stringOption(args: string[], name: string): string | null {
|
||||
const index = args.indexOf(name);
|
||||
if (index === -1) return null;
|
||||
@@ -120,6 +144,14 @@ function requireFullCommit(value: string | null, option = "--commit"): string {
|
||||
return commit;
|
||||
}
|
||||
|
||||
function requireServiceId(value: string | null): string {
|
||||
const serviceId = value ?? "";
|
||||
if (!/^[a-z0-9]([-a-z0-9]{0,62}[a-z0-9])?$/u.test(serviceId)) {
|
||||
throw new Error("ci publish-user-service requires --service <dns-safe-user-service-id>");
|
||||
}
|
||||
return serviceId;
|
||||
}
|
||||
|
||||
function requireDesiredRef(value: string | null): string {
|
||||
const ref = value ?? "master";
|
||||
if (!/^[A-Za-z0-9._/-]{1,160}$/u.test(ref) || ref.startsWith("-") || ref.includes("..")) {
|
||||
@@ -149,6 +181,30 @@ function repoSshUrl(repoUrl: string): string {
|
||||
return repoUrl;
|
||||
}
|
||||
|
||||
function requireRepoRelativePath(path: string, label: string): string {
|
||||
if (path.length === 0 || path.startsWith("/") || path.includes("\0") || path.split("/").includes("..")) {
|
||||
throw new Error(`${label} must be a repo-relative path`);
|
||||
}
|
||||
const normalized = posixPath.normalize(path);
|
||||
if (normalized === "." || normalized.startsWith("../")) throw new Error(`${label} must be a repo-relative path`);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function requireSupportedUserService(config: UniDeskConfig, serviceId: string): UniDeskMicroserviceConfig {
|
||||
if (serviceId === "backend-core") {
|
||||
throw new Error("backend-core uses ci publish-backend-core; publish-user-service is for registered user services");
|
||||
}
|
||||
const service = config.microservices.find((item) => item.id === serviceId);
|
||||
if (service === undefined) throw new Error(`unknown user service: ${serviceId}`);
|
||||
if (service.providerId !== d601ProviderId || service.development.providerId !== d601ProviderId) {
|
||||
throw new Error(`ci publish-user-service currently supports only D601 user services; ${serviceId} is assigned to ${service.providerId}`);
|
||||
}
|
||||
if (service.deployment.mode !== "k3sctl-managed") {
|
||||
throw new Error(`ci publish-user-service currently supports k3sctl-managed D601 user services; ${serviceId} uses ${service.deployment.mode}`);
|
||||
}
|
||||
return service;
|
||||
}
|
||||
|
||||
function chunks(value: string, size: number): string[] {
|
||||
const result: string[] = [];
|
||||
for (let index = 0; index < value.length; index += size) {
|
||||
@@ -542,10 +598,49 @@ spec:
|
||||
`;
|
||||
}
|
||||
|
||||
function userServiceArtifactPipelineRunManifest(options: CiPublishUserServiceArtifactOptions): string {
|
||||
const safeSuffix = new Date().toISOString().replace(/[-:.TZ]/g, "").slice(0, 14).toLowerCase();
|
||||
return `apiVersion: tekton.dev/v1
|
||||
kind: PipelineRun
|
||||
metadata:
|
||||
generateName: user-service-artifact-${options.serviceId}-${safeSuffix}-
|
||||
namespace: unidesk-ci
|
||||
labels:
|
||||
app.kubernetes.io/name: unidesk-user-service-artifact-publish
|
||||
app.kubernetes.io/part-of: unidesk
|
||||
unidesk.ai/service-id: ${JSON.stringify(options.serviceId)}
|
||||
unidesk.ai/revision: ${JSON.stringify(options.commit)}
|
||||
spec:
|
||||
pipelineRef:
|
||||
name: unidesk-user-service-artifact-publish
|
||||
taskRunTemplate:
|
||||
serviceAccountName: unidesk-ci-runner
|
||||
params:
|
||||
- name: repo-url
|
||||
value: ${JSON.stringify(options.repoUrl)}
|
||||
- name: revision
|
||||
value: ${JSON.stringify(options.commit)}
|
||||
- name: service-id
|
||||
value: ${JSON.stringify(options.serviceId)}
|
||||
- name: dockerfile
|
||||
value: ${JSON.stringify(options.dockerfile)}
|
||||
- name: source-host-path
|
||||
value: ${JSON.stringify(options.sourceHostPath)}
|
||||
workspaces:
|
||||
- name: shared-workspace
|
||||
persistentVolumeClaim:
|
||||
claimName: unidesk-ci-cache
|
||||
`;
|
||||
}
|
||||
|
||||
function backendCoreArtifactSourceHostPath(commit: string): string {
|
||||
return `/home/ubuntu/.unidesk/ci/backend-core-artifacts/${commit}`;
|
||||
}
|
||||
|
||||
function userServiceArtifactSourceHostPath(serviceId: string, commit: string): string {
|
||||
return `/home/ubuntu/.unidesk/ci/user-service-artifacts/${serviceId}/${commit}`;
|
||||
}
|
||||
|
||||
async function prepareBackendCoreArtifactSource(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
const sshIdentity = await ensureGithubSshIdentityForProvider(config, d601ProviderId);
|
||||
if (!sshIdentity.ok) throw new Error(sshIdentity.detail);
|
||||
@@ -613,6 +708,82 @@ async function prepareBackendCoreArtifactSource(config: UniDeskConfig, options:
|
||||
};
|
||||
}
|
||||
|
||||
async function prepareUserServiceArtifactSource(config: UniDeskConfig, options: CiPublishUserServiceArtifactOptions): Promise<Record<string, unknown>> {
|
||||
const sshIdentity = await ensureGithubSshIdentityForProvider(config, d601ProviderId);
|
||||
if (!sshIdentity.ok) throw new Error(sshIdentity.detail);
|
||||
const proxyPython = gitSshHttpConnectProxySource();
|
||||
const sourceRoot = `/home/ubuntu/.unidesk/ci/user-service-artifacts/${options.serviceId}`;
|
||||
const sourceHostPath = options.sourceHostPath;
|
||||
const repoCache = "/home/ubuntu/.unidesk/ci/git/unidesk.git";
|
||||
const repoFetchUrl = repoSshUrl(options.repoUrl);
|
||||
const dockerfileDir = posixPath.dirname(options.dockerfile);
|
||||
const script = [
|
||||
"set -euo pipefail",
|
||||
`service_id=${shellQuote(options.serviceId)}`,
|
||||
`commit=${shellQuote(options.commit)}`,
|
||||
`repo_url=${shellQuote(options.repoUrl)}`,
|
||||
`repo_fetch_url=${shellQuote(repoFetchUrl)}`,
|
||||
`dockerfile=${shellQuote(options.dockerfile)}`,
|
||||
`dockerfile_dir=${shellQuote(dockerfileDir)}`,
|
||||
`source_root=${shellQuote(sourceRoot)}`,
|
||||
`source_dir=${shellQuote(sourceHostPath)}`,
|
||||
`repo_cache=${shellQuote(repoCache)}`,
|
||||
`proxy_url=${shellQuote(providerGatewayWsEgressProxyUrl)}`,
|
||||
"mkdir -p \"$(dirname \"$repo_cache\")\" \"$source_root\"",
|
||||
"export HTTP_PROXY=\"$proxy_url\" HTTPS_PROXY=\"$proxy_url\" ALL_PROXY=\"$proxy_url\"",
|
||||
"export NO_PROXY=\"localhost,127.0.0.1,::1,host.docker.internal,.svc,.cluster.local,kubernetes.default.svc\"",
|
||||
"curl -fsSI --max-time 20 -x \"$proxy_url\" https://github.com >/dev/null",
|
||||
"git_ssh_proxy=/tmp/unidesk-git-ssh-http-connect.py",
|
||||
"cat > \"$git_ssh_proxy\" <<'UNIDESK_GIT_SSH_PROXY'",
|
||||
proxyPython,
|
||||
"UNIDESK_GIT_SSH_PROXY",
|
||||
"chmod 700 \"$git_ssh_proxy\"",
|
||||
"export UNIDESK_GIT_SSH_HTTP_PROXY=\"$proxy_url\"",
|
||||
"export GIT_SSH_COMMAND=\"ssh -o BatchMode=yes -o IdentitiesOnly=yes -o StrictHostKeyChecking=yes -o UserKnownHostsFile=$HOME/.ssh/known_hosts -i $HOME/.ssh/id_ed25519 -o 'ProxyCommand=$git_ssh_proxy %h %p'\"",
|
||||
"echo user_service_artifact_source_proxy=provider-gateway-ws-egress:$proxy_url",
|
||||
"echo user_service_artifact_repo_fetch_url=$repo_fetch_url",
|
||||
"echo user_service_artifact_service_id=$service_id",
|
||||
"if [ ! -d \"$repo_cache\" ]; then git clone --mirror \"$repo_fetch_url\" \"$repo_cache\"; fi",
|
||||
"git -C \"$repo_cache\" remote set-url origin \"$repo_fetch_url\"",
|
||||
"git -C \"$repo_cache\" fetch --no-tags origin \"$commit\" || git -C \"$repo_cache\" fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'",
|
||||
"resolved=$(git -C \"$repo_cache\" rev-parse --verify \"$commit^{commit}\")",
|
||||
"test \"$resolved\" = \"$commit\" || { echo \"user_service_artifact_resolved_commit_mismatch=$resolved expected=$commit\" >&2; exit 1; }",
|
||||
"git -C \"$repo_cache\" cat-file -e \"$commit:$dockerfile\"",
|
||||
"git -C \"$repo_cache\" cat-file -e \"$commit:$dockerfile_dir/src\"",
|
||||
"tmp_dir=\"$source_root/.tmp-$commit-$$\"",
|
||||
"rm -rf \"$tmp_dir\"",
|
||||
"mkdir -p \"$tmp_dir\"",
|
||||
"git -C \"$repo_cache\" archive \"$commit\" | tar -x -C \"$tmp_dir\"",
|
||||
"printf '%s\\n' \"$commit\" > \"$tmp_dir/.unidesk-source-commit\"",
|
||||
"printf '%s\\n' \"$repo_url\" > \"$tmp_dir/.unidesk-source-repo\"",
|
||||
"printf '%s\\n' \"$service_id\" > \"$tmp_dir/.unidesk-service-id\"",
|
||||
"printf '%s\\n' \"$dockerfile\" > \"$tmp_dir/.unidesk-dockerfile\"",
|
||||
"rm -rf \"$source_dir\"",
|
||||
"mv \"$tmp_dir\" \"$source_dir\"",
|
||||
"test -f \"$source_dir/$dockerfile\"",
|
||||
"test -d \"$source_dir/$dockerfile_dir/src\"",
|
||||
"echo user_service_artifact_source_host_path=$source_dir",
|
||||
].join("\n");
|
||||
const result = await runRemoteBackground(`prepare-${options.serviceId}-source`, script, 300_000);
|
||||
if (!result.ok) throw new Error(`failed to prepare ${options.serviceId} source on D601: ${result.stderr || result.stdout || JSON.stringify(result.raw)}`);
|
||||
return {
|
||||
ok: true,
|
||||
mode: "d601-host-github-ssh-export",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
dockerfile: options.dockerfile,
|
||||
sourceHostPath,
|
||||
identity: {
|
||||
fingerprint: sshIdentity.fingerprint,
|
||||
seededFromLocal: sshIdentity.seededFromLocal,
|
||||
},
|
||||
stdoutTail: result.stdout.slice(-4000),
|
||||
};
|
||||
}
|
||||
|
||||
async function remoteCreatePipelineRun(manifest: string): Promise<string> {
|
||||
const encoded = Buffer.from(manifest, "utf8").toString("base64");
|
||||
const token = randomUUID().replace(/-/gu, "").slice(0, 12);
|
||||
@@ -689,6 +860,46 @@ function pipelineRunWaitSucceeded(wait: DispatchResult | null, condition: Pipeli
|
||||
return wait.ok || wait.exitCode === 0 || wait.stdout.includes("condition=True\tSucceeded\t");
|
||||
}
|
||||
|
||||
function parseArtifactSummaryFromOutput(output: string, serviceId: string, commit: string, repoUrl: string, dockerfile: string): ArtifactSummary {
|
||||
const fields = new Map<string, string>();
|
||||
for (const line of output.split(/\r?\n/u)) {
|
||||
const match = /^(user_service_artifact_[a-z_]+|backend_core_artifact_[a-z_]+)=(.*)$/u.exec(line.trim());
|
||||
if (match !== null) fields.set(match[1], match[2]);
|
||||
}
|
||||
const digest = fields.get("user_service_artifact_digest") ?? fields.get("backend_core_artifact_digest") ?? null;
|
||||
const imageRef = fields.get("user_service_artifact_image") ?? fields.get("backend_core_artifact_image") ?? `127.0.0.1:5000/unidesk/${serviceId}:${commit}`;
|
||||
const repository = fields.get("user_service_artifact_repository") ?? `127.0.0.1:5000/unidesk/${serviceId}`;
|
||||
const digestRef = fields.get("user_service_artifact_digest_ref") ?? fields.get("backend_core_artifact_digest_ref") ?? (digest === null || digest.length === 0 ? null : `${repository}@${digest}`);
|
||||
return {
|
||||
serviceId: fields.get("user_service_artifact_service_id") ?? serviceId,
|
||||
sourceCommit: fields.get("user_service_artifact_source_commit") ?? fields.get("backend_core_artifact_source_commit") ?? commit,
|
||||
sourceRepo: fields.get("user_service_artifact_source_repo") ?? repoUrl,
|
||||
dockerfile: fields.get("user_service_artifact_dockerfile") ?? dockerfile,
|
||||
registry: fields.get("user_service_artifact_registry") ?? "127.0.0.1:5000",
|
||||
repository,
|
||||
tag: fields.get("user_service_artifact_tag") ?? commit,
|
||||
imageRef,
|
||||
digest,
|
||||
digestRef,
|
||||
};
|
||||
}
|
||||
|
||||
function assertArtifactSummaryComplete(artifact: ArtifactSummary, pipelineRun: string): void {
|
||||
if (artifact.serviceId.length === 0 || artifact.sourceCommit.length !== 40 || artifact.imageRef.length === 0 || artifact.tag.length === 0 || artifact.digest === null || artifact.digest.length === 0 || artifact.digestRef === null || artifact.digestRef.length === 0) {
|
||||
throw new Error(`artifact summary for ${pipelineRun} is incomplete`);
|
||||
}
|
||||
}
|
||||
|
||||
async function readPipelineRunLogText(name: string): Promise<string> {
|
||||
const result = await runRemoteKubectlRaw([
|
||||
"set -euo pipefail",
|
||||
`kubectl get pipelinerun/${shellQuote(name)} -n unidesk-ci -o wide`,
|
||||
`kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o wide`,
|
||||
`for pod in $(kubectl get pods -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o name); do echo "===== $pod"; kubectl logs -n unidesk-ci "$pod" --all-containers=true --tail=240 || true; done`,
|
||||
].join("\n"), 60_000, 45_000);
|
||||
return `${result.stdout}\n${result.stderr}`.trim();
|
||||
}
|
||||
|
||||
async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
const name = await remoteCreatePipelineRun(pipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
@@ -717,11 +928,27 @@ async function run(options: CiOptions): Promise<Record<string, unknown>> {
|
||||
}
|
||||
|
||||
async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
|
||||
const plannedArtifact: ArtifactSummary = {
|
||||
serviceId: "backend-core",
|
||||
sourceCommit: options.commit,
|
||||
sourceRepo: options.repoUrl,
|
||||
dockerfile: "src/components/backend-core/Dockerfile",
|
||||
registry: "127.0.0.1:5000",
|
||||
repository: "127.0.0.1:5000/unidesk/backend-core",
|
||||
tag: options.commit,
|
||||
imageRef: `127.0.0.1:5000/unidesk/backend-core:${options.commit}`,
|
||||
digest: null,
|
||||
digestRef: null,
|
||||
};
|
||||
const source = await prepareBackendCoreArtifactSource(config, options);
|
||||
const name = await remoteCreatePipelineRun(backendCoreArtifactPipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
const artifact = waitSucceeded && wait !== null
|
||||
? parseArtifactSummaryFromOutput(await readPipelineRunLogText(name), "backend-core", options.commit, options.repoUrl, "src/components/backend-core/Dockerfile")
|
||||
: plannedArtifact;
|
||||
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
@@ -729,7 +956,8 @@ async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPubl
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
source,
|
||||
artifact: `127.0.0.1:5000/unidesk/backend-core:${options.commit}`,
|
||||
artifact: artifact.imageRef,
|
||||
artifactSummary: artifact,
|
||||
boundary: "CI publishes the image to D601 registry; CD must pull it and must not build backend-core",
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
@@ -747,6 +975,84 @@ async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPubl
|
||||
};
|
||||
}
|
||||
|
||||
async function publishUserServiceArtifact(config: UniDeskConfig, options: CiPublishUserServiceArtifactOptions): Promise<Record<string, unknown>> {
|
||||
const plannedArtifact: ArtifactSummary = {
|
||||
serviceId: options.serviceId,
|
||||
sourceCommit: options.commit,
|
||||
sourceRepo: options.repoUrl,
|
||||
dockerfile: options.dockerfile,
|
||||
registry: "127.0.0.1:5000",
|
||||
repository: `127.0.0.1:5000/unidesk/${options.serviceId}`,
|
||||
tag: options.commit,
|
||||
imageRef: `127.0.0.1:5000/unidesk/${options.serviceId}:${options.commit}`,
|
||||
digest: null,
|
||||
digestRef: null,
|
||||
} satisfies ArtifactSummary;
|
||||
if (options.dryRun) {
|
||||
return {
|
||||
ok: true,
|
||||
mode: "dry-run",
|
||||
pipeline: "unidesk-user-service-artifact-publish",
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
source: {
|
||||
ok: true,
|
||||
mode: "planned-only",
|
||||
providerId: d601ProviderId,
|
||||
repoUrl: options.repoUrl,
|
||||
repoFetchUrl: repoSshUrl(options.repoUrl),
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
dockerfile: options.dockerfile,
|
||||
sourceHostPath: options.sourceHostPath,
|
||||
},
|
||||
artifact: plannedArtifact.imageRef,
|
||||
artifactSummary: plannedArtifact,
|
||||
boundary: "dry-run only; no D601 source export, no Tekton submission, no production mutation",
|
||||
next: [
|
||||
`bun scripts/cli.ts ci publish-user-service --service ${options.serviceId} --commit ${options.commit} --wait-ms 1200000`,
|
||||
],
|
||||
};
|
||||
}
|
||||
const source = await prepareUserServiceArtifactSource(config, options);
|
||||
const name = await remoteCreatePipelineRun(userServiceArtifactPipelineRunManifest(options));
|
||||
const wait = await waitForPipelineRun(name, options.waitMs);
|
||||
const condition = wait === null ? null : await readPipelineRunCondition(name);
|
||||
const waitSucceeded = pipelineRunWaitSucceeded(wait, condition);
|
||||
const artifact = waitSucceeded && wait !== null
|
||||
? parseArtifactSummaryFromOutput(await readPipelineRunLogText(name), options.serviceId, options.commit, options.repoUrl, options.dockerfile)
|
||||
: plannedArtifact;
|
||||
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
|
||||
return {
|
||||
ok: waitSucceeded,
|
||||
pipelineRun: name,
|
||||
namespace: "unidesk-ci",
|
||||
repoUrl: options.repoUrl,
|
||||
commit: options.commit,
|
||||
serviceId: options.serviceId,
|
||||
source,
|
||||
artifact: artifact.imageRef,
|
||||
artifactSummary: artifact,
|
||||
boundary: "CI publishes the user-service image to the D601 registry only; it must not deploy production or mutate the production namespace",
|
||||
wait: wait === null ? null : {
|
||||
ok: waitSucceeded,
|
||||
dispatchOk: wait.ok,
|
||||
dispatchStatus: wait.status,
|
||||
dispatchExitCode: wait.exitCode,
|
||||
stdoutTail: wait.stdout.slice(-6000),
|
||||
stderrTail: wait.stderr.slice(-6000),
|
||||
},
|
||||
condition,
|
||||
next: [
|
||||
`bun scripts/cli.ts ci logs ${name}`,
|
||||
"use artifactSummary.imageRef or artifactSummary.digestRef as later dev/prod deployment input",
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async function runRemoteDevE2ELauncher(options: CiDevE2EOptions): Promise<DispatchResult> {
|
||||
const scriptTimeoutMs = Math.max(options.scriptTimeoutMs, options.waitMs, 60_000);
|
||||
const remoteTimeoutMs = 45_000;
|
||||
@@ -995,12 +1301,13 @@ async function logs(name: string): Promise<Record<string, unknown>> {
|
||||
|
||||
export function ciHelp(): Record<string, unknown> {
|
||||
return {
|
||||
command: "ci install|status|run|publish-backend-core|run-dev-e2e|logs",
|
||||
description: "Manage the D601 k3s Tekton CI gate. CI may publish backend-core image artifacts, but it intentionally does not deploy CD.",
|
||||
command: "ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs",
|
||||
description: "Manage the D601 k3s Tekton CI gate. CI may publish commit-pinned image artifacts, but it intentionally does not deploy CD.",
|
||||
examples: [
|
||||
"bun scripts/cli.ts ci install",
|
||||
"bun scripts/cli.ts ci run --revision <commit>",
|
||||
"bun scripts/cli.ts ci publish-backend-core --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha>",
|
||||
"bun scripts/cli.ts ci run-dev-e2e --wait-ms 600000",
|
||||
"bun scripts/cli.ts ci logs <runId>",
|
||||
],
|
||||
@@ -1018,6 +1325,14 @@ export function ciHelp(): Record<string, unknown> {
|
||||
registry: "127.0.0.1:5000/unidesk/backend-core:<commit>",
|
||||
cdCommand: "bun scripts/cli.ts artifact-registry deploy-backend-core --commit <full-sha>",
|
||||
},
|
||||
userServiceArtifact: {
|
||||
producer: "D601 CI",
|
||||
command: "bun scripts/cli.ts ci publish-user-service --service <service-id> --commit <full-sha>",
|
||||
initiallySupportedServices: ["decision-center"],
|
||||
registry: "127.0.0.1:5000/unidesk/<service-id>:<commit>",
|
||||
outputFields: ["imageRef", "tag", "digest", "sourceCommit", "serviceId"],
|
||||
boundary: "artifact producer only; no prod deploy and no production namespace mutation",
|
||||
},
|
||||
runDevE2E: {
|
||||
defaultTriggerMode: "commit-pinned-ssh-launcher",
|
||||
desiredState: "origin/master:deploy.json#environments.dev",
|
||||
@@ -1050,6 +1365,27 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
return publishBackendCoreArtifact(config, { repoUrl, commit, waitMs, sourceHostPath: backendCoreArtifactSourceHostPath(commit) });
|
||||
}
|
||||
if (action === "publish-user-service") {
|
||||
const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
|
||||
const service = requireSupportedUserService(config, serviceId);
|
||||
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? service.repository.url;
|
||||
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
|
||||
const waitMs = numberOption(args, "--wait-ms", 0);
|
||||
const dryRun = boolFlag(args, "--dry-run");
|
||||
const dockerfile = requireRepoRelativePath(service.repository.dockerfile, `microservices.${serviceId}.repository.dockerfile`);
|
||||
if (serviceId !== "decision-center") {
|
||||
throw new Error("ci publish-user-service currently allows only decision-center until each user-service Dockerfile contract is reviewed");
|
||||
}
|
||||
return publishUserServiceArtifact(config, {
|
||||
repoUrl,
|
||||
commit,
|
||||
waitMs,
|
||||
serviceId,
|
||||
dockerfile,
|
||||
sourceHostPath: userServiceArtifactSourceHostPath(serviceId, commit),
|
||||
dryRun,
|
||||
});
|
||||
}
|
||||
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"));
|
||||
@@ -1071,7 +1407,7 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
|
||||
});
|
||||
}
|
||||
if (action === "logs") return logs(nameArg ?? "");
|
||||
throw new Error("ci command must be one of: install, status, run, publish-backend-core, run-dev-e2e, logs");
|
||||
throw new Error("ci command must be one of: install, status, run, publish-backend-core, publish-user-service, run-dev-e2e, logs");
|
||||
}
|
||||
|
||||
export function startCiInstallJob(): Record<string, unknown> {
|
||||
|
||||
Reference in New Issue
Block a user