ci: standardize artifact summary catalog
This commit is contained in:
@@ -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>"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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 guard,core manifest 必须包含 `backend-core-dev`/`frontend-dev` Deployment/Service,Code 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`,由它写入主 PostgreSQL;D601 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 可用。
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user