test: record issue 9 user service artifact gaps
This commit is contained in:
@@ -14,6 +14,7 @@ This matrix closes the current review pass for the `decision-center`, `mdtodo`,
|
||||
| Project-manager registry probe | `deploy.json` wants `0c3cdb4ee06a23361ed511a2da033d67b53d16f4`; `config.json` still records runtime commit `a278de032d5cdb91010466ac1e2183c79026550d`; remote registry HEAD for `127.0.0.1:5000/unidesk/project-manager:0c3cdb4ee06a23361ed511a2da033d67b53d16f4` returned 404, so no digest is available yet. | The producer and consumer contract is wired, but the desired artifact is still missing. | Publish the desired artifact before any live dev or prod apply. |
|
||||
| Dev service catalog | `decision-center-dev` exists in k3s, while `mdtodo-dev` and `claudeqq-dev` do not. The adapter public catalog did not expose the dev aliases during this pass. | Dev promotion cannot be claimed for `mdtodo` or `claudeqq`; `decision-center-dev` is reachable through direct k3s service proxy only. | Publish artifacts first, then create/verify dev consumers and register dev aliases where intended. |
|
||||
| Frontend closure contract | `bun scripts/frontend-artifact-lane-contract-test.ts` is the standard lightweight contract for the frontend artifact sample. | Frontend lane evidence is now repeatable without live deploy, publish, service restart, or Playwright. | Keep the contract green whenever frontend deploy.json, health metadata, artifact digest, producer preflight, or CD dry-run shape changes. |
|
||||
| User-service gap contract | `bun scripts/issue-9-user-service-artifact-gap-contract-test.ts` normalizes `mdtodo`, `claudeqq` and `todo-note` with `desiredCommit`, `runtimeCommit`, `artifactExists`, `devStatus`, `prodStatus`, `blockedScopes` and `recommendedAction`. | The remaining gap surface is repeatable from `deploy.json`, `CI.json` and local artifact-registry dry-runs without publish, deploy, service restart, or full check/e2e. | Keep the contract green when desired commits, producer metadata, consumer targets or next-hop classifications change. |
|
||||
|
||||
## Service Matrix
|
||||
|
||||
@@ -26,6 +27,26 @@ This matrix closes the current review pass for the `decision-center`, `mdtodo`,
|
||||
| `project-manager` | `127.0.0.1:5000/unidesk/project-manager:0c3cdb4ee06a23361ed511a2da033d67b53d16f4`; registry HEAD returned 404, so no digest was available. Current runtime registry commit in `config.json` is `a278de032d5cdb91010466ac1e2183c79026550d`. | Main-server Compose artifact consumer. CI producer is UniDesk source-build from `src/components/microservices/project-manager/Dockerfile`. | `deploy plan --env dev --service project-manager` resolves the same no-build main-server Compose path; no live dev apply was attempted because the desired artifact is absent. | `deploy plan --env prod --service project-manager --dry-run` resolves the same main-server Compose consumer and health contract, but live prod apply remains blocked until the artifact exists and `/health` can report `deploy.commit` / `deploy.requestedCommit`. | Partial. The source and consumer contract are in place; the registry artifact is not. | Publish `0c3cdb4ee06a23361ed511a2da033d67b53d16f4` to the D601 registry, then run dev and prod artifact-consumer verification. |
|
||||
| `frontend` | `127.0.0.1:5000/unidesk/frontend:b5486a61ab0aa6c227366a95d1afa68281584359`, registry v2 manifest digest `sha256:76d7c47e797605470959ca2274f116149bdc367e6fa155913d19f42516e5b9e4`. | CI producer is `ci publish-user-service` from `src/components/frontend/Dockerfile`. Dev CD is D601 native k3s `frontend-dev`; prod CD is master-server Compose `frontend` / `unidesk-frontend`; both are pull-only artifact consumers. | Public dev `http://74.48.78.17:18083/health` reports `ok=true`, `environment=dev`, `namespace=unidesk-dev`, and deploy commit/requestedCommit `b5486a61ab0aa6c227366a95d1afa68281584359`. | Public prod `http://74.48.78.17:18081/health` reports `ok=true` and deploy commit/requestedCommit `b5486a61ab0aa6c227366a95d1afa68281584359`. Remote `ci publish-user-service --service frontend --commit b5486... --dry-run` reports `runnerDisposition=ready`, all control channels ready, and no missing control channels. | Complete for CI/CD contract. Dev/prod desired and live commit match the artifact; CD dry-runs are non-mutating and no-build. | Remaining UI route acceptance is a manual product/UI gate and is independent of CI/CD artifact correctness. |
|
||||
|
||||
## Normalized Gap Matrix
|
||||
|
||||
Focused read-only evidence for this refresh:
|
||||
|
||||
- remote `microservice status` and `microservice health` for `mdtodo`, `claudeqq` and `todo-note`;
|
||||
- D601 registry v2 manifest `HEAD` against each desired tag, all returning 404;
|
||||
- `deploy plan --env dev|prod --service <id>` and `artifact-registry deploy-service --env dev|prod --service <id> --commit <desiredCommit> --dry-run`, all resolving no-build artifact consumers.
|
||||
|
||||
| Service | desiredCommit | runtimeCommit | artifactExists | devStatus | prodStatus | blockedScopes | recommendedAction |
|
||||
| --- | --- | --- | --- | --- | --- | --- | --- |
|
||||
| `mdtodo` | `75fb6757b2504ba86d61f2587fb34a9c9ed4019a` | `75fb6757b2504ba86d61f2587fb34a9c9ed4019a` from prod Deployment annotations; `/health` is ok but has no deploy metadata | `false` | `missing-dev-service` | `healthy-prod-annotation-aligned` | `registry-artifact`, `dev-service`, `health-deploy-metadata` | Publish the desired artifact, create/verify `unidesk-dev/mdtodo-dev`, then run focused dev smoke before deciding whether prod needs replacement. |
|
||||
| `claudeqq` | `203b1f46684c91340ecbbd8a74502bd55e4f2011` | `203b1f46684c91340ecbbd8a74502bd55e4f2011` from prod `/health.deploy.commit` and `/health.deploy.requestedCommit` | `false` | `missing-dev-service` | `healthy-prod-health-aligned-event-api-unverified` | `registry-artifact`, `dev-service`, `event-api-surface` | Publish the desired artifact, create/verify `unidesk-dev/claudeqq-dev`, then resolve or document the event API paths before prod artifact replacement. |
|
||||
| `todo-note` | `a14ce0eb855a685fa17b47adacd54623e72cd2ff` | `null`; prod health and container labels do not expose source commit | `false` | `consumer-plan-only-no-live-dev` | `healthy-behavior-no-commit-proof` | `registry-artifact`, `runtime-commit-proof`, `health-deploy-metadata` | Publish the desired artifact, then use the no-build Compose artifact consumer to recreate only `todo-note` and verify image labels plus health deploy metadata. |
|
||||
|
||||
Repeatable contract:
|
||||
|
||||
```bash
|
||||
bun scripts/issue-9-user-service-artifact-gap-contract-test.ts
|
||||
```
|
||||
|
||||
## Execution Decision
|
||||
|
||||
No live deployment or publish was executed in this pass.
|
||||
|
||||
@@ -260,10 +260,10 @@ This matrix describes the next promotion stage after dry-run coverage is in plac
|
||||
| `baidu-netdisk` | `master` | source-build supported | dev + prod artifact consumer | pull-only dev validation is diagnosable through `runtimeSecrets.secretSource`, `requiredSecretsPresent`, `missingSecretKeys` and `recommendedAction`; live dev apply waits for the secret source condition | prod artifact and health are aligned; further prod action is only focused post-apply proxy/auth verification after operator approval | canonical Compose env secret source and `/health.auth` gate; no manual secret operation in worker tasks | `GPT-5.5` |
|
||||
| `project-manager` | `master` | source-build supported | dev + prod artifact consumer | dev artifact validation with `/api/projects` | prod artifact validation with live commit proof | none beyond standard artifact/CD checks | `MiniMax` for dry-run/reporting, `GPT-5.5` for release sign-off |
|
||||
| `oa-event-flow` | `master` | source-build supported | dev + prod artifact consumer | dev artifact validation with `/api/diagnostics` | prod artifact validation with live commit proof | none beyond standard artifact/CD checks | `MiniMax` for dry-run/reporting, `GPT-5.5` for release sign-off |
|
||||
| `todo-note` | `master` | external source-build supported | dev + prod artifact consumer | dev recreate with PostgreSQL-backed deploy metadata | prod recreate with matching `deploy.commit` and `deploy.requestedCommit` | external repo fetch and runtime metadata consistency | `DeepSeek` for digesting external-source evidence, `GPT-5.5` for final gate |
|
||||
| `todo-note` | `master` | external source-build supported | dev + prod Compose artifact consumer | consumer dry-run is ready; live dev remains blocked until the desired artifact exists | prod behavior is healthy, but runtime commit proof is absent until the no-build recreate lands image labels and health deploy metadata | registry artifact, runtime commit proof and health deploy metadata | `DeepSeek` for digesting external-source evidence, `GPT-5.5` for final gate |
|
||||
| `decision-center` | `master` | source-build supported | dev + prod k3s consumer closed when desired/live/artifact commit match and dry-run stays no-build | dev artifact CD closed; remaining dev acceptance is focused record CRUD, diary lifecycle, doc-number uniqueness and frontend visibility | prod artifact CD closed; remaining prod acceptance is manual UI/product verification after health/live commit proof | doc-management completeness, PostgreSQL truth and UI acceptance; no deployment drift when desired/live/artifact are aligned | `GPT-5.5` |
|
||||
| `mdtodo` | `master` | source-build supported | dev + prod k3s consumer | dev rollout with deployment metadata and `/health` or `/live` proof | prod rollout with service proxy verification and live commit proof | no NodePort/hostPort/public backend exposure | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `claudeqq` | `master` | source-build supported | dev + prod k3s consumer | dev rollout with Deployment metadata and health via Kubernetes API proxy | prod rollout with same commit-pinned artifact contract | NapCat/backend port exposure must stay private | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `mdtodo` | `master` | source-build supported | dev + prod k3s consumer | dev service is absent until the desired artifact is published and `unidesk-dev/mdtodo-dev` is created/verified | prod is healthy with desired Deployment annotations, but artifact and health deploy metadata are still missing | registry artifact, dev service and health deploy metadata; no NodePort/hostPort/public backend exposure | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `claudeqq` | `master` | source-build supported | dev + prod k3s consumer | dev service is absent until the desired artifact is published and `unidesk-dev/claudeqq-dev` is created/verified | prod health reports the desired commit and NapCat login, but artifact and event API proof remain open | registry artifact, dev service, event API surface; NapCat/backend port exposure must stay private | `MiniMax` for prompt prep, `GPT-5.5` for approval |
|
||||
| `findjob` | `master` | source-build supported | dev + prod direct Compose consumer | pull-only dev validation on D601 with image labels and `/api/health` | pull-only prod recreate with live commit proof | target-side compose health/labels only, no public business ports | `DeepSeek` for dry-run matrix drafting, `GPT-5.5` for final gate |
|
||||
| `pipeline` | `master` | source-build supported | dev + prod direct Compose consumer | pull-only dev validation on D601 with image labels and `/health` | pull-only prod recreate with live commit proof | runtime contract is commit-label + compose service identity | `DeepSeek` for dry-run matrix drafting, `GPT-5.5` for final gate |
|
||||
| `met-nonlinear` | `master` | source-build supported | dev dry-run only | runtime-verification-blocked until long-running TS service image contract is fixed | not authorized | image contract mismatch between ML Dockerfile and TS runtime service | `GPT-5.5` |
|
||||
@@ -278,6 +278,14 @@ bun scripts/frontend-artifact-lane-contract-test.ts
|
||||
|
||||
The contract fixes the current sample around one artifact lane: `deploy.json` dev/prod both request the same frontend commit, `CI.json` keeps the producer as `ci publish-user-service`, the D601 registry manifest digest is recorded, dev and prod health expose matching `deploy.commit` / `deploy.requestedCommit`, `ci publish-user-service --dry-run` is ready, and both CD dry-runs are artifact consumers with no source build.
|
||||
|
||||
User-service artifact gap reviews must report the same normalized fields for each service: `desiredCommit`, `runtimeCommit`, `artifactExists`, `devStatus`, `prodStatus`, `blockedScopes` and `recommendedAction`. The issue #9 gap contract is intentionally lightweight and non-mutating:
|
||||
|
||||
```bash
|
||||
bun scripts/issue-9-user-service-artifact-gap-contract-test.ts
|
||||
```
|
||||
|
||||
The contract pins the current `mdtodo`, `claudeqq` and `todo-note` gap surface: `deploy.json` dev/prod desired commits, `CI.json` producer metadata, structured status fields and dev/prod artifact-consumer dry-runs. It does not publish artifacts, apply manifests, recreate services, restart services, run full check/e2e, or probe browser UI.
|
||||
|
||||
Planned parallelism for the next wave should be three lanes:
|
||||
|
||||
1. Lane A: `frontend`, `baidu-netdisk`, `project-manager`, `oa-event-flow`.
|
||||
|
||||
@@ -0,0 +1,256 @@
|
||||
import { readFileSync } from "node:fs";
|
||||
import { rootPath } from "./src/config";
|
||||
import { runArtifactRegistryCommand } from "./src/artifact-registry";
|
||||
|
||||
type Environment = "dev" | "prod";
|
||||
type ServiceId = "mdtodo" | "claudeqq" | "todo-note";
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
type ServiceContract = {
|
||||
serviceId: ServiceId;
|
||||
desiredCommit: string;
|
||||
runtimeCommit: string | null;
|
||||
runtimeCommitSource: string;
|
||||
artifactExists: boolean;
|
||||
devStatus: string;
|
||||
prodStatus: string;
|
||||
blockedScopes: string[];
|
||||
recommendedAction: string;
|
||||
sourceRepo: string;
|
||||
dockerfile: string;
|
||||
registryRepository: string;
|
||||
consumerKind: "d601-k3s" | "compose";
|
||||
};
|
||||
|
||||
const contracts: ServiceContract[] = [
|
||||
{
|
||||
serviceId: "mdtodo",
|
||||
desiredCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a",
|
||||
runtimeCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a",
|
||||
runtimeCommitSource: "prod Deployment annotations; /health is ok but does not expose deploy metadata",
|
||||
artifactExists: false,
|
||||
devStatus: "missing-dev-service",
|
||||
prodStatus: "healthy-prod-annotation-aligned",
|
||||
blockedScopes: ["registry-artifact", "dev-service", "health-deploy-metadata"],
|
||||
recommendedAction: "Publish the desired artifact, create/verify unidesk-dev/mdtodo-dev, then run focused dev smoke before deciding whether prod needs replacement.",
|
||||
sourceRepo: "https://github.com/pikasTech/unidesk",
|
||||
dockerfile: "src/components/microservices/mdtodo/Dockerfile",
|
||||
registryRepository: "unidesk/mdtodo",
|
||||
consumerKind: "d601-k3s",
|
||||
},
|
||||
{
|
||||
serviceId: "claudeqq",
|
||||
desiredCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011",
|
||||
runtimeCommit: "203b1f46684c91340ecbbd8a74502bd55e4f2011",
|
||||
runtimeCommitSource: "prod /health deploy.commit and deploy.requestedCommit",
|
||||
artifactExists: false,
|
||||
devStatus: "missing-dev-service",
|
||||
prodStatus: "healthy-prod-health-aligned-event-api-unverified",
|
||||
blockedScopes: ["registry-artifact", "dev-service", "event-api-surface"],
|
||||
recommendedAction: "Publish the desired artifact, create/verify unidesk-dev/claudeqq-dev, then resolve or document the event API paths before prod artifact replacement.",
|
||||
sourceRepo: "https://gitee.com/lyon1998/agent_skills",
|
||||
dockerfile: "claudeqq/Dockerfile",
|
||||
registryRepository: "unidesk/claudeqq",
|
||||
consumerKind: "d601-k3s",
|
||||
},
|
||||
{
|
||||
serviceId: "todo-note",
|
||||
desiredCommit: "a14ce0eb855a685fa17b47adacd54623e72cd2ff",
|
||||
runtimeCommit: null,
|
||||
runtimeCommitSource: "prod health and container labels do not expose source commit",
|
||||
artifactExists: false,
|
||||
devStatus: "consumer-plan-only-no-live-dev",
|
||||
prodStatus: "healthy-behavior-no-commit-proof",
|
||||
blockedScopes: ["registry-artifact", "runtime-commit-proof", "health-deploy-metadata"],
|
||||
recommendedAction: "Publish the desired artifact, then use the no-build Compose artifact consumer to recreate only todo-note and verify image labels plus health deploy metadata.",
|
||||
sourceRepo: "https://gitee.com/Lyon1998/todo_note",
|
||||
dockerfile: "Dockerfile",
|
||||
registryRepository: "unidesk/todo-note",
|
||||
consumerKind: "compose",
|
||||
},
|
||||
];
|
||||
|
||||
function assertCondition(condition: unknown, message: string, detail: unknown = {}): void {
|
||||
if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`);
|
||||
}
|
||||
|
||||
function asRecord(value: unknown, label: string): JsonRecord {
|
||||
assertCondition(typeof value === "object" && value !== null && !Array.isArray(value), `${label} must be an object`, value);
|
||||
return value as JsonRecord;
|
||||
}
|
||||
|
||||
function asArray(value: unknown, label: string): unknown[] {
|
||||
assertCondition(Array.isArray(value), `${label} must be an array`, value);
|
||||
return value as unknown[];
|
||||
}
|
||||
|
||||
function strings(value: unknown, label: string): string[] {
|
||||
return asArray(value, label).map(String);
|
||||
}
|
||||
|
||||
function manifestService(manifest: JsonRecord, environment: Environment, serviceId: ServiceId): JsonRecord {
|
||||
const environments = asRecord(manifest.environments, "deploy.json.environments");
|
||||
const env = asRecord(environments[environment], `deploy.json.environments.${environment}`);
|
||||
const services = asArray(env.services, `deploy.json.environments.${environment}.services`);
|
||||
const service = services.map((item, index) => asRecord(item, `${environment}.services[${index}]`)).find((item) => item.id === serviceId);
|
||||
assertCondition(service !== undefined, `deploy.json ${environment} must include ${serviceId}`, env);
|
||||
return service as JsonRecord;
|
||||
}
|
||||
|
||||
function artifactCatalogEntry(catalog: JsonRecord, serviceId: ServiceId): JsonRecord {
|
||||
const artifacts = asArray(catalog.artifacts, "CI.json.artifacts").map((item, index) => asRecord(item, `CI.json.artifacts[${index}]`));
|
||||
const artifact = artifacts.find((item) => item.serviceId === serviceId);
|
||||
assertCondition(artifact !== undefined, `CI.json must include ${serviceId}`, catalog);
|
||||
return artifact as JsonRecord;
|
||||
}
|
||||
|
||||
function assertDeployJson(): void {
|
||||
const manifest = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as unknown, "deploy.json");
|
||||
assertCondition(manifest.schemaVersion === 2, "deploy.json must use schemaVersion=2", manifest);
|
||||
for (const contract of contracts) {
|
||||
for (const environment of ["dev", "prod"] as const) {
|
||||
const service = manifestService(manifest, environment, contract.serviceId);
|
||||
assertCondition(service.repo === contract.sourceRepo, `${contract.serviceId} ${environment} repo mismatch`, service);
|
||||
assertCondition(service.commitId === contract.desiredCommit, `${contract.serviceId} ${environment} desired commit mismatch`, service);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function assertCiCatalog(): void {
|
||||
const catalog = asRecord(JSON.parse(readFileSync(rootPath("CI.json"), "utf8")) as unknown, "CI.json");
|
||||
for (const contract of contracts) {
|
||||
const artifact = artifactCatalogEntry(catalog, contract.serviceId);
|
||||
const source = asRecord(artifact.source, `${contract.serviceId} CI source`);
|
||||
const image = asRecord(artifact.image, `${contract.serviceId} CI image`);
|
||||
assertCondition(artifact.kind === "source-build", `${contract.serviceId} producer must be source-build`, artifact);
|
||||
assertCondition(artifact.status === "supported", `${contract.serviceId} producer must be supported`, artifact);
|
||||
assertCondition(artifact.producer === "ci publish-user-service", `${contract.serviceId} producer command mismatch`, artifact);
|
||||
assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} source repo mismatch`, source);
|
||||
assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} Dockerfile mismatch`, source);
|
||||
assertCondition(image.repository === contract.registryRepository, `${contract.serviceId} registry repository mismatch`, image);
|
||||
}
|
||||
}
|
||||
|
||||
function assertNormalizedStatus(): void {
|
||||
for (const contract of contracts) {
|
||||
assertCondition(/^[0-9a-f]{40}$/u.test(contract.desiredCommit), `${contract.serviceId} desiredCommit must be a full sha`, contract);
|
||||
assertCondition(contract.runtimeCommit === null || /^[0-9a-f]{40}$/u.test(contract.runtimeCommit), `${contract.serviceId} runtimeCommit must be a full sha or null`, contract);
|
||||
assertCondition(contract.artifactExists === false, `${contract.serviceId} desired artifact should remain recorded as missing`, contract);
|
||||
assertCondition(contract.devStatus.length > 0, `${contract.serviceId} devStatus must be structured`, contract);
|
||||
assertCondition(contract.prodStatus.length > 0, `${contract.serviceId} prodStatus must be structured`, contract);
|
||||
assertCondition(contract.blockedScopes.length > 0, `${contract.serviceId} blockedScopes must not be empty`, contract);
|
||||
assertCondition(contract.recommendedAction.length > 0, `${contract.serviceId} recommendedAction must not be empty`, contract);
|
||||
}
|
||||
}
|
||||
|
||||
function assertCommonDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
|
||||
const source = asRecord(plan.source, `${contract.serviceId} ${environment} source`);
|
||||
const registry = asRecord(plan.registry, `${contract.serviceId} ${environment} registry`);
|
||||
const build = asRecord(plan.build, `${contract.serviceId} ${environment} build`);
|
||||
const labels = asRecord(plan.requiredLabels, `${contract.serviceId} ${environment} labels`);
|
||||
const registryProbe = asRecord(plan.registryProbe, `${contract.serviceId} ${environment} registryProbe`);
|
||||
|
||||
assertCondition(plan.ok === true && plan.supported === true, `${contract.serviceId} ${environment} dry-run must be supported`, plan);
|
||||
assertCondition(plan.dryRun === true && plan.mutation === false, `${contract.serviceId} ${environment} dry-run must be non-mutating`, plan);
|
||||
assertCondition(plan.environment === environment, `${contract.serviceId} ${environment} environment mismatch`, plan);
|
||||
assertCondition(plan.providerId === "D601", `${contract.serviceId} ${environment} provider mismatch`, plan);
|
||||
assertCondition(plan.serviceId === contract.serviceId, `${contract.serviceId} ${environment} service id mismatch`, plan);
|
||||
assertCondition(plan.commit === contract.desiredCommit, `${contract.serviceId} ${environment} commit mismatch`, plan);
|
||||
assertCondition(plan.deployRef === `deploy.json#environments.${environment}.services.${contract.serviceId}`, `${contract.serviceId} ${environment} deployRef mismatch`, plan);
|
||||
assertCondition(plan.sourceImage === `127.0.0.1:5000/${contract.registryRepository}:${contract.desiredCommit}`, `${contract.serviceId} ${environment} source image mismatch`, plan);
|
||||
assertCondition(source.repo === contract.sourceRepo, `${contract.serviceId} ${environment} source repo mismatch`, source);
|
||||
assertCondition(source.commit === contract.desiredCommit, `${contract.serviceId} ${environment} source commit mismatch`, source);
|
||||
assertCondition(source.dockerfile === contract.dockerfile, `${contract.serviceId} ${environment} source dockerfile mismatch`, source);
|
||||
assertCondition(registry.repository === contract.registryRepository, `${contract.serviceId} ${environment} registry repository mismatch`, registry);
|
||||
assertCondition(registry.imageRef === plan.sourceImage, `${contract.serviceId} ${environment} registry image mismatch`, registry);
|
||||
assertCondition(registry.digest === null, `${contract.serviceId} ${environment} dry-run must not fake a digest`, registry);
|
||||
assertCondition(String(registry.digestSource ?? "").includes("live apply must read this digest"), `${contract.serviceId} ${environment} digest source mismatch`, registry);
|
||||
assertCondition(registryProbe.method === "HEAD", `${contract.serviceId} ${environment} registry probe must be HEAD`, registryProbe);
|
||||
assertCondition(labels["unidesk.ai/service-id"] === contract.serviceId, `${contract.serviceId} ${environment} service label mismatch`, labels);
|
||||
assertCondition(labels["unidesk.ai/source-commit"] === contract.desiredCommit, `${contract.serviceId} ${environment} commit label mismatch`, labels);
|
||||
assertCondition(labels["unidesk.ai/dockerfile"] === contract.dockerfile, `${contract.serviceId} ${environment} Dockerfile label mismatch`, labels);
|
||||
assertCondition(build.willCompile === false, `${contract.serviceId} ${environment} CD must not compile`, build);
|
||||
assertCondition(build.willRunCargoBuild === false, `${contract.serviceId} ${environment} CD must not run cargo build`, build);
|
||||
assertCondition(build.willRunDockerBuild === false, `${contract.serviceId} ${environment} CD must not run docker build`, build);
|
||||
assertCondition(build.willRunDockerComposeBuild === false, `${contract.serviceId} ${environment} CD must not run docker compose build`, build);
|
||||
assertCondition(build.producerBoundary === "ci publish-user-service", `${contract.serviceId} ${environment} producer boundary mismatch`, build);
|
||||
assertCondition(String(plan.boundary ?? "").includes("artifact-consumer only"), `${contract.serviceId} ${environment} boundary must be artifact-only`, plan);
|
||||
assertCondition(String(plan.boundary ?? "").includes("never builds source"), `${contract.serviceId} ${environment} boundary must forbid source builds`, plan);
|
||||
}
|
||||
|
||||
function assertK3sDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
|
||||
const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`);
|
||||
const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`);
|
||||
const suffix = environment === "dev" ? "-dev" : "";
|
||||
assertCondition(target.kind === "d601-k3s", `${contract.serviceId} ${environment} target kind mismatch`, target);
|
||||
assertCondition(target.namespace === (environment === "dev" ? "unidesk-dev" : "unidesk"), `${contract.serviceId} ${environment} namespace mismatch`, target);
|
||||
assertCondition(target.deployment === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} deployment mismatch`, target);
|
||||
assertCondition(target.service === `${contract.serviceId}${suffix}`, `${contract.serviceId} ${environment} service mismatch`, target);
|
||||
assertCondition(String(target.deployCommandShape ?? "").includes("kubectl set image"), `${contract.serviceId} ${environment} command must be k3s image update`, target);
|
||||
assertCondition(validation.some((line) => line.includes("Kubernetes API service proxy") || line.includes("service health")), `${contract.serviceId} ${environment} validation should include k3s service health`, validation);
|
||||
}
|
||||
|
||||
function assertComposeDryRun(plan: JsonRecord, contract: ServiceContract, environment: Environment): void {
|
||||
const target = asRecord(plan.target, `${contract.serviceId} ${environment} target`);
|
||||
const validation = strings(plan.validation, `${contract.serviceId} ${environment} validation`);
|
||||
assertCondition(target.kind === "compose", `${contract.serviceId} ${environment} target kind mismatch`, target);
|
||||
assertCondition(target.runtimeHost === "main-server", `${contract.serviceId} ${environment} runtime host mismatch`, target);
|
||||
assertCondition(target.composeService === "todo-note", `${contract.serviceId} ${environment} compose service mismatch`, target);
|
||||
assertCondition(target.containerName === "todo-note-backend", `${contract.serviceId} ${environment} container mismatch`, target);
|
||||
assertCondition(target.deployCommandShape === "docker compose up -d --no-build --no-deps --force-recreate todo-note", `${contract.serviceId} ${environment} command shape mismatch`, target);
|
||||
assertCondition(validation.some((line) => line.includes("deploy.commit/deploy.requestedCommit")), `${contract.serviceId} ${environment} validation must require health deploy metadata`, validation);
|
||||
}
|
||||
|
||||
async function assertDryRuns(): Promise<void> {
|
||||
for (const contract of contracts) {
|
||||
for (const environment of ["dev", "prod"] as const) {
|
||||
const plan = asRecord(await runArtifactRegistryCommand([
|
||||
"deploy-service",
|
||||
"--env",
|
||||
environment,
|
||||
"--service",
|
||||
contract.serviceId,
|
||||
"--commit",
|
||||
contract.desiredCommit,
|
||||
"--dry-run",
|
||||
]), `${contract.serviceId} ${environment} artifact dry-run`);
|
||||
assertCommonDryRun(plan, contract, environment);
|
||||
if (contract.consumerKind === "d601-k3s") assertK3sDryRun(plan, contract, environment);
|
||||
else assertComposeDryRun(plan, contract, environment);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function main(): Promise<void> {
|
||||
assertDeployJson();
|
||||
assertCiCatalog();
|
||||
assertNormalizedStatus();
|
||||
await assertDryRuns();
|
||||
|
||||
process.stdout.write(`${JSON.stringify({
|
||||
ok: true,
|
||||
checks: [
|
||||
"deploy.json dev/prod desired commits match the issue #9 service matrix",
|
||||
"CI.json keeps mdtodo, claudeqq and todo-note on supported ci publish-user-service source-build producers",
|
||||
"normalized status records expose desiredCommit, runtimeCommit, artifactExists, devStatus, prodStatus, blockedScopes and recommendedAction",
|
||||
"dev/prod artifact-registry dry-runs are non-mutating, commit-pinned and no-build",
|
||||
"mdtodo and claudeqq dry-runs target D601 k3s consumers",
|
||||
"todo-note dry-runs target the main-server Compose consumer with no-build/no-deps recreate shape",
|
||||
],
|
||||
services: contracts.map((contract) => ({
|
||||
serviceId: contract.serviceId,
|
||||
desiredCommit: contract.desiredCommit,
|
||||
runtimeCommit: contract.runtimeCommit,
|
||||
runtimeCommitSource: contract.runtimeCommitSource,
|
||||
artifactExists: contract.artifactExists,
|
||||
devStatus: contract.devStatus,
|
||||
prodStatus: contract.prodStatus,
|
||||
blockedScopes: contract.blockedScopes,
|
||||
recommendedAction: contract.recommendedAction,
|
||||
})),
|
||||
}, null, 2)}\n`);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
await main();
|
||||
}
|
||||
Reference in New Issue
Block a user