Merge pull request #499 from pikasTech/fix/deploy-json-human-truth-20260527
fix: keep deploy manifest human-authored
This commit is contained in:
+12
-17
@@ -67,25 +67,20 @@ node scripts/deploy-desired-state-plan.mjs --target-tag <tag> --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --promotion-commit <sha> --check
|
||||
```
|
||||
|
||||
The desired-state plan reads only `deploy/deploy.json`,
|
||||
The desired-state plan reads `deploy/deploy.json`,
|
||||
`deploy/artifact-catalog.dev.json`, `deploy/k8s/base/workloads.yaml`, and the
|
||||
optional artifact report snapshot. It checks that commit IDs, service image
|
||||
tags, workload images, and present env mirrors such as `HWLAB_COMMIT_ID`,
|
||||
`HWLAB_IMAGE`, `HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` converge. It
|
||||
also blocks if `hwlab-cloud-api` loses the Code Agent provider Secret ref
|
||||
`hwlab-code-agent-provider/openai-api-key`, the `OPENAI_API_KEY` env name, or
|
||||
the DEV egress proxy base-url env. Without `--check`, `--target-ref` or
|
||||
`--target-tag` reports a uniform older desired-state as a read-only promotion
|
||||
plan. With `--check`, any explicit target is a hard current-main/commit-pinning
|
||||
gate: stale deploy, catalog, workload image, or runtime mirror fields are
|
||||
blocked before apply.
|
||||
optional artifact report snapshot. `deploy/deploy.json` is the human-authored
|
||||
runtime config truth source. Generated image identity lives in
|
||||
`deploy/artifact-catalog.dev.json`; GitOps render overlays catalog image fields
|
||||
and generated env mirrors such as `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`,
|
||||
`HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID` into runtime manifests.
|
||||
|
||||
`deploy/deploy.json` is the deploy truth, but artifact catalog and workload
|
||||
refs are part of the same desired-state contract. Promotion updates must move
|
||||
all three together by using the refresh command in blocked mode or
|
||||
publish-report mode, then run `--target-ref origin/main --check` or the
|
||||
promotion commit check. Report files are contextual evidence only and must not
|
||||
override these desired-state files.
|
||||
The plan blocks if generated artifact identity leaks back into `deploy.json`,
|
||||
or if `hwlab-cloud-api` loses the Code Agent provider Secret ref
|
||||
`hwlab-code-agent-provider/openai-api-key`, the `OPENAI_API_KEY` env name, or
|
||||
the DEV egress proxy base-url env. Promotion updates must refresh only
|
||||
`deploy/artifact-catalog.dev.json` from a publish report before render/check;
|
||||
report files are contextual evidence only and must not override source truth.
|
||||
|
||||
This is source/dry-run support only. It does not prove a registry image exists,
|
||||
does not build, pull, push, apply, restart, or touch PROD, and must not be used
|
||||
|
||||
+2
-24
@@ -1,7 +1,6 @@
|
||||
{
|
||||
"manifestVersion": "v1",
|
||||
"environment": "dev",
|
||||
"commitId": "af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"endpoint": "http://74.48.78.17:16667",
|
||||
"health": {
|
||||
@@ -181,7 +180,6 @@
|
||||
"services": [
|
||||
{
|
||||
"serviceId": "hwlab-cloud-api",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-api:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -190,9 +188,6 @@
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_PUBLIC_ENDPOINT": "http://74.48.78.17:16667",
|
||||
"HWLAB_CLOUD_API_PORT": "6667",
|
||||
"HWLAB_COMMIT_ID": "af46386",
|
||||
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-api:af46386",
|
||||
"HWLAB_IMAGE_TAG": "af46386",
|
||||
"HWLAB_RUNTIME_SUBSTITUTE_FORBIDDEN": "unidesk-backend,provider-gateway,microservice-proxy",
|
||||
"HWLAB_CLOUD_DB_URL": "secretRef:hwlab-cloud-api-dev-db/database-url",
|
||||
"HWLAB_CLOUD_DB_SSL_MODE": "disable",
|
||||
@@ -230,7 +225,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cloud-web",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cloud-web:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -238,15 +232,11 @@
|
||||
"env": {
|
||||
"HWLAB_ENVIRONMENT": "dev",
|
||||
"HWLAB_API_BASE_URL": "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||||
"HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000",
|
||||
"HWLAB_COMMIT_ID": "af46386",
|
||||
"HWLAB_IMAGE": "127.0.0.1:5000/hwlab/hwlab-cloud-web:af46386",
|
||||
"HWLAB_IMAGE_TAG": "af46386"
|
||||
"HWLAB_CLOUD_WEB_PROXY_TIMEOUT_MS": "1260000"
|
||||
}
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-mgr",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-mgr:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -258,7 +248,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-worker",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-worker:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -270,7 +259,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -282,7 +270,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-gateway-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-gateway-simu:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -302,7 +289,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-box-simu",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-box-simu:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -321,7 +307,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-patch-panel",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-patch-panel:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -342,7 +327,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-router",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-router:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -354,7 +338,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-tunnel-client",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-tunnel-client:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -368,7 +351,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-edge-proxy",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-edge-proxy:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -381,7 +363,6 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-cli",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-cli:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
@@ -392,14 +373,11 @@
|
||||
},
|
||||
{
|
||||
"serviceId": "hwlab-agent-skills",
|
||||
"image": "127.0.0.1:5000/hwlab/hwlab-agent-skills:af46386",
|
||||
"namespace": "hwlab-dev",
|
||||
"healthPath": "/health/live",
|
||||
"profile": "dev",
|
||||
"replicas": 1,
|
||||
"env": {
|
||||
"HWLAB_SKILLS_COMMIT_ID": "af46386"
|
||||
}
|
||||
"env": {}
|
||||
}
|
||||
],
|
||||
"createdAt": "2026-05-21T00:00:00Z"
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"$id": "https://hwlab.pikastech.local/deploy/deploy.schema.json",
|
||||
"title": "HWLAB Deploy Manifest",
|
||||
"type": "object",
|
||||
"required": ["manifestVersion", "environment", "commitId", "health", "publicEndpoints", "frp", "k3s", "profiles", "services"],
|
||||
"required": ["manifestVersion", "environment", "health", "publicEndpoints", "frp", "k3s", "profiles", "services"],
|
||||
"properties": {
|
||||
"manifestVersion": {
|
||||
"type": "string",
|
||||
@@ -13,10 +13,6 @@
|
||||
"type": "string",
|
||||
"enum": ["dev"]
|
||||
},
|
||||
"commitId": {
|
||||
"type": "string",
|
||||
"pattern": "^[a-f0-9]{7,40}$"
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"minLength": 1,
|
||||
@@ -317,15 +313,11 @@
|
||||
},
|
||||
"service": {
|
||||
"type": "object",
|
||||
"required": ["serviceId", "image", "namespace", "healthPath", "profile"],
|
||||
"required": ["serviceId", "namespace", "healthPath", "profile"],
|
||||
"properties": {
|
||||
"serviceId": {
|
||||
"$ref": "#/$defs/serviceId"
|
||||
},
|
||||
"image": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
},
|
||||
"namespace": {
|
||||
"type": "string",
|
||||
"minLength": 1
|
||||
|
||||
+25
-26
@@ -1,8 +1,9 @@
|
||||
# HWLAB Artifact Catalog Contract
|
||||
|
||||
The artifact catalog is the DEV-only MVP contract for interpreting deployment
|
||||
artifacts. It pins which HWLAB service image, commit, profile, namespace, and
|
||||
health path a static deploy manifest is claiming for acceptance review.
|
||||
The artifact catalog is the DEV-only MVP contract for interpreting generated
|
||||
deployment artifacts. It pins which HWLAB service image, commit, profile,
|
||||
namespace, and health path GitOps render must inject into runtime manifests.
|
||||
`deploy/deploy.json` stays the human-authored runtime config truth source.
|
||||
|
||||
This file is not automatically a CI publish record. It proves only the state it
|
||||
declares:
|
||||
@@ -34,10 +35,10 @@ metadata, then be copied into the catalog by the refresh command below.
|
||||
|
||||
| File | Role |
|
||||
| --- | --- |
|
||||
| `deploy/artifact-catalog.dev.json` | Machine-readable DEV artifact catalog sample and static source of truth for the skeleton. |
|
||||
| `deploy/deploy.json` | DEV deploy manifest skeleton that the catalog explains. |
|
||||
| `deploy/artifact-catalog.dev.json` | Machine-readable DEV artifact identity truth for generated image, digest, commit, and publish provenance fields. |
|
||||
| `deploy/deploy.json` | Human-authored DEV runtime config such as service topology, env, health, profile, replicas, and endpoints. |
|
||||
| `scripts/g14-gitops-render.mjs` | Declares the Tekton primitive CI tasks and minimal guardrails that generate the G14 CI/CD control plane. |
|
||||
| `scripts/deploy-desired-state-plan.mjs` | Read-only planner/checker for commit, image tag, workload image, and env mirror convergence across deploy desired-state files. |
|
||||
| `scripts/deploy-desired-state-plan.mjs` | Read-only planner/checker for catalog artifact identity, generated identity leakage into deploy config, and deploy/runtime config contracts. |
|
||||
| `scripts/refresh-artifact-catalog.mjs` | Refreshes source/artifact commit tags and records either blocked `not_published` state or proven publish digests. |
|
||||
| `scripts/validate-artifact-catalog.mjs` | Local static validator for the catalog and deploy manifest skeleton. |
|
||||
| `scripts/preflight-dev-base-image.mjs` | Local DEV builder base-image preflight for future artifact publish. |
|
||||
@@ -51,8 +52,8 @@ rules:
|
||||
| Field | Rule |
|
||||
| --- | --- |
|
||||
| `serviceId` | Must be one of the frozen HWLAB service IDs and must appear exactly once. |
|
||||
| `commitId` | Must match the catalog and `deploy/deploy.json` commit ID. Short or full lowercase Git SHA is allowed. |
|
||||
| `image` | Must match the DEV registry target `<registry>/<namespace>/<serviceId>:<imageTag>` and the matching `deploy/deploy.json` service image. |
|
||||
| `commitId` | Must match the top-level catalog commit ID. Short or full lowercase Git SHA is allowed. |
|
||||
| `image` | Must match the DEV registry target `<registry>/<namespace>/<serviceId>:<imageTag>`. The matching field must not be copied into `deploy/deploy.json`. |
|
||||
| `imageTag` | Must be the first seven characters of `commitId`. Mutable tags such as `latest`, `dev`, or `main` are forbidden. |
|
||||
| `profile` | Must be `dev`. |
|
||||
| `namespace` | Must be `hwlab-dev`. |
|
||||
@@ -64,8 +65,8 @@ rules:
|
||||
| `digest` | Must be `not_published` in the skeleton catalog; it may be a registry `sha256:<64 hex>` digest only after publish evidence exists. |
|
||||
|
||||
The catalog `commitId` is the artifact source commit, not proof that a registry
|
||||
artifact exists. It must match `deploy/deploy.json`, every service `commitId`,
|
||||
and the image tag. If a later target ref changes artifact build inputs after the
|
||||
artifact exists. It must match every service `commitId` and the image tag. If a
|
||||
later target ref changes artifact build inputs after the
|
||||
publish source, the DEV gate preflight may reopen `artifact-source-commit` even
|
||||
when the base-image blocker is already closed.
|
||||
|
||||
@@ -81,28 +82,26 @@ node scripts/deploy-desired-state-plan.mjs --target-tag <tag> --pretty
|
||||
node scripts/deploy-desired-state-plan.mjs --promotion-commit <sha> --check
|
||||
```
|
||||
|
||||
The planner treats `deploy/deploy.json`,
|
||||
`deploy/artifact-catalog.dev.json`, and `deploy/k8s/base/workloads.yaml` as the
|
||||
authoritative source desired-state. It reports top-level commit IDs, catalog
|
||||
service `commitId`/`image`/`imageTag`, deploy service image tags, workload
|
||||
images, and present env mirrors including `HWLAB_COMMIT_ID`, `HWLAB_IMAGE`,
|
||||
`HWLAB_IMAGE_TAG`, and `HWLAB_SKILLS_COMMIT_ID`.
|
||||
The planner treats `deploy/deploy.json` as human-authored runtime config,
|
||||
`deploy/artifact-catalog.dev.json` as artifact identity truth, and
|
||||
`deploy/k8s/base/workloads.yaml` as a runtime template. It reports catalog
|
||||
service `commitId`/`image`/`imageTag`, blocks generated artifact identity that
|
||||
leaks into `deploy.json`, and leaves workload image/env mirror values to GitOps
|
||||
render because render overwrites them from the catalog.
|
||||
|
||||
`--check` exits non-zero when the source files are missing or invalid, the
|
||||
desired-state has internal drift, an explicit target tag is invalid or mutable,
|
||||
or any authoritative field does not match the requested target. A uniform older
|
||||
`--check` exits non-zero when the source files are missing or invalid, generated
|
||||
artifact identity leaks into `deploy.json`, an explicit target tag is invalid or
|
||||
mutable, or catalog artifact identity does not match the requested target. A uniform older
|
||||
state under `--target-ref` is only allowed in read-only plan mode; with
|
||||
`--target-ref origin/main --check` it is a blocker because applying it would keep
|
||||
DEV pinned to stale artifacts. The optional report snapshot is contextual
|
||||
evidence only and is not an authoritative desired-state source.
|
||||
|
||||
For PR review or CI/CD promotion handoff, use `--promotion-commit <sha>
|
||||
--check`. That mode is stricter than a `--target-ref` plan: every top-level
|
||||
commit, catalog service commit, deploy service image tag, workload image tag,
|
||||
and present mirror env (`HWLAB_COMMIT_ID`, `HWLAB_IMAGE`,
|
||||
`HWLAB_IMAGE_TAG`, `HWLAB_SKILLS_COMMIT_ID`, `HWLAB_IMAGE_DIGEST`) must align
|
||||
with the promotion commit/tag. A stale but internally consistent manifest is a
|
||||
blocker in promotion mode because applying it would roll DEV back.
|
||||
--check`. That mode is stricter than a `--target-ref` plan: the catalog
|
||||
top-level commit and each catalog service commit/image tag must align with the
|
||||
promotion commit/tag. A stale but internally consistent catalog is a blocker in
|
||||
promotion mode because applying it would roll DEV back.
|
||||
|
||||
The planner does not prove registry existence, publish success, DEV apply, or
|
||||
M3 DEV-LIVE. Only real DEV observation of
|
||||
@@ -112,7 +111,7 @@ trace, audit, and evidence records can be called M3 DEV-LIVE.
|
||||
## Refresh Workflow
|
||||
|
||||
When the source commit changes and no publish report proves digests, refresh the
|
||||
catalog and deploy manifests in blocked mode:
|
||||
catalog in blocked mode:
|
||||
|
||||
```sh
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked
|
||||
|
||||
@@ -10,9 +10,11 @@ PROD, read secrets, or use UniDesk runtime services as substitutes.
|
||||
## Current Repository Constraints
|
||||
|
||||
- No repository Dockerfile is present in the DEV skeleton.
|
||||
- `deploy/deploy.json` and `deploy/artifact-catalog.dev.json` pin service image
|
||||
names to the DEV registry target, by default
|
||||
`127.0.0.1:5000/hwlab/<hwlab-service-id>:<commit>`.
|
||||
- `deploy/artifact-catalog.dev.json` pins service image names to the DEV
|
||||
registry target, by default
|
||||
`127.0.0.1:5000/hwlab/<hwlab-service-id>:<commit>`; `deploy/deploy.json`
|
||||
remains human-authored runtime config and must not carry generated image
|
||||
identity.
|
||||
- The artifact catalog may be either `contract-skeleton` or `published`. A
|
||||
`published` catalog must be backed by `/tmp/hwlab-dev-gate/dev-artifacts.json`
|
||||
with real `sha256:<64 hex>` digests for every required service.
|
||||
|
||||
@@ -94,8 +94,8 @@ The preflight checks:
|
||||
- Catalog image manifests are visible without reading credentials. A failed
|
||||
manifest read is tracked as a #66 registry reachability dimension and must not
|
||||
be conflated with missing publish digests.
|
||||
- Deploy images do not point at UniDesk, provider-gateway, or microservice-proxy
|
||||
substitutes.
|
||||
- Catalog images do not point at UniDesk, provider-gateway, or microservice-proxy
|
||||
substitutes; `deploy/deploy.json` must not contain generated image identity.
|
||||
|
||||
## Registry Capability Dimensions
|
||||
|
||||
|
||||
@@ -66,9 +66,9 @@ HWLAB 是 monorepo,G14 CI/CD 加速必须按组件输入判断构建和滚动
|
||||
- 服务清单兼容顺序固定为:显式 `--services`、`deploy.services[]`、`deploy.k3s.serviceMappings[]`、`internal/protocol.SERVICE_IDS`。因此旧 `deploy/deploy.json` 形态和当前完整 `deploy.services[]` 形态都必须能被 planner 识别。
|
||||
- 组件边界固定由 `scripts/src/g14-ci-plan-lib.mjs` 的内建 service-path model 定义;如需新增或调整 `componentPaths`、`sharedPaths`、`runtimeDeps` 或 `buildSystemPaths`,直接修改 planner 库和对应测试,不再额外维护 repo-local commands/forbidden skeleton。
|
||||
- `scripts/g14-artifact-publish.mjs` 默认启用组件级 lazy build:先运行 planner,再只构建/推送 `affectedServices`,`reusedServices` 必须从 `deploy/artifact-catalog.dev.json` 复用已有 sha256 digest;如果 catalog 没有可验证 digest,必须结构化阻塞并修复 catalog 或显式扩大 changed service 范围,不能退回 Docker 或 legacy full-build 路线。
|
||||
- `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 把这些字段复制进 `deploy/artifact-catalog.dev.json` 和 `deploy/deploy.json` 的对应 service。旧字段 `commitId`、`image`、`imageTag`、`digest`、`publishState` 语义不变,旧检查脚本不得因为新增字段失败。
|
||||
- `scripts/g14-gitops-render.mjs` 只支持混合 desired state:workload 的 container image、`HWLAB_IMAGE`、`HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy.services[]` 的 per-service image/env;全局 GitOps metadata 仍记录本次 source commit。`--legacy-source-images` 与 `HWLAB_G14_USE_DEPLOY_IMAGES=0` 已废弃,不得把所有 workload image 回退渲染为同一个 source commit tag。
|
||||
- G14 Tekton promotion 在推送 `G14-gitops` 前,必须用 publish report 刷新 workspace 内的 `deploy/deploy.json`、`deploy/artifact-catalog.dev.json` 和 `deploy/k8s/base/workloads.yaml`,并把该 per-service artifact truth 以 `chore: promote G14 GitOps source <short>` 提交回 `G14` source branch;poller 必须跳过这类生成提交,避免自触发循环。这样下一轮 planner 才能从最新 catalog 复用上一轮已构建服务,避免把未变服务回滚到旧 digest。
|
||||
- `scripts/g14-artifact-publish.mjs` 的 publish report 必须携带 planner 的 per-service 元数据;`scripts/refresh-artifact-catalog.mjs` 只把生成的 `commitId`、`image`、`imageTag`、`digest`、`publishState` 和 component provenance 字段复制进 `deploy/artifact-catalog.dev.json`。`deploy/deploy.json` 是人写的 runtime config 真相源,不得被 promotion 回写镜像身份字段。
|
||||
- `scripts/g14-gitops-render.mjs` 只支持混合 desired state:workload 的 container image、`HWLAB_IMAGE`、`HWLAB_IMAGE_TAG` 和 pod template `source-commit` 来自 `deploy/artifact-catalog.dev.json` 的 per-service artifact identity;普通 env、replica、healthPath、profile 等配置来自 `deploy/deploy.json`。全局 GitOps metadata 仍记录本次 source commit。`--legacy-source-images` 与 `HWLAB_G14_USE_DEPLOY_IMAGES=0` 已废弃,不得把所有 workload image 回退渲染为同一个 source commit tag。
|
||||
- G14 Tekton promotion 在推送 `G14-gitops` 前,必须用 publish report 刷新 workspace 内的 `deploy/artifact-catalog.dev.json`,并且只在 catalog 变化时以 `chore: promote G14 GitOps source <short>` 提交回 `G14` source branch;poller 必须跳过这类生成提交,避免自触发循环。promotion 不得自动修改 `deploy/deploy.json` 或 `deploy/k8s/base/workloads.yaml`,这样人写配置不会和 CI 生成身份反复冲突。
|
||||
- Tekton 并发化只能以 planner 输出作为输入;每个 service 都有独立 TaskRun,changed service 启动 BuildKit,unchanged service 只写 reuse result 并复用 catalog digest,且不能改 pod template,避免无意义 rollout。没有完整 per-service desired state 证据时,必须修复 planner/catalog 证据,不能使用 `--full-build`、`--legacy-source-images`、DIND 或 Docker fallback 回退。
|
||||
|
||||
## 加速判定与当前瓶颈
|
||||
@@ -78,7 +78,7 @@ G14 CI/CD 加速的第一判定标准不是 PipelineRun 总耗时单点变短,
|
||||
- 触发延迟:`hwlab-g14-branch-poller` 固定每 1 分钟轮询 `G14`。相对 5 分钟轮询,平均触发等待应从约 2.5 分钟降到约 0.5 分钟,最坏等待从 5 分钟降到 1 分钟。发现 `kubectl -n hwlab-ci get cronjob hwlab-g14-branch-poller -o jsonpath='{.spec.schedule}'` 不是 `* * * * *` 时,应先修 poller/reconciler,而不是手工长期创建 PipelineRun。
|
||||
- 组件懒构建:docs-only、GitOps-only、poller/reconciler manifest-only 等不影响服务输入的变更,planner 应输出 `affectedServices=[]`、`buildSkippedCount=<全部服务数>`,各 service TaskRun 应写出 `status=reused` 和 `build-backend=reused-catalog`。这类变更不应重新构建任何服务镜像,也不应把 workload image 改成当前 source commit tag。
|
||||
- 并发 fan-out:per-service TaskRun 应由 Tekton/k8s scheduler 同时调度,多个 service 的 build/reuse 窗口应接近“最慢单个服务任务耗时”,而不是所有服务串行相加。reuse-only 场景的 fan-out 窗口当前基线约为十几秒;真实组件构建场景仍需按 changed service 单独记录 BuildKit 耗时和 cache hit 情况。
|
||||
- CD rollout:unchanged service 必须复用 `deploy.services[]` 的 image/digest/env,pod template 不应因全局 source commit 改变而无意义滚动。手写 manifest(例如 `deepseek-proxy`、device-agent 类辅助 workload)只要复用某个已发布服务镜像,也必须走同一个 deploy catalog image 选择逻辑,不能直接用当前 source commit tag。
|
||||
- CD rollout:unchanged service 必须复用 `deploy/artifact-catalog.dev.json` 的 image/digest,pod template 不应因全局 source commit 改变而无意义滚动。手写 manifest(例如 `deepseek-proxy`、device-agent 类辅助 workload)只要复用某个已发布服务镜像,也必须走同一个 catalog image 选择逻辑,不能直接用当前 source commit tag。
|
||||
- 运行态验证:GitOps promote 成功只说明 `G14-gitops` 分支更新;最终通过必须看 Argo Application revision、sync 状态、目标 namespace Deployment/StatefulSet ready、公网 health。Argo 还停在旧 revision 时,优先做 `argocd.argoproj.io/refresh=hard` 刷新;不要把已经推送的 `G14-gitops` 分支误判成运行面已滚动。
|
||||
- Argo health 判定:`spec.suspend=true` 的模板型 Job 会让 Application 总 health 显示 `Suspended`。`hwlab-agent-worker-template`、`hwlab-cli-template` 这类只作为手动启动模板的 Job 必须带 `argocd.argoproj.io/ignore-healthcheck: "true"`,避免 DEV 实际 workload 全部 ready 时仍被误判为 Suspended;真正发布验收仍以长驻 workload ready、公网 health 和失败 Pod 清单为准。
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ async function makeFixture({
|
||||
deployEnvCommitId = commitId,
|
||||
deployEnvImageTag = commitId,
|
||||
deployEnvImage = null,
|
||||
deployEnvMirrors = true,
|
||||
deployEnvMirrors = false,
|
||||
deploySkillsCommitId = null,
|
||||
deployCodeAgentProviderEnv = true,
|
||||
deployDbEnv = true,
|
||||
@@ -123,25 +123,25 @@ async function makeFixture({
|
||||
const deploy = {
|
||||
manifestVersion: "v1",
|
||||
environment: "dev",
|
||||
commitId,
|
||||
services: [
|
||||
{
|
||||
serviceId,
|
||||
image,
|
||||
namespace: "hwlab-dev",
|
||||
healthPath: "/health/live",
|
||||
profile: "dev",
|
||||
replicas: 1,
|
||||
env: deployEnvMirrors
|
||||
? {
|
||||
env: {
|
||||
...(deployEnvMirrors
|
||||
? {
|
||||
HWLAB_COMMIT_ID: deployEnvCommitId,
|
||||
HWLAB_IMAGE: deployEnvImage ?? image,
|
||||
HWLAB_IMAGE_TAG: deployEnvImageTag,
|
||||
...(deploySkillsCommitId ? { HWLAB_SKILLS_COMMIT_ID: deploySkillsCommitId } : {}),
|
||||
...dbEnv,
|
||||
...providerEnv
|
||||
...(deploySkillsCommitId ? { HWLAB_SKILLS_COMMIT_ID: deploySkillsCommitId } : {})
|
||||
}
|
||||
: {}
|
||||
: {}),
|
||||
...dbEnv,
|
||||
...providerEnv
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
@@ -256,19 +256,16 @@ test("passes internally consistent desired-state", async () => {
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.summary.desiredCommitId, "abc1234");
|
||||
assert.equal(plan.summary.presentMirrorCount, 6);
|
||||
assert.equal(plan.summary.presentMirrorCount, 3);
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("passes mixed desired-state when unchanged services reuse service-level artifact tags", async () => {
|
||||
const repoRoot = await makeFixture({
|
||||
commitId: "803ba22",
|
||||
deployImageTag: "6a84959",
|
||||
catalogCommitId: "803ba22",
|
||||
catalogServiceCommitId: "6a84959",
|
||||
catalogImageTag: "6a84959",
|
||||
deployEnvCommitId: "6a84959",
|
||||
deployEnvImageTag: "6a84959",
|
||||
workloadTag: "6a84959",
|
||||
workloadEnvCommitId: "6a84959",
|
||||
workloadEnvImageTag: "6a84959"
|
||||
@@ -278,16 +275,16 @@ test("passes mixed desired-state when unchanged services reuse service-level art
|
||||
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.summary.desiredCommitId, "803ba22");
|
||||
assert.equal(plan.services[0].deploy.imageTag, "6a84959");
|
||||
assert.equal(plan.services[0].deploy.artifactIdentity, "human-authored-config");
|
||||
assert.equal(plan.services[0].catalog.commitId, "6a84959");
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("passes when cloud-web owns runtime identity mirrors", async () => {
|
||||
const repoRoot = await makeFixture({ serviceId: "hwlab-cloud-web" });
|
||||
test("passes when cloud-web relies on render-generated runtime identity mirrors", async () => {
|
||||
const repoRoot = await makeFixture({ serviceId: "hwlab-cloud-web", workloadEnvMirrors: false });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.equal(plan.summary.presentMirrorCount, 6);
|
||||
assert.equal(plan.summary.presentMirrorCount, 0);
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
@@ -362,21 +359,21 @@ test("blocks when cloud-api DB SSL mode drifts back to require", async () => {
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "cloud_api_db_contract_mismatch"));
|
||||
});
|
||||
|
||||
test("blocks when cloud-web runtime identity mirrors are missing", async () => {
|
||||
test("blocks when generated runtime identity leaks into deploy.json", async () => {
|
||||
const repoRoot = await makeFixture({
|
||||
serviceId: "hwlab-cloud-web",
|
||||
deployEnvMirrors: false,
|
||||
deployEnvMirrors: true,
|
||||
workloadEnvMirrors: false
|
||||
});
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.summary.blockers, 6);
|
||||
assert.equal(plan.summary.blockers, 3);
|
||||
assert.ok(plan.diagnostics.some((diagnostic) =>
|
||||
diagnostic.code === "missing_mirror" &&
|
||||
diagnostic.code === "generated_artifact_identity" &&
|
||||
diagnostic.path === "deploy/deploy.json.services.hwlab-cloud-web.env.HWLAB_COMMIT_ID"
|
||||
));
|
||||
assert.ok(plan.diagnostics.some((diagnostic) =>
|
||||
diagnostic.code === "missing_mirror" &&
|
||||
diagnostic.code === "generated_artifact_identity" &&
|
||||
diagnostic.path.endsWith(".env.HWLAB_IMAGE_TAG")
|
||||
));
|
||||
});
|
||||
@@ -410,7 +407,8 @@ test("target tag review is read-only promotion_pending when current state is uni
|
||||
assert.equal(plan.status, "planned");
|
||||
assert.equal(plan.target.convergence.state, "promotion_pending");
|
||||
assert.equal(plan.summary.blockers, 0);
|
||||
assert.match(plan.services[0].promotion.deployImage, /:def5678$/u);
|
||||
assert.equal(plan.services[0].promotion.deployImage, null);
|
||||
assert.match(plan.services[0].promotion.catalogImage, /:def5678$/u);
|
||||
});
|
||||
|
||||
test("target check blocks when current desired-state is uniformly older", async () => {
|
||||
@@ -421,7 +419,7 @@ test("target check blocks when current desired-state is uniformly older", async
|
||||
assert.equal(plan.target.convergenceRequirement, "target-check");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) =>
|
||||
diagnostic.code === "target_desired_state_mismatch" &&
|
||||
diagnostic.message.includes("stale desired-state is not pinned")
|
||||
diagnostic.message.includes("stale artifact catalog is not pinned")
|
||||
));
|
||||
});
|
||||
|
||||
@@ -490,31 +488,28 @@ test("promotion commit check blocks an explicit non-matching target tag", async
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "promotion_tag_mismatch"));
|
||||
});
|
||||
|
||||
test("blocks on mirror drift", async () => {
|
||||
test("ignores generated workload mirror drift because render owns runtime identity", async () => {
|
||||
const repoRoot = await makeFixture({ workloadEnvImageTag: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.equal(plan.summary.blockers, 1);
|
||||
assert.equal(plan.diagnostics[0].code, "mirror_mismatch");
|
||||
assert.match(plan.diagnostics[0].path, /HWLAB_IMAGE_TAG/u);
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("blocks on skills commit mirror drift", async () => {
|
||||
const repoRoot = await makeFixture({ deploySkillsCommitId: "badcafe" });
|
||||
test("blocks on skills commit mirror leak in deploy.json", async () => {
|
||||
const repoRoot = await makeFixture({ deployEnvMirrors: true, deploySkillsCommitId: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) =>
|
||||
diagnostic.code === "mirror_mismatch" &&
|
||||
diagnostic.code === "generated_artifact_identity" &&
|
||||
diagnostic.path.endsWith("HWLAB_SKILLS_COMMIT_ID")
|
||||
));
|
||||
});
|
||||
|
||||
test("blocks on workload image drift", async () => {
|
||||
test("ignores workload image drift because catalog is artifact truth", async () => {
|
||||
const repoRoot = await makeFixture({ workloadTag: "badcafe" });
|
||||
const plan = await buildDesiredStatePlan({ repoRoot });
|
||||
assert.equal(plan.status, "blocked");
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "image_tag_mismatch"));
|
||||
assert.ok(plan.diagnostics.some((diagnostic) => diagnostic.code === "image_mismatch"));
|
||||
assert.equal(plan.status, "pass");
|
||||
assert.deepEqual(plan.diagnostics, []);
|
||||
});
|
||||
|
||||
test("reports partial target drift when catalog top-level commit moves but services stay old", async () => {
|
||||
|
||||
@@ -39,7 +39,7 @@ const sourceIdentity = Object.freeze({
|
||||
const expectedRuntimeIdentity = Object.freeze({
|
||||
status: "observed",
|
||||
serviceId: "hwlab-cloud-api",
|
||||
source: "deploy/artifact-catalog.dev.json+deploy/deploy.json",
|
||||
source: "deploy/artifact-catalog.dev.json+deploy/deploy.json:runtime-config",
|
||||
commitId: "7de6edd",
|
||||
imageTag: "7de6edd",
|
||||
image: "127.0.0.1:5000/hwlab/hwlab-cloud-api:7de6edd"
|
||||
@@ -307,7 +307,7 @@ test("live workbench identity uses deploy desired runtime identity instead of gi
|
||||
assert.equal(deployed.status, "pass");
|
||||
assert.equal(deployed.expectedCommit, "7de6edd");
|
||||
assert.equal(deployed.expectedReportCommitId, "ff4b3f928f76");
|
||||
assert.equal(deployed.expectedSource, "deploy/artifact-catalog.dev.json+deploy/deploy.json");
|
||||
assert.equal(deployed.expectedSource, "deploy/artifact-catalog.dev.json+deploy/deploy.json:runtime-config");
|
||||
});
|
||||
|
||||
test("live web asset identity blocks stale deployed primary assets", () => {
|
||||
|
||||
@@ -211,7 +211,7 @@ function usage() {
|
||||
` --web-endpoint URL default: ${defaultWebEndpoint}`,
|
||||
` --prod-runtime-endpoint URL default: ${defaultProdRuntimeEndpoint}`,
|
||||
` --prod-web-endpoint URL default: ${defaultProdWebEndpoint}`,
|
||||
" --use-deploy-images default and only supported mode: render workload images/env from deploy/deploy.json per service",
|
||||
" --use-deploy-images default and only supported mode: render workload images from deploy/artifact-catalog.dev.json per service",
|
||||
" --check verify generated files are current without writing",
|
||||
" --no-write plan and print generated file list without writing"
|
||||
].join("\n");
|
||||
@@ -295,6 +295,18 @@ function upsertEnv(envList, name, value) {
|
||||
}
|
||||
}
|
||||
|
||||
function upsertEnvEntry(envList, entry) {
|
||||
if (!Array.isArray(envList) || !entry?.name) return;
|
||||
const existing = envList.find((item) => item?.name === entry.name);
|
||||
if (existing) {
|
||||
delete existing.value;
|
||||
delete existing.valueFrom;
|
||||
Object.assign(existing, cloneJson(entry));
|
||||
} else {
|
||||
envList.push(cloneJson(entry));
|
||||
}
|
||||
}
|
||||
|
||||
function annotate(metadata, annotations) {
|
||||
metadata.annotations = { ...(metadata.annotations ?? {}), ...annotations };
|
||||
}
|
||||
@@ -315,37 +327,34 @@ function imageTagFromReference(image) {
|
||||
return value.slice(colonIndex + 1).split("@", 1)[0] || null;
|
||||
}
|
||||
|
||||
function runtimeCommitForDeployService(deployService, source) {
|
||||
return deployService?.env?.HWLAB_COMMIT_ID ?? deployService?.commitId ?? imageTagFromReference(deployService?.image) ?? source.full;
|
||||
function catalogServiceById(catalog, serviceId) {
|
||||
return (catalog?.services ?? []).find((service) => service?.serviceId === serviceId) ?? null;
|
||||
}
|
||||
|
||||
function runtimeImageTagForDeployService(deployService, source) {
|
||||
return deployService?.env?.HWLAB_IMAGE_TAG ?? imageTagFromReference(deployService?.image) ?? source.short;
|
||||
}
|
||||
|
||||
function deployServiceById(deploy, serviceId) {
|
||||
return (deploy?.services ?? []).find((service) => service?.serviceId === serviceId) ?? null;
|
||||
}
|
||||
|
||||
function runtimeImageForService({ deploy, serviceId, source, registryPrefix, useDeployImages = false }) {
|
||||
const deployService = deployServiceById(deploy, serviceId);
|
||||
return useDeployImages && deployService?.image
|
||||
? deployService.image
|
||||
function runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages = false }) {
|
||||
const catalogService = catalogServiceById(catalog, serviceId);
|
||||
const image = useDeployImages && catalogService?.image
|
||||
? catalogService.image
|
||||
: imageFor(registryPrefix, serviceId, source.short);
|
||||
}
|
||||
|
||||
function runtimeCommitForService({ deploy, serviceId, source, useDeployImages = false }) {
|
||||
const deployService = deployServiceById(deploy, serviceId);
|
||||
return useDeployImages
|
||||
? runtimeCommitForDeployService(deployService, source)
|
||||
const imageTag = useDeployImages && catalogService?.imageTag
|
||||
? catalogService.imageTag
|
||||
: imageTagFromReference(image) ?? source.short;
|
||||
const commit = useDeployImages
|
||||
? catalogService?.sourceCommitId ?? catalogService?.commitId ?? imageTag ?? source.full
|
||||
: source.full;
|
||||
return { image, imageTag, commit };
|
||||
}
|
||||
|
||||
function runtimeImageTagForService({ deploy, serviceId, source, useDeployImages = false }) {
|
||||
const deployService = deployServiceById(deploy, serviceId);
|
||||
return useDeployImages
|
||||
? runtimeImageTagForDeployService(deployService, source)
|
||||
: source.short;
|
||||
function runtimeImageForService({ catalog, serviceId, source, registryPrefix, useDeployImages = false }) {
|
||||
return runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages }).image;
|
||||
}
|
||||
|
||||
function runtimeCommitForService({ catalog, serviceId, source, registryPrefix, useDeployImages = false }) {
|
||||
return runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages }).commit;
|
||||
}
|
||||
|
||||
function runtimeImageTagForService({ catalog, serviceId, source, registryPrefix, useDeployImages = false }) {
|
||||
return runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages }).imageTag;
|
||||
}
|
||||
|
||||
function namespaceNameForProfile(profile) {
|
||||
@@ -395,7 +404,34 @@ function namespaceScopedHost(value, namespace) {
|
||||
return value.replace(/\.hwlab-dev\.svc\.cluster\.local/gu, `.${namespace}.svc.cluster.local`);
|
||||
}
|
||||
|
||||
function transformWorkloads({ workloads, deploy, source, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", useDeployImages = false }) {
|
||||
function deployEnvEntry(name, value, namespace) {
|
||||
if (typeof value === "string" && value.startsWith("secretRef:")) {
|
||||
const ref = value.slice("secretRef:".length);
|
||||
const slashIndex = ref.indexOf("/");
|
||||
if (slashIndex > 0 && slashIndex < ref.length - 1) {
|
||||
return {
|
||||
name,
|
||||
valueFrom: {
|
||||
secretKeyRef: {
|
||||
name: ref.slice(0, slashIndex),
|
||||
key: ref.slice(slashIndex + 1),
|
||||
optional: true
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
return { name, value: namespaceScopedHost(value, namespace) };
|
||||
}
|
||||
|
||||
function applyDeployEnv(envList, deployService, namespace) {
|
||||
if (!deployService?.env || typeof deployService.env !== "object" || Array.isArray(deployService.env)) return;
|
||||
for (const [name, value] of Object.entries(deployService.env)) {
|
||||
upsertEnvEntry(envList, deployEnvEntry(name, value, namespace));
|
||||
}
|
||||
}
|
||||
|
||||
function transformWorkloads({ workloads, deploy, catalog, source, registryPrefix, runtimeEndpoint, webEndpoint, profile = "dev", useDeployImages = false }) {
|
||||
const result = cloneJson(workloads);
|
||||
const deployServices = new Map((deploy.services ?? []).map((service) => [service.serviceId, service]));
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
@@ -434,10 +470,8 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
|
||||
const podTemplate = item?.spec?.template;
|
||||
if (!podTemplate?.spec?.containers) continue;
|
||||
const templateServiceId = serviceIdForWorkload(item, podTemplate.spec.containers[0]);
|
||||
const templateDeployService = deployServices.get(templateServiceId);
|
||||
const templateSourceCommit = useDeployImages
|
||||
? runtimeCommitForDeployService(templateDeployService, source)
|
||||
: source.full;
|
||||
const templateArtifact = runtimeArtifactForService({ catalog, serviceId: templateServiceId, source, registryPrefix, useDeployImages });
|
||||
const templateSourceCommit = templateArtifact.commit;
|
||||
label(podTemplate.metadata ??= {}, {
|
||||
...stableRuntimeLabels,
|
||||
"hwlab.pikastech.local/gitops-target": "g14",
|
||||
@@ -457,21 +491,24 @@ function transformWorkloads({ workloads, deploy, source, registryPrefix, runtime
|
||||
const serviceId = serviceIdForWorkload(item, container);
|
||||
const deployService = deployServices.get(serviceId);
|
||||
if (!deployService) continue;
|
||||
const image = useDeployImages && deployService.image
|
||||
? deployService.image
|
||||
: imageFor(registryPrefix, serviceId, source.short);
|
||||
const runtimeCommit = useDeployImages ? runtimeCommitForDeployService(deployService, source) : source.full;
|
||||
const runtimeImageTag = useDeployImages ? runtimeImageTagForDeployService(deployService, source) : source.short;
|
||||
const artifact = runtimeArtifactForService({ catalog, serviceId, source, registryPrefix, useDeployImages });
|
||||
const image = artifact.image;
|
||||
const runtimeCommit = artifact.commit;
|
||||
const runtimeImageTag = artifact.imageTag;
|
||||
container.image = image;
|
||||
container.imagePullPolicy = "IfNotPresent";
|
||||
container.env ??= [];
|
||||
for (const entry of container.env) {
|
||||
if (Object.hasOwn(entry, "value")) entry.value = namespaceScopedHost(entry.value, namespace);
|
||||
}
|
||||
applyDeployEnv(container.env, deployService, namespace);
|
||||
upsertEnv(container.env, "HWLAB_ENVIRONMENT", profileLabel);
|
||||
upsertEnv(container.env, "HWLAB_COMMIT_ID", runtimeCommit);
|
||||
upsertEnv(container.env, "HWLAB_IMAGE", image);
|
||||
upsertEnv(container.env, "HWLAB_IMAGE_TAG", runtimeImageTag);
|
||||
if (serviceId === "hwlab-agent-skills") {
|
||||
upsertEnv(container.env, "HWLAB_SKILLS_COMMIT_ID", runtimeCommit);
|
||||
}
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_TARGET", "g14");
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_PROFILE", profileLabel);
|
||||
upsertEnv(container.env, "HWLAB_GITOPS_SOURCE_COMMIT", source.full);
|
||||
@@ -881,9 +918,9 @@ git config --global user.name "HWLAB G14 GitOps Bot"
|
||||
git config --global user.email "hwlab-g14-gitops-bot@users.noreply.github.com"
|
||||
if [ -s /workspace/source/dev-artifacts.json ]; then
|
||||
node scripts/refresh-artifact-catalog.mjs --target-ref "$(params.revision)" --publish-report /workspace/source/dev-artifacts.json
|
||||
if ! git diff --quiet -- deploy/deploy.json deploy/artifact-catalog.dev.json deploy/k8s/base/workloads.yaml; then
|
||||
if ! git diff --quiet -- deploy/artifact-catalog.dev.json; then
|
||||
short="$(printf '%.7s' "$(params.revision)")"
|
||||
git add deploy/deploy.json deploy/artifact-catalog.dev.json deploy/k8s/base/workloads.yaml
|
||||
git add deploy/artifact-catalog.dev.json
|
||||
git commit -m "chore: promote G14 GitOps source $short"
|
||||
git push origin "HEAD:$(params.source-branch)"
|
||||
fi
|
||||
@@ -1823,12 +1860,12 @@ remotePort = ${edgeRemotePort}
|
||||
};
|
||||
}
|
||||
|
||||
function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defaultRegistryPrefix, deploy = null, useDeployImages = false } = {}) {
|
||||
function deepSeekProxyManifest({ profile = "dev", source, registryPrefix = defaultRegistryPrefix, catalog = null, useDeployImages = false } = {}) {
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const bridgeServiceId = "hwlab-cloud-api";
|
||||
const bridgeImage = runtimeImageForService({ deploy, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeCommit = runtimeCommitForService({ deploy, serviceId: bridgeServiceId, source, useDeployImages });
|
||||
const bridgeImageTag = runtimeImageTagForService({ deploy, serviceId: bridgeServiceId, source, useDeployImages });
|
||||
const bridgeImage = runtimeImageForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeCommit = runtimeCommitForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeImageTag = runtimeImageTagForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": "hwlab-deepseek-proxy",
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
@@ -2108,14 +2145,14 @@ function deviceAgent71FreqServerScript() {
|
||||
].join("\n") + "\n";
|
||||
}
|
||||
|
||||
function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, deploy = null, useDeployImages = false }) {
|
||||
function deviceAgent71FreqManifest({ profile = "dev", source, registryPrefix, catalog = null, useDeployImages = false }) {
|
||||
assert.equal(profile, "dev", "71-freq device-agent is dev-only");
|
||||
const namespace = namespaceNameForProfile(profile);
|
||||
const name = "device-agent-71-freq";
|
||||
const bridgeServiceId = "hwlab-cloud-api";
|
||||
const bridgeImage = runtimeImageForService({ deploy, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeCommit = runtimeCommitForService({ deploy, serviceId: bridgeServiceId, source, useDeployImages });
|
||||
const bridgeImageTag = runtimeImageTagForService({ deploy, serviceId: bridgeServiceId, source, useDeployImages });
|
||||
const bridgeImage = runtimeImageForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeCommit = runtimeCommitForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const bridgeImageTag = runtimeImageTagForService({ catalog, serviceId: bridgeServiceId, source, registryPrefix, useDeployImages });
|
||||
const labels = {
|
||||
"app.kubernetes.io/name": name,
|
||||
"app.kubernetes.io/part-of": "hwlab",
|
||||
@@ -2213,11 +2250,11 @@ function runtimeKustomization({ profile = "dev", includeDeviceAgent = profile ==
|
||||
function readme({ source, args }) {
|
||||
return `# HWLAB G14 GitOps CI/CD
|
||||
|
||||
This directory is generated from built-in Tekton-native CI primitive declarations plus \`deploy/deploy.json\` and \`deploy/k8s/*\` by \`scripts/g14-gitops-render.mjs\`.
|
||||
This directory is generated from built-in Tekton-native CI primitive declarations plus human-authored \`deploy/deploy.json\`, CI-authored \`deploy/artifact-catalog.dev.json\`, and \`deploy/k8s/*\` by \`scripts/g14-gitops-render.mjs\`.
|
||||
|
||||
- Target: G14 k3s only.
|
||||
- CI: Tekton Pipeline \`hwlab-ci/hwlab-g14-ci-image-publish\`.
|
||||
- Artifact contract: images are tagged by source git commit \`${source.short}\` and pushed to \`${args.registryPrefix}\`.
|
||||
- Artifact contract: runtime images come from \`deploy/artifact-catalog.dev.json\`; \`deploy/deploy.json\` only carries human-authored config such as env and topology.
|
||||
- Branch split: source branch \`${args.sourceBranch}\` remains the watched code branch; generated desired state is promoted to \`${args.gitopsBranch}\`.
|
||||
- CD: Argo CD Applications \`argocd/hwlab-g14-dev\` and \`argocd/hwlab-g14-prod\` consume \`${args.gitopsBranch}:deploy/gitops/g14/runtime-dev\` and \`runtime-prod\`.
|
||||
- Public preview: FRP exposes G14 DEV web \`${args.webEndpoint}\` / edge \`${args.runtimeEndpoint}\` through \`hwlab-dev/hwlab-g14-frpc\`, and G14 PROD web \`${args.prodWebEndpoint}\` / edge \`${args.prodRuntimeEndpoint}\` through \`hwlab-prod/hwlab-g14-prod-frpc\`; D601 keeps \`:16666/:16667\`.
|
||||
@@ -2229,8 +2266,9 @@ This directory is generated from built-in Tekton-native CI primitive declaration
|
||||
}
|
||||
|
||||
async function plannedFiles(args) {
|
||||
const [deploy, namespaceTemplate, config, services, health, workloads] = await Promise.all([
|
||||
const [deploy, artifactCatalog, namespaceTemplate, config, services, health, workloads] = await Promise.all([
|
||||
readJson("deploy/deploy.json"),
|
||||
readJson("deploy/artifact-catalog.dev.json"),
|
||||
readJson("deploy/k8s/base/namespace.yaml"),
|
||||
readJson("deploy/k8s/base/code-agent-codex-config.yaml"),
|
||||
readJson("deploy/k8s/base/services.yaml"),
|
||||
@@ -2267,7 +2305,8 @@ async function plannedFiles(args) {
|
||||
};
|
||||
if (args.useDeployImages) {
|
||||
sourceDescriptor.renderMode = {
|
||||
imageSource: "deploy.services",
|
||||
imageSource: "deploy/artifact-catalog.dev.json",
|
||||
configSource: "deploy/deploy.json",
|
||||
mixedDesiredState: true
|
||||
};
|
||||
}
|
||||
@@ -2298,9 +2337,9 @@ async function plannedFiles(args) {
|
||||
putJson(`${runtimePath}/code-agent-codex-config.yaml`, transformListNamespace(config, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/services.yaml`, transformListNamespace(services, namespace, profileLabels, annotations));
|
||||
putJson(`${runtimePath}/health-contract.yaml`, transformHealthContract(health, namespace, profileLabels, annotations, endpoints.runtimeEndpoint, endpoints.webEndpoint, profile));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix, deploy, useDeployImages: args.useDeployImages }));
|
||||
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, deploy, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/workloads.yaml`, transformWorkloads({ workloads, deploy, catalog: artifactCatalog, source, registryPrefix: args.registryPrefix, runtimeEndpoint: endpoints.runtimeEndpoint, webEndpoint: endpoints.webEndpoint, profile, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/deepseek-proxy.yaml`, deepSeekProxyManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||
if (includeDeviceAgent) putJson(`${runtimePath}/device-agent-71-freq.yaml`, deviceAgent71FreqManifest({ profile, source, registryPrefix: args.registryPrefix, catalog: artifactCatalog, useDeployImages: args.useDeployImages }));
|
||||
putJson(`${runtimePath}/g14-frpc.yaml`, g14FrpcManifest({ profile }));
|
||||
}
|
||||
return { files, source };
|
||||
|
||||
@@ -17,7 +17,6 @@ const execFileAsync = promisify(execFile);
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
const catalogPath = "deploy/artifact-catalog.dev.json";
|
||||
const deployPath = "deploy/deploy.json";
|
||||
const workloadsPath = "deploy/k8s/base/workloads.yaml";
|
||||
const defaultPublishReportPath = tempReportPath("dev-artifacts.json");
|
||||
const defaultRegistryPrefix = process.env.HWLAB_DEV_REGISTRY_PREFIX || process.env.HWLAB_DEV_REGISTRY || "127.0.0.1:5000/hwlab";
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
@@ -225,49 +224,7 @@ function publishRecordsFromReport(report, target) {
|
||||
return { records, requiredServiceIds, disabledServiceIds };
|
||||
}
|
||||
|
||||
function updateEnvObject(env, service, target, publishRecord) {
|
||||
if (!env || typeof env !== "object" || Array.isArray(env)) return;
|
||||
const shortCommitId = shortCommitForRecord(publishRecord, target);
|
||||
const revision = revisionForRecord(publishRecord, target);
|
||||
if (Object.hasOwn(env, "HWLAB_COMMIT_ID")) env.HWLAB_COMMIT_ID = shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE")) env.HWLAB_IMAGE = service.image;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_TAG")) env.HWLAB_IMAGE_TAG = service.imageTag;
|
||||
if (Object.hasOwn(env, "HWLAB_REVISION")) env.HWLAB_REVISION = revision;
|
||||
if (Object.hasOwn(env, "HWLAB_BUILD_CREATED_AT")) env.HWLAB_BUILD_CREATED_AT = publishRecord?.buildCreatedAt ?? "";
|
||||
if (Object.hasOwn(env, "HWLAB_BUILD_SOURCE")) env.HWLAB_BUILD_SOURCE = publishRecord?.buildSource ?? "";
|
||||
if (Object.hasOwn(env, "HWLAB_SKILLS_COMMIT_ID")) env.HWLAB_SKILLS_COMMIT_ID = shortCommitId;
|
||||
if (Object.hasOwn(env, "HWLAB_IMAGE_DIGEST")) env.HWLAB_IMAGE_DIGEST = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
|
||||
function serviceIdForWorkload(item, container) {
|
||||
return (
|
||||
item?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
||||
item?.spec?.template?.metadata?.labels?.["hwlab.pikastech.local/service-id"] ||
|
||||
container?.name
|
||||
);
|
||||
}
|
||||
|
||||
function updateEnvList(envList, service, target, publishRecord) {
|
||||
if (!Array.isArray(envList)) return;
|
||||
const shortCommitId = shortCommitForRecord(publishRecord, target);
|
||||
const revision = revisionForRecord(publishRecord, target);
|
||||
for (const entry of envList) {
|
||||
if (entry.name === "HWLAB_COMMIT_ID") entry.value = shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE") entry.value = service.image;
|
||||
if (entry.name === "HWLAB_IMAGE_TAG") entry.value = service.imageTag;
|
||||
if (entry.name === "HWLAB_REVISION") entry.value = revision;
|
||||
if (entry.name === "HWLAB_BUILD_CREATED_AT") entry.value = publishRecord?.buildCreatedAt ?? "";
|
||||
if (entry.name === "HWLAB_BUILD_SOURCE") entry.value = publishRecord?.buildSource ?? "";
|
||||
if (entry.name === "HWLAB_SKILLS_COMMIT_ID") entry.value = shortCommitId;
|
||||
if (entry.name === "HWLAB_IMAGE_DIGEST") entry.value = publishRecord?.digest ?? "not_published";
|
||||
}
|
||||
}
|
||||
|
||||
function workloadContainers(item) {
|
||||
return item?.spec?.template?.spec?.containers ?? [];
|
||||
}
|
||||
|
||||
function refreshDocuments({ deploy, catalog, workloads, target, publishRecords, serviceInventory, provenancePath, registryPrefix }) {
|
||||
function refreshDocuments({ deploy, catalog, target, publishRecords, serviceInventory, provenancePath, registryPrefix }) {
|
||||
const published = Boolean(publishRecords);
|
||||
const records = publishRecords?.records ?? null;
|
||||
const requiredIds = new Set(publishRecords?.requiredServiceIds ?? serviceInventory.requiredServiceIds);
|
||||
@@ -276,7 +233,6 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
const inventoryByService = new Map(serviceInventory.services.map((service) => [service.serviceId, service]));
|
||||
const refreshedServices = [];
|
||||
|
||||
deploy.commitId = target.shortCommitId;
|
||||
catalog.commitId = target.shortCommitId;
|
||||
catalog.artifactState = published ? "published" : "contract-skeleton";
|
||||
catalog.serviceInventory = serviceInventory;
|
||||
@@ -303,9 +259,6 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
const imageTag = publishRecord?.imageTag ?? target.shortCommitId;
|
||||
const serviceCommitId = shortCommitForRecord(publishRecord, target);
|
||||
|
||||
deployService.image = image;
|
||||
updateEnvObject(deployService.env, { image, imageTag }, target, publishRecord);
|
||||
|
||||
catalogService.commitId = serviceCommitId;
|
||||
catalogService.image = image;
|
||||
catalogService.imageTag = imageTag;
|
||||
@@ -347,16 +300,6 @@ function refreshDocuments({ deploy, catalog, workloads, target, publishRecords,
|
||||
});
|
||||
}
|
||||
|
||||
for (const item of workloads.items ?? []) {
|
||||
for (const container of workloadContainers(item)) {
|
||||
const serviceId = serviceIdForWorkload(item, container);
|
||||
const service = catalogByService.get(serviceId);
|
||||
if (!service) continue;
|
||||
container.image = service.image;
|
||||
updateEnvList(container.env, service, target, records?.get(serviceId) ?? null);
|
||||
}
|
||||
}
|
||||
|
||||
return refreshedServices;
|
||||
}
|
||||
|
||||
@@ -374,10 +317,9 @@ async function main() {
|
||||
}
|
||||
|
||||
const target = await resolveTarget(args.targetRef);
|
||||
const [deploy, catalog, workloads, publishReport] = await Promise.all([
|
||||
const [deploy, catalog, publishReport] = await Promise.all([
|
||||
readJson(deployPath),
|
||||
readJson(catalogPath),
|
||||
readJson(workloadsPath),
|
||||
args.publishReportPath ? readJson(args.publishReportPath) : Promise.resolve(null)
|
||||
]);
|
||||
assertDevOnlyDeployAndCatalog(deploy, catalog);
|
||||
@@ -389,7 +331,6 @@ async function main() {
|
||||
const services = refreshDocuments({
|
||||
deploy,
|
||||
catalog,
|
||||
workloads,
|
||||
target,
|
||||
publishRecords,
|
||||
serviceInventory,
|
||||
@@ -398,11 +339,7 @@ async function main() {
|
||||
});
|
||||
|
||||
if (args.write) {
|
||||
await Promise.all([
|
||||
writeJson(deployPath, deploy),
|
||||
writeJson(catalogPath, catalog),
|
||||
writeJson(workloadsPath, workloads)
|
||||
]);
|
||||
await writeJson(catalogPath, catalog);
|
||||
}
|
||||
|
||||
console.log(JSON.stringify({
|
||||
@@ -410,7 +347,8 @@ async function main() {
|
||||
targetRef: target.ref,
|
||||
sourceCommitId: target.commitId,
|
||||
artifactCommitId: target.shortCommitId,
|
||||
wrote: args.write ? [deployPath, catalogPath, workloadsPath] : [],
|
||||
wrote: args.write ? [catalogPath] : [],
|
||||
deployTruthPolicy: "deploy.json is human-authored runtime config; artifact promotion must not rewrite it",
|
||||
ciPublished: Boolean(publishRecords),
|
||||
registryVerified: Boolean(publishRecords),
|
||||
publishedCount: publishRecords?.records.size ?? 0,
|
||||
|
||||
@@ -26,13 +26,12 @@ const mirrorEnvNames = [
|
||||
"HWLAB_SKILLS_COMMIT_ID",
|
||||
"HWLAB_IMAGE_DIGEST"
|
||||
];
|
||||
const forbiddenDeployArtifactFields = new Set(["commitId", "image", "imageTag", "digest", "sourceCommitId", "repositoryDigest", "publishState"]);
|
||||
const forbiddenDeployArtifactEnv = new Set(mirrorEnvNames.concat(["HWLAB_REVISION", "HWLAB_BUILD_CREATED_AT", "HWLAB_BUILD_SOURCE"]));
|
||||
const commitMirrorEnvNames = new Set(["HWLAB_COMMIT_ID", "HWLAB_SKILLS_COMMIT_ID"]);
|
||||
const imageMirrorEnvNames = new Set(["HWLAB_IMAGE"]);
|
||||
const tagMirrorEnvNames = new Set(["HWLAB_IMAGE_TAG"]);
|
||||
const digestMirrorEnvNames = new Set(["HWLAB_IMAGE_DIGEST"]);
|
||||
const requiredMirrorEnvNamesByService = new Map([
|
||||
["hwlab-cloud-web", ["HWLAB_COMMIT_ID", "HWLAB_IMAGE", "HWLAB_IMAGE_TAG"]]
|
||||
]);
|
||||
const tagPattern = /^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$/;
|
||||
const commitPattern = /^[a-f0-9]{7,40}$/;
|
||||
|
||||
@@ -621,7 +620,7 @@ function buildTargetConvergence(ctx, target) {
|
||||
path: "desired-state",
|
||||
message: promotionCommitRequired
|
||||
? "desired-state fields do not match the required promotion commit/tag"
|
||||
: `stale desired-state is not pinned to ${targetLabel(target)}; explicit target checks require current-main/commit-pinned deploy, catalog, workload, and runtime mirror fields`,
|
||||
: `stale artifact catalog is not pinned to ${targetLabel(target)}; explicit target checks require current-main/commit-pinned catalog identity`,
|
||||
expected: {
|
||||
commitId: target.comparisonCommitId,
|
||||
imageTag: target.comparisonTag
|
||||
@@ -691,7 +690,7 @@ function servicePromotion(service, target) {
|
||||
return {
|
||||
targetCommitId: target.comparisonCommitId ?? target.commitId,
|
||||
targetImageTag: targetTag,
|
||||
deployImage: service.deploy?.image ? replaceImageTag(service.deploy.image, targetTag) : null,
|
||||
deployImage: null,
|
||||
catalogImage: service.catalog?.image ? replaceImageTag(service.catalog.image, targetTag) : null,
|
||||
workloadImages: (service.workloads ?? []).map((workload) => ({
|
||||
workload: workload.name,
|
||||
@@ -738,17 +737,9 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
const deployByService = new Map(deployServices.map((service) => [service.serviceId, service]));
|
||||
const catalogByService = new Map(catalogServices.map((service) => [service.serviceId, service]));
|
||||
const workloadByService = indexWorkloads(workloads);
|
||||
const desiredCommitId = deploy.commitId;
|
||||
const desiredCommitId = catalog.commitId;
|
||||
const desiredImageTag = shortCommit(desiredCommitId);
|
||||
|
||||
recordObservation(ctx, {
|
||||
source: "deploy",
|
||||
path: `${deployPath}.commitId`,
|
||||
field: "commitId",
|
||||
kind: "commit",
|
||||
value: deploy.commitId,
|
||||
expected: desiredCommitId
|
||||
});
|
||||
recordObservation(ctx, {
|
||||
source: "catalog",
|
||||
path: `${catalogPath}.commitId`,
|
||||
@@ -758,11 +749,11 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
expected: desiredCommitId
|
||||
});
|
||||
|
||||
if (!commitPattern.test(String(deploy.commitId ?? ""))) {
|
||||
addMismatch(ctx, "invalid_commit", `${deployPath}.commitId`, "deploy commitId must be a short or full lowercase Git SHA", "7-40 lowercase hex", deploy.commitId);
|
||||
if (Object.hasOwn(deploy, "commitId")) {
|
||||
addMismatch(ctx, "generated_artifact_identity", `${deployPath}.commitId`, "deploy.json is human-authored config; generated commit identity must stay in artifact catalog", "absent", deploy.commitId);
|
||||
}
|
||||
if (!commitEquivalent(catalog.commitId, desiredCommitId)) {
|
||||
addMismatch(ctx, "commit_mismatch", `${catalogPath}.commitId`, "artifact catalog commitId must match deploy commitId", desiredCommitId, catalog.commitId);
|
||||
if (!commitPattern.test(String(catalog.commitId ?? ""))) {
|
||||
addMismatch(ctx, "invalid_commit", `${catalogPath}.commitId`, "artifact catalog commitId must be a short or full lowercase Git SHA", "7-40 lowercase hex", catalog.commitId);
|
||||
}
|
||||
|
||||
const serviceIds = serviceIdsFrom(
|
||||
@@ -796,44 +787,31 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
addMismatch(ctx, "missing_workload", `${workloadsPath}.${serviceId}`, `${serviceId} missing from k8s workloads`, "workload container", null, { serviceId });
|
||||
}
|
||||
|
||||
const deployImage = parseImageRef(deployService?.image);
|
||||
const catalogImage = parseImageRef(catalogService?.image);
|
||||
const serviceImageTag = catalogService?.imageTag ?? deployImage.tag ?? catalogImage.tag ?? desiredImageTag;
|
||||
const serviceImageTag = catalogService?.imageTag ?? catalogImage.tag ?? desiredImageTag;
|
||||
const serviceCommitId = catalogService?.commitId ?? serviceImageTag;
|
||||
const expectedService = {
|
||||
image: deployService?.image ?? catalogService?.image ?? null,
|
||||
imageTag: serviceImageTag,
|
||||
commitId: serviceCommitId,
|
||||
digest: catalogService?.digest ?? "not_published"
|
||||
};
|
||||
|
||||
if (deployService) {
|
||||
const deployPathBase = `${deployPath}.services.${serviceId}`;
|
||||
recordObservation(ctx, {
|
||||
source: "deploy",
|
||||
serviceId,
|
||||
path: `${deployPathBase}.image`,
|
||||
field: "image",
|
||||
kind: "image",
|
||||
value: deployService.image,
|
||||
expected: catalogService?.image ?? deployService.image
|
||||
});
|
||||
if (!deployImage.valid) {
|
||||
addMismatch(ctx, "invalid_image", `${deployPathBase}.image`, deployImage.reason, "tagged image ref", deployService.image, { serviceId });
|
||||
} else if (deployImage.tag !== serviceImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${deployPathBase}.image`, `${serviceId} deploy image tag must match service artifact tag`, serviceImageTag, deployImage.tag, { serviceId });
|
||||
for (const field of Object.keys(deployService)) {
|
||||
if (forbiddenDeployArtifactFields.has(field)) {
|
||||
addMismatch(ctx, "generated_artifact_identity", `${deployPathBase}.${field}`, `${serviceId} deploy service field is generated artifact identity and must stay in artifact catalog`, "absent", deployService[field], { serviceId });
|
||||
}
|
||||
}
|
||||
for (const envName of Object.keys(deployService.env ?? {})) {
|
||||
if (forbiddenDeployArtifactEnv.has(envName)) {
|
||||
addMismatch(ctx, "generated_artifact_identity", `${deployPathBase}.env.${envName}`, `${serviceId} deploy env is generated artifact identity and must be rendered from artifact catalog`, "absent", deployService.env[envName], { serviceId });
|
||||
}
|
||||
}
|
||||
service.deploy = {
|
||||
image: deployService.image,
|
||||
imageTag: deployImage.tag,
|
||||
artifactIdentity: "human-authored-config",
|
||||
envMirrors: inspectEnvMirrors(ctx, {
|
||||
source: "deploy",
|
||||
basePath: `${deployPathBase}.env`,
|
||||
serviceId,
|
||||
env: deployService.env ?? {},
|
||||
service: expectedService,
|
||||
desiredCommitId,
|
||||
requiredMirrorNames: requiredMirrorEnvNamesByService.get(serviceId) ?? []
|
||||
service: null,
|
||||
desiredCommitId: null,
|
||||
requiredMirrorNames: []
|
||||
})
|
||||
};
|
||||
}
|
||||
@@ -856,7 +834,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
field: "image",
|
||||
kind: "image",
|
||||
value: catalogService.image,
|
||||
expected: deployService?.image ?? catalogService.image
|
||||
expected: catalogService.image
|
||||
});
|
||||
recordObservation(ctx, {
|
||||
source: "catalog",
|
||||
@@ -881,9 +859,6 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
if (catalogImage.valid && catalogImage.tag !== catalogService.imageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${catalogPathBase}.image`, `${serviceId} catalog image tag must match catalog imageTag`, catalogService.imageTag, catalogImage.tag, { serviceId });
|
||||
}
|
||||
if (deployService?.image && catalogService.image !== deployService.image) {
|
||||
addMismatch(ctx, "image_mismatch", `${catalogPathBase}.image`, `${serviceId} catalog image must match deploy image`, deployService.image, catalogService.image, { serviceId });
|
||||
}
|
||||
service.catalog = {
|
||||
commitId: catalogService.commitId,
|
||||
image: catalogService.image,
|
||||
@@ -903,15 +878,10 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
field: "image",
|
||||
kind: "image",
|
||||
value: workload.image,
|
||||
expected: deployService?.image ?? catalogService?.image ?? null
|
||||
expected: catalogService?.image ?? null
|
||||
});
|
||||
if (!workloadImage.valid) {
|
||||
addMismatch(ctx, "invalid_image", `${workload.path}.image`, workloadImage.reason, "tagged image ref", workload.image, { serviceId });
|
||||
} else if (workloadImage.tag !== serviceImageTag) {
|
||||
addMismatch(ctx, "image_tag_mismatch", `${workload.path}.image`, `${serviceId} workload image tag must match service artifact tag`, serviceImageTag, workloadImage.tag, { serviceId });
|
||||
}
|
||||
if (deployService?.image && workload.image !== deployService.image) {
|
||||
addMismatch(ctx, "image_mismatch", `${workload.path}.image`, `${serviceId} workload image must match deploy image`, deployService.image, workload.image, { serviceId });
|
||||
}
|
||||
service.workloads.push({
|
||||
kind: workload.kind,
|
||||
@@ -925,9 +895,9 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
basePath: `${workload.path}.env`,
|
||||
serviceId,
|
||||
env: workload.env,
|
||||
service: expectedService,
|
||||
desiredCommitId,
|
||||
requiredMirrorNames: requiredMirrorEnvNamesByService.get(serviceId) ?? []
|
||||
service: null,
|
||||
desiredCommitId: null,
|
||||
requiredMirrorNames: []
|
||||
})
|
||||
});
|
||||
}
|
||||
@@ -976,7 +946,7 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
prod: false,
|
||||
devLiveClaim: false
|
||||
},
|
||||
checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, internal commit/image mirror drift, invalid target tags, partial target drift, or any mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. When the checked target ref is the current checked-out merge commit, the comparable current-main target is its accepted first parent because a commit cannot embed its own content hash. Without --check, a uniform older desired-state under --target-ref/--target-tag is a read-only promotion plan.",
|
||||
checkSemantics: "--check exits non-zero for missing/invalid desired-state sources, generated artifact identity leaking into deploy.json, invalid target tags, partial target drift, or catalog identity mismatch with an explicit --target-ref, --target-tag, or --promotion-commit. When the checked target ref is the current checked-out merge commit, the comparable current-main target is its accepted first parent because a commit cannot embed its own content hash. Without --check, a uniform older catalog under --target-ref/--target-tag is a read-only promotion plan.",
|
||||
summary: {
|
||||
desiredCommitId,
|
||||
desiredImageTag,
|
||||
@@ -998,8 +968,10 @@ export async function buildDesiredStatePlan(options = {}) {
|
||||
promotionBoundary: {
|
||||
writes: [],
|
||||
authoritativeDesiredState: [deployPath, catalogPath, workloadsPath],
|
||||
humanAuthoredTruth: [deployPath, workloadsPath],
|
||||
artifactIdentityTruth: [catalogPath],
|
||||
nonAuthoritativeEvidence: [artifactReportPath],
|
||||
note: "This planner reviews source desired-state only. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
|
||||
note: "This planner reviews source desired-state only. deploy.json is human-authored config; artifact-catalog carries generated image identity. It does not prove image existence, registry reachability, a real DEV apply, or M3 DEV-LIVE hardware-loop evidence."
|
||||
},
|
||||
cloudApiDb: cloudApiDb ?? {
|
||||
status: "blocked",
|
||||
|
||||
@@ -1887,12 +1887,12 @@ function observeExpectedRuntimeIdentity(serviceId) {
|
||||
const catalogService = Array.isArray(catalog?.services)
|
||||
? catalog.services.find((service) => service?.serviceId === serviceId)
|
||||
: null;
|
||||
const image = sanitizeRuntimeString(catalogService?.image ?? deployService?.image);
|
||||
const imageTag = sanitizeRuntimeString(catalogService?.imageTag ?? deployService?.env?.HWLAB_IMAGE_TAG ?? imageTagFromImage(image));
|
||||
const commitId = sanitizeRevision(catalogService?.commitId ?? deployService?.env?.HWLAB_COMMIT_ID ?? deploy?.commitId ?? catalog?.commitId ?? imageTag);
|
||||
const image = sanitizeRuntimeString(catalogService?.image);
|
||||
const imageTag = sanitizeRuntimeString(catalogService?.imageTag ?? imageTagFromImage(image));
|
||||
const commitId = sanitizeRevision(catalogService?.commitId ?? catalog?.commitId ?? imageTag);
|
||||
const sources = [];
|
||||
if (catalogService) sources.push("deploy/artifact-catalog.dev.json");
|
||||
if (deployService || deploy?.commitId) sources.push("deploy/deploy.json");
|
||||
if (deployService) sources.push("deploy/deploy.json:runtime-config");
|
||||
const observed = commitId !== "unknown" || imageTag !== "unknown";
|
||||
return {
|
||||
status: observed ? "observed" : "not_observed",
|
||||
|
||||
@@ -47,6 +47,17 @@ const blockerTypes = new Set([
|
||||
"safety_blocker"
|
||||
]);
|
||||
const digestPattern = /^sha256:[a-f0-9]{64}$/;
|
||||
const forbiddenDeployArtifactFields = new Set(["commitId", "image", "imageTag", "digest", "sourceCommitId", "repositoryDigest", "publishState"]);
|
||||
const forbiddenDeployArtifactEnv = new Set([
|
||||
"HWLAB_COMMIT_ID",
|
||||
"HWLAB_IMAGE",
|
||||
"HWLAB_IMAGE_TAG",
|
||||
"HWLAB_IMAGE_DIGEST",
|
||||
"HWLAB_REVISION",
|
||||
"HWLAB_BUILD_CREATED_AT",
|
||||
"HWLAB_BUILD_SOURCE",
|
||||
"HWLAB_SKILLS_COMMIT_ID"
|
||||
]);
|
||||
const artifactBuildInputPrefixes = [
|
||||
"cmd/",
|
||||
"internal/",
|
||||
@@ -394,7 +405,6 @@ async function artifactIdentityFor({ deploy, catalog, artifactReport, targetComm
|
||||
const requiredServices = requiredCatalogServices(catalog);
|
||||
const disabledServices = disabledCatalogServices(catalog);
|
||||
const catalogMatchesSource = matchesTargetCommit(catalog.commitId, artifactSourceCommit, artifactSourceShortCommit);
|
||||
const deployMatchesSource = matchesTargetCommit(deploy.commitId, artifactSourceCommit, artifactSourceShortCommit);
|
||||
const servicesMatchSource = serviceCommitIds.length === 1 &&
|
||||
matchesTargetCommit(serviceCommitIds[0], artifactSourceCommit, artifactSourceShortCommit);
|
||||
const allRequiredDigestsPublished = requiredServices.length > 0 &&
|
||||
@@ -421,9 +431,10 @@ async function artifactIdentityFor({ deploy, catalog, artifactReport, targetComm
|
||||
targetCoverage,
|
||||
deployManifest: {
|
||||
path: "deploy/deploy.json",
|
||||
commitId: deploy.commitId,
|
||||
matchesSource: deployMatchesSource,
|
||||
matchesTarget: matchesTargetCommit(deploy.commitId, targetCommit, targetShortCommit)
|
||||
identitySource: "human-authored-config",
|
||||
artifactIdentity: false,
|
||||
matchesSource: true,
|
||||
matchesTarget: true
|
||||
},
|
||||
artifactCatalog: {
|
||||
path: "deploy/artifact-catalog.dev.json",
|
||||
@@ -451,7 +462,7 @@ async function artifactIdentityFor({ deploy, catalog, artifactReport, targetComm
|
||||
notPublishedReason: service.notPublishedReason ?? null
|
||||
})),
|
||||
serviceCommitIds,
|
||||
matchesSource: targetCoverage.covered && deployMatchesSource && catalogMatchesSource && servicesMatchSource,
|
||||
matchesSource: targetCoverage.covered && catalogMatchesSource && servicesMatchSource,
|
||||
publishVerified,
|
||||
refreshCommands: {
|
||||
blocked: `node scripts/refresh-artifact-catalog.mjs --target-ref ${targetRef} --blocked`,
|
||||
@@ -467,6 +478,17 @@ function assertStaticContract(deploy, catalog) {
|
||||
assert.equal(deploy.profiles.dev.enabled, true, "dev profile must be enabled");
|
||||
assert.equal(deploy.profiles.prod.enabled, false, "prod profile must stay disabled");
|
||||
assert.deepEqual(deploy.services.map((service) => service.serviceId), SERVICE_IDS);
|
||||
for (const field of Object.keys(deploy)) {
|
||||
assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy ${field} must stay in artifact catalog`);
|
||||
}
|
||||
for (const service of deploy.services) {
|
||||
for (const field of Object.keys(service)) {
|
||||
assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy service ${service.serviceId}.${field} must stay in artifact catalog`);
|
||||
}
|
||||
for (const envName of Object.keys(service.env ?? {})) {
|
||||
assert.equal(forbiddenDeployArtifactEnv.has(envName), false, `deploy service ${service.serviceId}.env.${envName} must be rendered from artifact catalog`);
|
||||
}
|
||||
}
|
||||
|
||||
assert.equal(catalog.environment, ENVIRONMENT_DEV, "catalog environment must be dev");
|
||||
assert.equal(catalog.namespace, "hwlab-dev", "catalog namespace must be hwlab-dev");
|
||||
@@ -475,13 +497,11 @@ function assertStaticContract(deploy, catalog) {
|
||||
assert.ok(catalog.forbiddenProfiles.includes("prod"), "catalog must forbid prod");
|
||||
assert.deepEqual(catalog.services.map((service) => service.serviceId), SERVICE_IDS);
|
||||
assert.ok(unique(catalog.services.map((service) => service.serviceId)), "catalog service IDs must be unique");
|
||||
assert.equal(catalog.commitId, deploy.commitId, "catalog commitId must match deploy commitId");
|
||||
|
||||
const deployByService = new Map(deploy.services.map((service) => [service.serviceId, service]));
|
||||
for (const service of catalog.services) {
|
||||
const deployService = deployByService.get(service.serviceId);
|
||||
assert.ok(deployService, `${service.serviceId} missing from deploy manifest`);
|
||||
assert.equal(service.image, deployService.image, `${service.serviceId} image mismatch`);
|
||||
assert.equal(service.namespace, "hwlab-dev", `${service.serviceId} namespace mismatch`);
|
||||
assert.equal(service.profile, ENVIRONMENT_DEV, `${service.serviceId} profile mismatch`);
|
||||
assert.equal(service.healthPath, "/health/live", `${service.serviceId} health path mismatch`);
|
||||
@@ -1117,12 +1137,12 @@ async function validateLiveProbes(reporter, catalog, timeoutMs) {
|
||||
}
|
||||
}
|
||||
|
||||
function validateRuntimeBoundary(reporter, deploy) {
|
||||
const substituteImages = deploy.services
|
||||
function validateRuntimeBoundary(reporter, catalog) {
|
||||
const substituteImages = (catalog.services ?? [])
|
||||
.map((service) => service.image)
|
||||
.filter((image) => /unidesk|provider-gateway|microservice-proxy/.test(image));
|
||||
if (substituteImages.length === 0) {
|
||||
reporter.check("runtime-substitution-boundary", "safety", "pass", "Deploy images do not point at UniDesk/provider-gateway/microservice-proxy substitutes.");
|
||||
reporter.check("runtime-substitution-boundary", "safety", "pass", "Catalog images do not point at UniDesk/provider-gateway/microservice-proxy substitutes.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1130,7 +1150,7 @@ function validateRuntimeBoundary(reporter, deploy) {
|
||||
reporter.block({
|
||||
type: "safety_blocker",
|
||||
scope: "runtime-boundary",
|
||||
summary: "DEV deploy manifest references a forbidden non-HWLAB runtime substitute.",
|
||||
summary: "DEV artifact catalog references a forbidden non-HWLAB runtime substitute.",
|
||||
nextTask: "Replace substitute runtime references with HWLAB-owned service images."
|
||||
});
|
||||
}
|
||||
@@ -1245,7 +1265,7 @@ export async function runPreflight(argv) {
|
||||
validateArtifactPublishReport(reporter, optionalReports.artifactPublish, artifactIdentity, targetShortCommit, targetCommit, args.targetRef);
|
||||
validateEdgeHealthReport(reporter, optionalReports.edgeHealth);
|
||||
await validateEdgeContracts(reporter, masterEdge);
|
||||
validateRuntimeBoundary(reporter, deploy);
|
||||
validateRuntimeBoundary(reporter, catalog);
|
||||
await validateLiveProbes(reporter, catalog, args.timeoutMs);
|
||||
|
||||
const report = makeReport(args, targetCommit, targetShortCommit, reporter, artifactIdentity, registryCapabilities);
|
||||
|
||||
@@ -28,6 +28,37 @@ const requiredForbiddenItems = [
|
||||
"unidesk-runtime-substitute",
|
||||
"force-push"
|
||||
];
|
||||
const forbiddenDeployArtifactFields = new Set([
|
||||
"commitId",
|
||||
"image",
|
||||
"imageTag",
|
||||
"digest",
|
||||
"sourceCommitId",
|
||||
"repositoryDigest",
|
||||
"buildCreatedAt",
|
||||
"buildSource",
|
||||
"componentCommitId",
|
||||
"componentInputHash",
|
||||
"dockerfileHash",
|
||||
"baseImageReference",
|
||||
"baseImageDigest",
|
||||
"buildArgsHash",
|
||||
"ciAffected",
|
||||
"ciReason",
|
||||
"reuse",
|
||||
"reusedFrom",
|
||||
"publishState"
|
||||
]);
|
||||
const forbiddenDeployArtifactEnv = new Set([
|
||||
"HWLAB_COMMIT_ID",
|
||||
"HWLAB_IMAGE",
|
||||
"HWLAB_IMAGE_TAG",
|
||||
"HWLAB_IMAGE_DIGEST",
|
||||
"HWLAB_REVISION",
|
||||
"HWLAB_BUILD_CREATED_AT",
|
||||
"HWLAB_BUILD_SOURCE",
|
||||
"HWLAB_SKILLS_COMMIT_ID"
|
||||
]);
|
||||
|
||||
async function readJSON(relativePath) {
|
||||
const raw = await readFile(path.join(repoRoot, relativePath), "utf8");
|
||||
@@ -124,7 +155,7 @@ function assertServiceInventory(catalog) {
|
||||
function assertDeployManifest(deployManifest, catalog) {
|
||||
assert.equal(deployManifest.manifestVersion, "v1", "deploy manifestVersion");
|
||||
assert.equal(deployManifest.environment, ENVIRONMENT_DEV, "deploy environment");
|
||||
assert.equal(deployManifest.commitId, catalog.commitId, "deploy commitId must match catalog");
|
||||
assert.equal(Object.hasOwn(deployManifest, "commitId"), false, "deploy commitId is generated artifact identity and must stay in artifact catalog");
|
||||
assert.equal(deployManifest.namespace, catalog.namespace, "deploy namespace must match catalog");
|
||||
assert.equal(deployManifest.endpoint, catalog.endpoint, "deploy endpoint must match catalog");
|
||||
assert.equal(deployManifest.profiles.dev.enabled, true, "deploy dev profile must be enabled");
|
||||
@@ -132,6 +163,17 @@ function assertDeployManifest(deployManifest, catalog) {
|
||||
assert.equal(deployManifest.profiles.dev.endpoint, catalog.endpoint, "deploy dev endpoint must match catalog");
|
||||
assert.ok(deployManifest.profiles.prod, "deploy may keep disabled prod placeholder");
|
||||
assert.equal(deployManifest.profiles.prod.enabled, false, "deploy prod profile must stay disabled");
|
||||
for (const field of Object.keys(deployManifest)) {
|
||||
assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy ${field} is generated artifact identity and must stay in artifact catalog`);
|
||||
}
|
||||
for (const service of deployManifest.services ?? []) {
|
||||
for (const field of Object.keys(service)) {
|
||||
assert.equal(forbiddenDeployArtifactFields.has(field), false, `deploy service ${service.serviceId}.${field} is generated artifact identity and must stay in artifact catalog`);
|
||||
}
|
||||
for (const envName of Object.keys(service.env ?? {})) {
|
||||
assert.equal(forbiddenDeployArtifactEnv.has(envName), false, `deploy service ${service.serviceId}.env.${envName} is generated artifact identity and must be rendered from artifact catalog`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertCatalogServices(catalog, deployManifest, catalogMode) {
|
||||
@@ -161,7 +203,6 @@ function assertCatalogServices(catalog, deployManifest, catalogMode) {
|
||||
const image = imageParts(service.image, `${context} image`);
|
||||
assert.equal(image.repositoryName, service.serviceId, `${context} image repository must end with serviceId`);
|
||||
assert.equal(image.tag, service.imageTag, `${context} image tag`);
|
||||
assert.equal(service.image, deployService.image, `${context} image must match deploy manifest`);
|
||||
|
||||
assert.equal(service.profile, ENVIRONMENT_DEV, `${context} profile`);
|
||||
assert.equal(service.profile, deployService.profile, `${context} profile must match deploy manifest`);
|
||||
|
||||
@@ -60,7 +60,7 @@ async function parseDeployManifest(relativePath) {
|
||||
const doc = JSON.parse(raw);
|
||||
assert.equal(doc.manifestVersion, "v1", `${relativePath} manifestVersion`);
|
||||
assert.equal(doc.environment, ENVIRONMENT_DEV, `${relativePath} environment`);
|
||||
assert.ok(doc.commitId, `${relativePath} commitId`);
|
||||
assert.equal(Object.hasOwn(doc, "commitId"), false, `${relativePath} commitId must stay in artifact catalog`);
|
||||
assert.ok(Array.isArray(doc.services) && doc.services.length >= 1, `${relativePath} services`);
|
||||
return doc;
|
||||
}
|
||||
@@ -77,16 +77,6 @@ function assertUnique(name, values) {
|
||||
assert.equal(new Set(values).size, values.length, `${name} must be unique`);
|
||||
}
|
||||
|
||||
function imageTagFromReference(image) {
|
||||
if (typeof image !== "string" || image.length === 0) return null;
|
||||
const digestIndex = image.indexOf("@");
|
||||
const withoutDigest = digestIndex >= 0 ? image.slice(0, digestIndex) : image;
|
||||
const slashIndex = withoutDigest.lastIndexOf("/");
|
||||
const colonIndex = withoutDigest.lastIndexOf(":");
|
||||
if (colonIndex <= slashIndex) return null;
|
||||
return withoutDigest.slice(colonIndex + 1) || null;
|
||||
}
|
||||
|
||||
function assertCommonSchema(commonSchema) {
|
||||
const schemaServiceIds = commonSchema.$defs.serviceId.enum;
|
||||
assert.deepEqual(schemaServiceIds, SERVICE_IDS, "common service ids must match runtime constants");
|
||||
@@ -101,9 +91,22 @@ function assertDeploySchema(deploySchema) {
|
||||
assert.ok(deploySchema.required.includes("publicEndpoints"), "deploy schema requires public endpoint source");
|
||||
assert.ok(deploySchema.required.includes("frp"), "deploy schema requires frp source");
|
||||
assert.ok(deploySchema.required.includes("k3s"), "deploy schema requires k3s source");
|
||||
assert.equal(deploySchema.required.includes("commitId"), false, "deploy schema must not require generated commitId");
|
||||
assert.equal(deploySchema.$defs.service.required.includes("image"), false, "deploy service schema must not require generated image");
|
||||
assert.equal(deploySchema.$defs.health.properties.path.const, "/health/live", "deploy health path source");
|
||||
}
|
||||
|
||||
function assertNoDeployArtifactIdentity(service, serviceId) {
|
||||
const forbiddenServiceFields = ["image", "imageTag", "digest", "commitId", "sourceCommitId"];
|
||||
const forbiddenEnvFields = ["HWLAB_COMMIT_ID", "HWLAB_IMAGE", "HWLAB_IMAGE_TAG", "HWLAB_IMAGE_DIGEST", "HWLAB_REVISION", "HWLAB_BUILD_CREATED_AT", "HWLAB_BUILD_SOURCE", "HWLAB_SKILLS_COMMIT_ID"];
|
||||
for (const field of forbiddenServiceFields) {
|
||||
assert.equal(Object.hasOwn(service, field), false, `${serviceId}.${field} must stay in artifact catalog or rendered runtime manifests`);
|
||||
}
|
||||
for (const field of forbiddenEnvFields) {
|
||||
assert.equal(Object.hasOwn(service.env ?? {}, field), false, `${serviceId}.env.${field} must be rendered from artifact catalog`);
|
||||
}
|
||||
}
|
||||
|
||||
function assertEnvelopeValidation() {
|
||||
validateRequest({
|
||||
jsonrpc: "2.0",
|
||||
@@ -187,10 +190,7 @@ assert.ok(
|
||||
"deploy edge proxy k3s mapping"
|
||||
);
|
||||
assert.equal(cloudApi.healthPath, "/health/live", "cloud-api health path");
|
||||
const cloudApiImageTag = imageTagFromReference(cloudApi.image) ?? deployManifest.commitId.slice(0, 7);
|
||||
assert.equal(cloudApi.env.HWLAB_COMMIT_ID, cloudApiImageTag, "cloud-api health commit evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_IMAGE, cloudApi.image, "cloud-api health image evidence");
|
||||
assert.equal(cloudApi.env.HWLAB_IMAGE_TAG, cloudApiImageTag, "cloud-api health image tag evidence");
|
||||
for (const service of deployManifest.services) assertNoDeployArtifactIdentity(service, service.serviceId);
|
||||
assert.equal(cloudApi.env.HWLAB_CLOUD_DB_URL, "secretRef:hwlab-cloud-api-dev-db/database-url", "cloud-api DB URL must be a Secret reference placeholder");
|
||||
assert.equal(
|
||||
cloudApi.env.HWLAB_CLOUD_DB_SSL_MODE,
|
||||
|
||||
Reference in New Issue
Block a user