ci: standardize artifact summary catalog

This commit is contained in:
Codex
2026-05-19 15:34:10 +00:00
parent c90c939eac
commit 76b27c6aa9
7 changed files with 318 additions and 49 deletions
+80
View File
@@ -0,0 +1,80 @@
{
"schemaVersion": 1,
"kind": "ci-artifact-catalog",
"purpose": "CI artifact catalog only. This file describes build inputs and image artifact naming; it does not describe runtime topology and does not replace deploy.json.",
"summaryContract": {
"requiredOnSuccess": [
"serviceId",
"sourceCommit",
"sourceRepo",
"dockerfile",
"imageRef",
"tag",
"digest",
"digestRef"
],
"fieldSemantics": {
"serviceId": "Stable UniDesk service id for the artifact.",
"sourceCommit": "Full 40-character Git commit used as the source and tag.",
"sourceRepo": "Git repository URL used to materialize the source.",
"dockerfile": "Repo-relative Dockerfile path used by CI.",
"imageRef": "Commit-tagged image reference pushed by CI.",
"tag": "Commit-pinned image tag; mutable tags such as latest are not valid.",
"digest": "Registry manifest digest for the pushed image.",
"digestRef": "Immutable image reference in repository@digest form."
}
},
"defaults": {
"producer": "D601 Tekton CI",
"registry": "127.0.0.1:5000",
"tag": "{{sourceCommit}}",
"mutableTagsAllowed": false,
"runtimeFieldsForbidden": [
"providerId",
"namespace",
"ports",
"composeService",
"kubernetesService",
"healthPath",
"replicas",
"env",
"volumes"
]
},
"artifacts": [
{
"serviceId": "baidu-netdisk",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/microservices/baidu-netdisk/Dockerfile",
"imageRepository": "unidesk/baidu-netdisk",
"imageRef": "127.0.0.1:5000/unidesk/baidu-netdisk:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/baidu-netdisk@{{digest}}"
},
{
"serviceId": "decision-center",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/microservices/decision-center/Dockerfile",
"imageRepository": "unidesk/decision-center",
"imageRef": "127.0.0.1:5000/unidesk/decision-center:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/decision-center@{{digest}}",
"publishCommand": "bun scripts/cli.ts ci publish-user-service --service decision-center --commit <full-sha>"
},
{
"serviceId": "frontend",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/frontend/Dockerfile",
"imageRepository": "unidesk/frontend",
"imageRef": "127.0.0.1:5000/unidesk/frontend:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/frontend@{{digest}}"
},
{
"serviceId": "backend-core",
"sourceRepo": "https://github.com/pikasTech/unidesk",
"dockerfile": "src/components/backend-core/Dockerfile",
"imageRepository": "unidesk/backend-core",
"imageRef": "127.0.0.1:5000/unidesk/backend-core:{{sourceCommit}}",
"digestRef": "127.0.0.1:5000/unidesk/backend-core@{{digest}}",
"publishCommand": "bun scripts/cli.ts ci publish-backend-core --commit <full-sha>"
}
]
}
+3 -1
View File
@@ -6,6 +6,8 @@ backend-core 和 reviewed user services 的长期分工是:CI 在 D601 构建
Production CI/CD runtime pinning and release-line boundaries follow `docs/reference/release-governance.md` and [GitHub issue #6](https://github.com/pikasTech/unidesk/issues/6). The registry may cache commit-pinned artifacts, but it must not become a floating replacement for `deploy.json`, `release/v1`, or `master` source history.
The CI-side artifact catalog is root `CI.json`. That file describes only artifact producer inputs and naming; registry consumers still verify the real image labels, manifest digest and live runtime separately. The producer summary contract is owned by `docs/reference/ci.md` and includes `serviceId`, `sourceCommit`, `sourceRepo`, `dockerfile`, `imageRef`, `tag`, `digest` and `digestRef`.
## Architecture
registry 运行在 D601 host/WSL OS 上,由 systemd 管理 Docker Compose 项目:
@@ -111,7 +113,7 @@ docker compose -p unidesk-artifact-registry -f /home/ubuntu/.unidesk/artifact-re
1. D601 artifact registry 已安装并通过 `health`
2. D601 CI 从 pushed Git checkout 构建 `unidesk/<service-id>:<commit>`
3. CI 将镜像 push 到 `127.0.0.1:5000/unidesk/<service-id>:<commit>`,并记录 image ref 与 digest。
3. CI 将镜像 push 到 `127.0.0.1:5000/unidesk/<service-id>:<commit>`,并记录完整 artifact summary,包括 service id、source repo、source commit、Dockerfile、image ref、tag、digest 和 digest ref
4. CD 在运行目标上确认目标 commit/tag/digest 存在。
5. Compose runtime 通过 provider-gateway Host SSH 从 D601 registry 流式读取 commit-pinned 镜像 tar,不开放 registry 端口,也不使用第三方镜像托管。
6. Compose runtime retag 为 Compose 使用的镜像名,并执行 `docker compose up -d --no-build --no-deps --force-recreate <service>`
+20 -2
View File
@@ -8,6 +8,7 @@ UniDesk CI is hosted on the D601 native k3s cluster with Tekton Pipelines and Te
- Tekton Triggers: `v0.34.0`.
- UniDesk CI namespace: `unidesk-ci`.
- Manifests: `src/components/microservices/k3sctl-adapter/k3s/ci/`.
- Artifact catalog: root `CI.json`, which is CI artifact catalog only. It describes build inputs, image naming and summary fields; runtime topology, rollout target, ports, namespaces and desired service commits remain in `config.json`, service manifests and `deploy.json`.
- CLI entry: `bun scripts/cli.ts ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs`.
- Dev namespace e2e runner: `bun scripts/cli.ts ci run-dev-e2e`; authoritative runner path, manifest contract and safety boundary are in `docs/reference/dev-ci-runner.md`.
- Rust backend-core check/build boundary: CI may run `UNIDESK_D601_RUST_CHECK=1 bun scripts/cli.ts check --full --rust` on D601; the master server must not compile Rust for backend-core iteration. The authoritative dev environment rule is `docs/reference/dev-environment.md`.
@@ -32,6 +33,23 @@ Git clone and dependency downloads inside the repo check task use `d601-provider
Private repository source authentication is part of the CI contract and follows `docs/reference/devops-hygiene.md`. If the repo-check task fails at `git clone` because credentials are unavailable, treat it as a CI infrastructure/auth gap, not as an application test result.
## Artifact Catalog And Summary Contract
`CI.json` is the reusable CI artifact catalog. It must remain artifact-only: `serviceId`, `sourceRepo`, `dockerfile`, registry repository naming, tag policy and summary-field semantics are allowed; provider ids, runtime namespaces, ports, compose services, Kubernetes Services, health paths, env, volumes and desired deploy commits are not allowed. `deploy.json` remains the version intent for deployments and must not be replaced by `CI.json`.
Every successful image-producing CI task must expose a common `artifactSummary` contract:
- `serviceId`: stable UniDesk service id.
- `sourceCommit`: full 40-character Git commit used as source and tag.
- `sourceRepo`: Git repository URL used to materialize the source.
- `dockerfile`: repo-relative Dockerfile path.
- `imageRef`: commit-tagged image reference pushed by CI.
- `tag`: commit-pinned image tag; mutable `latest` is invalid.
- `digest`: registry manifest digest for the pushed image.
- `digestRef`: immutable `repository@digest` image reference.
Tekton artifact tasks write these values as TaskRun results and also print the legacy `*_artifact_*` log lines for operator diagnostics. The CLI must read TaskRun results first, fall back to pod logs only for older runs, derive `imageRef`/`digestRef` from repository, tag and digest where possible, and report exact missing fields such as `digest` or `digestRef`. It must not turn a succeeded PipelineRun into a generic `incomplete` failure.
## CI/CD Runtime Governance
CI/CD server and control-plane runtime is production-like infrastructure. Its service version must be pinned by `deploy.json` and verified through runtime commit metadata; it must not float with the latest `master` just because the operator's CLI is newer.
@@ -72,7 +90,7 @@ The CI artifact task must follow these rules:
- The image is tagged with the source commit, for example `unidesk/backend-core:<commit>`, and pushed to the D601 artifact registry as `127.0.0.1:5000/unidesk/backend-core:<commit>`.
- The image must carry at least `unidesk.ai/service-id=backend-core`, `unidesk.ai/source-repo`, `unidesk.ai/source-commit` and `unidesk.ai/dockerfile=src/components/backend-core/Dockerfile`.
- Publication must fail if the D601 artifact registry is not healthy. It must not fall back to a third-party registry or a mutable `latest` tag.
- CI may output the image ref and digest as deployment input, but it must not restart production Compose services, call production `deploy apply`, mutate production namespaces, or change `deploy.json`.
- CI output must include the common `artifactSummary` fields defined above. `artifactSummary.imageRef` and `artifactSummary.digestRef` are deployment inputs for later CD, but CI must not restart production Compose services, call production `deploy apply`, mutate production namespaces, or change `deploy.json`.
The artifact registry contract and CD consumption path are defined in `docs/reference/artifact-registry.md`. CI is the producer of the backend-core image artifact; CD is only the consumer.
@@ -86,7 +104,7 @@ The CI user-service artifact task must follow these rules:
- D601 prepares a commit-pinned source export under `/home/ubuntu/.unidesk/ci/user-service-artifacts/<service-id>/<commit>` using the existing GitHub SSH deploy identity and node-local provider-gateway WS egress proxy. Tekton consumes that export through a read-only hostPath.
- The image is tagged only with the source commit and pushed to the D601 registry as `127.0.0.1:5000/unidesk/<service-id>:<commit>`. The producer must reject third-party registries and must not publish or consume a mutable `latest` tag.
- The image must carry `unidesk.ai/service-id`, `unidesk.ai/source-repo`, `unidesk.ai/source-commit` and `unidesk.ai/dockerfile` labels.
- The command output must include the image ref, tag, digest, source commit and service id. The digest ref is suitable as immutable input for later dev/prod deployment work.
- The command output must include the common `artifactSummary` fields: `serviceId`, `sourceCommit`, `sourceRepo`, `dockerfile`, `imageRef`, `tag`, `digest` and `digestRef`. The digest ref is suitable as immutable input for later dev/prod deployment work.
- CI is an artifact producer only. It must not restart production services, call production `deploy apply`, mutate the production namespace, or change `deploy.json`.
Publish a Baidu Netdisk artifact:
+1 -1
View File
@@ -29,7 +29,7 @@ CLI 可以从 `master` 快速演进,但必须兼容 `deploy.json` 固定的 CI
- `dev-env validate [--manifest path] [--kubectl-dry-run]` 离线校验 D601 `unidesk-dev` namespace、dev PostgreSQL 底座和 dev workload manifest。默认检查 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-foundation.k8s.yaml`;也可显式校验 `src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-core.k8s.yaml``src/components/microservices/k3sctl-adapter/k3s/dev/unidesk-dev-code-queue.k8s.yaml`。所有 namespaced 对象必须只落到 `unidesk-dev`foundation manifest 必须包含 `postgres-dev` StatefulSet/Service、dev secret/config、迁移 Job 和 DB URL guardcore manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/ServiceCode Queue dev manifest 必须包含 `code-queue-scheduler-dev``code-queue-read-dev``code-queue-write-dev` 和 dev provider egress proxy。加 `--kubectl-dry-run` 时额外执行 `kubectl apply --dry-run=client --validate=false -f <manifest>`,仍不 apply 资源。
- `dev-env prewarm-images [--image image] [--provider-id D601] [--no-pull] [--proxy-url URL] [--pull-timeout-ms N] [--dry-run]` 创建异步 job,通过 UniDesk SSH 维护桥在 D601 上把开发底座依赖镜像从 Docker 缓存导入原生 k3s containerd。默认镜像是 `postgres:16-alpine``rancher/mirrored-library-busybox:1.36.1`,用于避免 `postgres-dev` 与 local-path helper pod 卡在外部 registry 拉取。该命令固定验证 `/etc/rancher/k3s/k3s.yaml` 指向的 native k3s 上下文,并输出 `dev_env_containerd_image_ready=...` 作为成功判据;它不 apply manifest、不修改生产 `unidesk` namespace。
- `artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service` 管理 D601 host-managed CNCF Distribution registry 的声明、安装、只读检查和 pull-only artifact CD。该 registry 固定为 D601 loopback `127.0.0.1:5000`,由 systemd + Docker Compose 管理,位于 native k3s 故障域外;`deploy-backend-core``deploy-service` 只拉取 CI 已发布的 commit-pinned 镜像、retag/recreate 或导入 native k3s,并做 live commit 验证,不构建 runtime source。长期规则见 `docs/reference/artifact-registry.md`
- `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core``publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/<service>:<commit>`,但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`
- `ci install|status|run|publish-backend-core|publish-user-service|run-dev-e2e|logs` 管理 D601 原生 k3s 上的 Tekton CI。`run` 手动创建每 commit 检查和 Code Queue 只读性能门禁;`publish-backend-core``publish-user-service` 从 pushed Git commit 构建并发布 `127.0.0.1:5000/unidesk/<service>:<commit>` commit-pinned artifacts,输出 `artifactSummary`(含 `serviceId``sourceCommit``sourceRepo``dockerfile``imageRef``tag``digest``digestRef`,但不部署生产;`run-dev-e2e` 的 Git 控制 runner、短 launcher、host fetch 边界、临时 smoke namespace 和 no-CD 规则只在 `docs/reference/dev-ci-runner.md` 定义;Tekton CI 通用规则见 `docs/reference/ci.md`
- `codex deploy <commitId>` 是旧 Code Queue 兼容部署入口,已禁用以防止维护通道直连 D601 部署 Code Queue;当前 dev 自动化只做 `ci run-dev-e2e` smoke,不提供 Code Queue CD,详细规则见 `docs/reference/codex-deploy.md`
- `codex submit [prompt] [--prompt-file path|--prompt-stdin] [--queue queueId] [--provider-id id] [--cwd path] [--model model] [--reasoning-effort effort] [--execution-mode mode] [--max-attempts N] [--reference-task-id id] [--dry-run]` 通过 backend-core 私有代理向稳定 `code-queue` 用户服务路径提交任务;prompt 必须且只能来自位置参数、文件或 stdin 之一,`--dry-run` 只返回结构化请求且不实际入队。提交确认和 dry-run 必须返回完整 prompt、字符数和 `truncated=false`,不能套用任务详情的预览截断策略,否则长任务 prompt 无法被人工验收。backend-core 默认把提交、队列 CRUD、已读状态、历史摘要和轻量 Trace 读取分流到主 server `code-queue-mgr`,由它写入主 PostgreSQLD601 scheduler 只轮询并执行已入库任务。
- `codex task <taskId>` 通过 Code Queue 私有代理按任务 ID 查询结构化执行摘要;默认只返回有界 prompt/response 预览、执行 Provider、工作目录、最后 assistant message、最近工具调用摘要、attempt、judge、错误、耗时和 trace 翻页提示,适合在新队列任务中引用历史 session 且避免噪声爆炸。该摘要读取默认由主 server `code-queue-mgr` 从 PostgreSQL 返回,不依赖 D601 `code-queue-read` Service 可用。
+2 -1
View File
@@ -32,7 +32,8 @@ The default release flow for a user-service change is:
- No user-service artifact may rely on a third-party registry as source of truth.
- No production deploy may rebuild the source from a dirty worktree.
- Commit-pinned image tags are the deployment truth; mutable `latest` tags are not.
- The standard CI artifact producer is `bun scripts/cli.ts ci publish-user-service --service <id> --commit <full-sha>`. It accepts only a pushed Git commit and a registered service id, and reports image ref, tag, digest, source commit and service id.
- Root `CI.json` is an artifact catalog only. It can list user-service CI build inputs such as `serviceId`, `sourceRepo`, `dockerfile`, image repository naming and the required artifact summary fields; it must not carry runtime topology or replace `deploy.json`.
- The standard CI artifact producer is `bun scripts/cli.ts ci publish-user-service --service <id> --commit <full-sha>`. It accepts only a pushed Git commit and a registered service id, and reports `serviceId`, `sourceCommit`, `sourceRepo`, `dockerfile`, `imageRef`, `tag`, `digest` and `digestRef`.
- The CI artifact producer is not a deploy executor. It must not mutate the production namespace, restart production services, or update `deploy.json`.
- Every production release must finish with a manual acceptance step after the automated checks pass.
+146 -42
View File
@@ -41,6 +41,7 @@ interface CiPublishBackendCoreOptions {
commit: string;
waitMs: number;
sourceHostPath: string;
dryRun: boolean;
}
interface CiPublishUserServiceArtifactOptions {
@@ -116,6 +117,13 @@ interface ArtifactSummary {
digestRef: string | null;
}
interface ArtifactSummaryContext {
serviceId: string;
commit: string;
repoUrl: string;
dockerfile: string;
}
function stringOption(args: string[], name: string): string | null {
const index = args.indexOf(name);
if (index === -1) return null;
@@ -863,34 +871,112 @@ 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}`);
function artifactSummaryDefaults(context: ArtifactSummaryContext): ArtifactSummary {
const registry = "127.0.0.1:5000";
const repository = `${registry}/unidesk/${context.serviceId}`;
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",
serviceId: context.serviceId,
sourceCommit: context.commit,
sourceRepo: context.repoUrl,
dockerfile: context.dockerfile,
registry,
repository,
tag: fields.get("user_service_artifact_tag") ?? commit,
tag: context.commit,
imageRef: `${repository}:${context.commit}`,
digest: null,
digestRef: null,
};
}
function artifactSummaryField(fields: Map<string, string>, suffix: string): string | null {
const value = fields.get(`user_service_artifact_${suffix}`) ?? fields.get(`backend_core_artifact_${suffix}`) ?? null;
return value === null || value.length === 0 ? null : value;
}
function parseArtifactSummaryFromFields(fields: Map<string, string>, context: ArtifactSummaryContext): ArtifactSummary {
const planned = artifactSummaryDefaults(context);
const registry = artifactSummaryField(fields, "registry") ?? planned.registry;
const repository = artifactSummaryField(fields, "repository") ?? `${registry}/unidesk/${context.serviceId}`;
const tag = artifactSummaryField(fields, "tag") ?? planned.tag;
const imageRef = artifactSummaryField(fields, "image") ?? (repository.length > 0 && tag.length > 0 ? `${repository}:${tag}` : planned.imageRef);
const digest = artifactSummaryField(fields, "digest");
const digestRef = artifactSummaryField(fields, "digest_ref") ?? (digest === null || digest.length === 0 || repository.length === 0 ? null : `${repository}@${digest}`);
return {
serviceId: artifactSummaryField(fields, "service_id") ?? planned.serviceId,
sourceCommit: artifactSummaryField(fields, "source_commit") ?? planned.sourceCommit,
sourceRepo: artifactSummaryField(fields, "source_repo") ?? planned.sourceRepo,
dockerfile: artifactSummaryField(fields, "dockerfile") ?? planned.dockerfile,
registry,
repository,
tag,
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`);
function parseArtifactSummaryFromOutput(output: string, context: ArtifactSummaryContext): 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]);
}
return parseArtifactSummaryFromFields(fields, context);
}
function missingArtifactSummaryFields(artifact: ArtifactSummary): string[] {
const missing: string[] = [];
if (artifact.serviceId.length === 0) missing.push("serviceId");
if (!/^[0-9a-f]{40}$/u.test(artifact.sourceCommit)) missing.push("sourceCommit");
if (artifact.sourceRepo.length === 0) missing.push("sourceRepo");
if (artifact.dockerfile.length === 0) missing.push("dockerfile");
if (artifact.imageRef.length === 0) missing.push("imageRef");
if (artifact.tag.length === 0) missing.push("tag");
if (artifact.digest === null || artifact.digest.length === 0) missing.push("digest");
if (artifact.digestRef === null || artifact.digestRef.length === 0) missing.push("digestRef");
return missing;
}
function assertArtifactSummaryComplete(artifact: ArtifactSummary, pipelineRun: string): void {
const missing = missingArtifactSummaryFields(artifact);
if (missing.length > 0) {
throw new Error(`artifact summary for ${pipelineRun} is missing required field(s): ${missing.join(", ")}`);
}
}
async function readArtifactSummaryFromPipelineRun(name: string, context: ArtifactSummaryContext): Promise<ArtifactSummary> {
const result = await runRemoteKubectlRaw([
"set -euo pipefail",
`kubectl get taskrun -n unidesk-ci -l tekton.dev/pipelineRun=${shellQuote(name)} -o json`,
].join("\n"), 60_000, 45_000);
if (result.ok && result.stdout.trim().length > 0) {
try {
const parsed = JSON.parse(result.stdout) as unknown;
const fields = new Map<string, string>();
const list = asRecord(parsed);
const items = Array.isArray(list?.items) ? list.items : [];
for (const item of items) {
const taskRun = asRecord(item);
const status = asRecord(taskRun?.status);
const results = Array.isArray(status?.results) ? status.results : [];
for (const rawResult of results) {
const taskResult = asRecord(rawResult);
const nameValue = asString(taskResult?.name);
const value = asString(taskResult?.value);
if (/^(user_service_artifact_[a-z_]+|backend_core_artifact_[a-z_]+)$/u.test(nameValue) && value.length > 0) {
fields.set(nameValue, value);
}
}
}
if (fields.size > 0) {
const fromResults = parseArtifactSummaryFromFields(fields, context);
if (missingArtifactSummaryFields(fromResults).length === 0) return fromResults;
}
} catch {
// Fall back to pod logs below; a malformed diagnostic line must not mask a succeeded PipelineRun.
}
}
return parseArtifactSummaryFromOutput(await readPipelineRunLogText(name), context);
}
async function readPipelineRunLogText(name: string): Promise<string> {
@@ -931,25 +1017,47 @@ async function run(options: CiOptions): Promise<Record<string, unknown>> {
}
async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPublishBackendCoreOptions): Promise<Record<string, unknown>> {
const plannedArtifact: ArtifactSummary = {
const summaryContext: ArtifactSummaryContext = {
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,
commit: options.commit,
repoUrl: options.repoUrl,
};
const plannedArtifact = artifactSummaryDefaults(summaryContext);
if (options.dryRun) {
return {
ok: true,
mode: "dry-run",
pipeline: "unidesk-backend-core-artifact-publish",
namespace: "unidesk-ci",
repoUrl: options.repoUrl,
commit: options.commit,
sourceHostPath: options.sourceHostPath,
source: {
ok: true,
mode: "planned-only",
providerId: d601ProviderId,
repoUrl: options.repoUrl,
repoFetchUrl: repoSshUrl(options.repoUrl),
commit: options.commit,
dockerfile: "src/components/backend-core/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-backend-core --commit ${options.commit} --wait-ms 1200000`,
],
};
}
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")
? await readArtifactSummaryFromPipelineRun(name, summaryContext)
: plannedArtifact;
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
return {
@@ -979,18 +1087,13 @@ async function publishBackendCoreArtifact(config: UniDeskConfig, options: CiPubl
}
async function publishUserServiceArtifact(config: UniDeskConfig, options: CiPublishUserServiceArtifactOptions): Promise<Record<string, unknown>> {
const plannedArtifact: ArtifactSummary = {
const summaryContext: ArtifactSummaryContext = {
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;
commit: options.commit,
repoUrl: options.repoUrl,
};
const plannedArtifact = artifactSummaryDefaults(summaryContext);
if (options.dryRun) {
return {
ok: true,
@@ -1026,7 +1129,7 @@ async function publishUserServiceArtifact(config: UniDeskConfig, options: CiPubl
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)
? await readArtifactSummaryFromPipelineRun(name, summaryContext)
: plannedArtifact;
if (waitSucceeded && wait !== null) assertArtifactSummaryComplete(artifact, name);
return {
@@ -1334,7 +1437,7 @@ export function ciHelp(): Record<string, unknown> {
command: "bun scripts/cli.ts ci publish-user-service --service <service-id> --commit <full-sha>",
initiallySupportedServices: ["baidu-netdisk", "decision-center"],
registry: "127.0.0.1:5000/unidesk/<service-id>:<commit>",
outputFields: ["imageRef", "tag", "digest", "sourceCommit", "serviceId"],
outputFields: ["serviceId", "sourceCommit", "sourceRepo", "dockerfile", "imageRef", "tag", "digest", "digestRef"],
boundary: "artifact producer only; no prod deploy and no production namespace mutation",
},
runDevE2E: {
@@ -1367,7 +1470,8 @@ export async function runCiCommand(config: UniDeskConfig, args: string[]): Promi
const repoUrl = stringOption(args, "--repo") ?? stringOption(args, "--repo-url") ?? "https://github.com/pikasTech/unidesk";
const commit = requireFullCommit(stringOption(args, "--commit") ?? stringOption(args, "--revision"));
const waitMs = numberOption(args, "--wait-ms", 0);
return publishBackendCoreArtifact(config, { repoUrl, commit, waitMs, sourceHostPath: backendCoreArtifactSourceHostPath(commit) });
const dryRun = boolFlag(args, "--dry-run");
return publishBackendCoreArtifact(config, { repoUrl, commit, waitMs, sourceHostPath: backendCoreArtifactSourceHostPath(commit), dryRun });
}
if (action === "publish-user-service") {
const serviceId = requireServiceId(stringOption(args, "--service") ?? stringOption(args, "--service-id"));
@@ -328,6 +328,27 @@ spec:
default: 127.0.0.1:5000
- name: source-host-path
type: string
results:
- name: backend_core_artifact_service_id
description: CI artifact summary service id.
- name: backend_core_artifact_source_commit
description: Full Git source commit used to build the artifact.
- name: backend_core_artifact_source_repo
description: Source repository URL used to build the artifact.
- name: backend_core_artifact_dockerfile
description: Repo-relative Dockerfile path used to build the artifact.
- name: backend_core_artifact_registry
description: D601 loopback registry host.
- name: backend_core_artifact_repository
description: Registry repository without tag or digest.
- name: backend_core_artifact_image
description: Commit-tagged image reference.
- name: backend_core_artifact_tag
description: Commit-pinned image tag.
- name: backend_core_artifact_digest
description: Registry manifest digest.
- name: backend_core_artifact_digest_ref
description: Immutable image digest reference.
workspaces:
- name: source
volumes:
@@ -428,7 +449,18 @@ spec:
repo_digests="$(docker image inspect "$registry_image" --format '{{json .RepoDigests}}')"
digest="$(docker image inspect "$registry_image" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -F@ -v repo="$repository" '$1 == repo { print $2; exit }')"
test -n "$digest"
printf 'backend_core_artifact_service_id=backend-core\nbackend_core_artifact_image=%s\nbackend_core_artifact_repository=%s\nbackend_core_artifact_tag=%s\nbackend_core_artifact_digest=%s\nbackend_core_artifact_digest_ref=%s@%s\nbackend_core_artifact_source_commit=%s\nbackend_core_artifact_source_repo=%s\nbackend_core_artifact_dockerfile=src/components/backend-core/Dockerfile\nbackend_core_artifact_repo_digests=%s\n' "$registry_image" "$repository" "$commit" "$digest" "$repository" "$digest" "$commit" "$(params.repo-url)" "$repo_digests"
digest_ref="$repository@$digest"
printf '%s' "backend-core" > "$(results.backend_core_artifact_service_id.path)"
printf '%s' "$commit" > "$(results.backend_core_artifact_source_commit.path)"
printf '%s' "$(params.repo-url)" > "$(results.backend_core_artifact_source_repo.path)"
printf '%s' "src/components/backend-core/Dockerfile" > "$(results.backend_core_artifact_dockerfile.path)"
printf '%s' "$registry" > "$(results.backend_core_artifact_registry.path)"
printf '%s' "$repository" > "$(results.backend_core_artifact_repository.path)"
printf '%s' "$registry_image" > "$(results.backend_core_artifact_image.path)"
printf '%s' "$commit" > "$(results.backend_core_artifact_tag.path)"
printf '%s' "$digest" > "$(results.backend_core_artifact_digest.path)"
printf '%s' "$digest_ref" > "$(results.backend_core_artifact_digest_ref.path)"
printf 'backend_core_artifact_service_id=backend-core\nbackend_core_artifact_image=%s\nbackend_core_artifact_repository=%s\nbackend_core_artifact_tag=%s\nbackend_core_artifact_digest=%s\nbackend_core_artifact_digest_ref=%s\nbackend_core_artifact_source_commit=%s\nbackend_core_artifact_source_repo=%s\nbackend_core_artifact_dockerfile=src/components/backend-core/Dockerfile\nbackend_core_artifact_registry=%s\nbackend_core_artifact_repo_digests=%s\n' "$registry_image" "$repository" "$commit" "$digest" "$digest_ref" "$commit" "$(params.repo-url)" "$registry" "$repo_digests"
---
apiVersion: tekton.dev/v1
kind: Pipeline
@@ -501,6 +533,27 @@ spec:
default: 127.0.0.1:5000
- name: source-host-path
type: string
results:
- name: user_service_artifact_service_id
description: CI artifact summary service id.
- name: user_service_artifact_source_commit
description: Full Git source commit used to build the artifact.
- name: user_service_artifact_source_repo
description: Source repository URL used to build the artifact.
- name: user_service_artifact_dockerfile
description: Repo-relative Dockerfile path used to build the artifact.
- name: user_service_artifact_registry
description: D601 loopback registry host.
- name: user_service_artifact_repository
description: Registry repository without tag or digest.
- name: user_service_artifact_image
description: Commit-tagged image reference.
- name: user_service_artifact_tag
description: Commit-pinned image tag.
- name: user_service_artifact_digest
description: Registry manifest digest.
- name: user_service_artifact_digest_ref
description: Immutable image digest reference.
workspaces:
- name: source
volumes:
@@ -625,7 +678,18 @@ spec:
repo_digests="$(docker image inspect "$registry_image" --format '{{json .RepoDigests}}')"
digest="$(docker image inspect "$registry_image" --format '{{range .RepoDigests}}{{println .}}{{end}}' | awk -F@ -v repo="$repository" '$1 == repo { print $2; exit }')"
test -n "$digest"
printf 'user_service_artifact_service_id=%s\nuser_service_artifact_image=%s\nuser_service_artifact_repository=%s\nuser_service_artifact_tag=%s\nuser_service_artifact_digest=%s\nuser_service_artifact_digest_ref=%s@%s\nuser_service_artifact_source_commit=%s\nuser_service_artifact_source_repo=%s\nuser_service_artifact_dockerfile=%s\nuser_service_artifact_registry=%s\nuser_service_artifact_repo_digests=%s\n' "$service_id" "$registry_image" "$repository" "$commit" "$digest" "$repository" "$digest" "$commit" "$(params.repo-url)" "$dockerfile" "$registry" "$repo_digests"
digest_ref="$repository@$digest"
printf '%s' "$service_id" > "$(results.user_service_artifact_service_id.path)"
printf '%s' "$commit" > "$(results.user_service_artifact_source_commit.path)"
printf '%s' "$(params.repo-url)" > "$(results.user_service_artifact_source_repo.path)"
printf '%s' "$dockerfile" > "$(results.user_service_artifact_dockerfile.path)"
printf '%s' "$registry" > "$(results.user_service_artifact_registry.path)"
printf '%s' "$repository" > "$(results.user_service_artifact_repository.path)"
printf '%s' "$registry_image" > "$(results.user_service_artifact_image.path)"
printf '%s' "$commit" > "$(results.user_service_artifact_tag.path)"
printf '%s' "$digest" > "$(results.user_service_artifact_digest.path)"
printf '%s' "$digest_ref" > "$(results.user_service_artifact_digest_ref.path)"
printf 'user_service_artifact_service_id=%s\nuser_service_artifact_image=%s\nuser_service_artifact_repository=%s\nuser_service_artifact_tag=%s\nuser_service_artifact_digest=%s\nuser_service_artifact_digest_ref=%s\nuser_service_artifact_source_commit=%s\nuser_service_artifact_source_repo=%s\nuser_service_artifact_dockerfile=%s\nuser_service_artifact_registry=%s\nuser_service_artifact_repo_digests=%s\n' "$service_id" "$registry_image" "$repository" "$commit" "$digest" "$digest_ref" "$commit" "$(params.repo-url)" "$dockerfile" "$registry" "$repo_digests"
---
apiVersion: tekton.dev/v1
kind: Pipeline