From 5ebef5493638b24ea37ea0bd654420a8a52c4ed6 Mon Sep 17 00:00:00 2001 From: Codex Date: Thu, 21 May 2026 12:58:03 +0000 Subject: [PATCH] fix: align mdtodo artifact health metadata contract --- deploy.json | 4 +- .../issue-9-k3s-user-service-cicd-state.md | 18 +- docs/reference/cicd-standardization.md | 5 +- docs/reference/user-service-delivery.md | 1 + ...-9-mdtodo-health-metadata-contract-test.ts | 172 ++++++++++++++++++ ...user-service-artifact-gap-contract-test.ts | 10 +- ...vice-deploy-apply-dry-run-contract-test.ts | 8 +- 7 files changed, 202 insertions(+), 16 deletions(-) create mode 100644 scripts/issue-9-mdtodo-health-metadata-contract-test.ts diff --git a/deploy.json b/deploy.json index 72d6877c..5947c1f8 100644 --- a/deploy.json +++ b/deploy.json @@ -71,7 +71,7 @@ { "id": "mdtodo", "repo": "https://github.com/pikasTech/unidesk", - "commitId": "75fb6757b2504ba86d61f2587fb34a9c9ed4019a" + "commitId": "595de3d320b73ec006794440b32db48b3ad14d2b" }, { "id": "decision-center", @@ -125,7 +125,7 @@ { "id": "mdtodo", "repo": "https://github.com/pikasTech/unidesk", - "commitId": "75fb6757b2504ba86d61f2587fb34a9c9ed4019a" + "commitId": "595de3d320b73ec006794440b32db48b3ad14d2b" }, { "id": "claudeqq", diff --git a/docs/issue/issue-9-k3s-user-service-cicd-state.md b/docs/issue/issue-9-k3s-user-service-cicd-state.md index 50d4ce8d..ed900ba3 100644 --- a/docs/issue/issue-9-k3s-user-service-cicd-state.md +++ b/docs/issue/issue-9-k3s-user-service-cicd-state.md @@ -21,7 +21,7 @@ This matrix closes the current review pass for the `decision-center`, `mdtodo`, | Service | Desired artifact | Deployment and CI shape | Dev acceptance | Prod acceptance | Ideal-state status | Blockers / next step | | --- | --- | --- | --- | --- | --- | --- | | `decision-center` | `127.0.0.1:5000/unidesk/decision-center:b5486a61ab0aa6c227366a95d1afa68281584359`, registry digest `sha256:55ae6b20af3b6ec88394de46678cd4ddf86c461126ee1e95e91005baf72f03ed`. Previous desired tag `54c1f8e165f90fa8509fda1f0c01f8c3fa82cbee` still exists with digest `sha256:8af6842a2a1b23bfaf6067a402821f4d0e54b76ebc24e59303c6cbefad6490d1`, but it is no longer the desired state. | k3s-managed artifact consumer on D601. CI producer is UniDesk source-build from `src/components/microservices/decision-center/Dockerfile`; CD dry-run is a no-build D601 k3s artifact consumer for dev and prod. | `unidesk-dev/decision-center-dev` is ready 1/1 and health reports `deploy.commit` / `deploy.requestedCommit` as `b5486a61ab0aa6c227366a95d1afa68281584359`, matching desired and artifact. | `unidesk/decision-center` is ready 1/1 and health reports `deploy.commit` / `deploy.requestedCommit` as `b5486a61ab0aa6c227366a95d1afa68281584359`; private proxy `/api/records?limit=1` returned 200. | Complete for artifact CD contract. Dev/prod desired, live health and registry artifact now align on `b5486a61ab0aa6c227366a95d1afa68281584359`; no deploy was needed. | Remaining work is manual UI/product acceptance only: record CRUD, diary lifecycle, doc-number uniqueness and frontend Decision Center visibility. Keep the desired-state contract green so future edits cannot reintroduce stale desired commits or source-build CD. | -| `mdtodo` | `127.0.0.1:5000/unidesk/mdtodo:75fb6757b2504ba86d61f2587fb34a9c9ed4019a`; registry HEAD returned 404, so no digest was available. | k3s-managed artifact consumer on D601. CI producer is UniDesk source-build from `src/components/microservices/mdtodo/Dockerfile`. | `unidesk-dev/mdtodo-dev` does not exist. | `unidesk/mdtodo` is ready 1/1. Deployment annotations record deploy and requested commit `75fb6757b2504ba86d61f2587fb34a9c9ed4019a`; health returned `ok=true`, and `/live` returned 200. Health does not expose deploy metadata. | Partial. Prod is healthy and annotated with the desired commit, but the desired registry artifact is absent and dev is absent. | Publish the desired artifact, add deploy metadata to health or keep strict label/annotation verification, then run dev -> focused smoke -> prod if prod replacement is still needed. | +| `mdtodo` | `127.0.0.1:5000/unidesk/mdtodo:595de3d320b73ec006794440b32db48b3ad14d2b`; registry artifact still needs publication. The previous `75fb6757b2504ba86d61f2587fb34a9c9ed4019a` target predates `mdtodo` health deploy metadata. | k3s-managed artifact consumer on D601. CI producer is UniDesk source-build from `src/components/microservices/mdtodo/Dockerfile`. | `unidesk-dev/mdtodo-dev` does not exist. | `unidesk/mdtodo` is ready 1/1 at the old annotated commit `75fb6757b2504ba86d61f2587fb34a9c9ed4019a`; health returned `ok=true`, and `/live` returned 200 during the earlier smoke. Runtime health metadata still needs proof after the new artifact is deployed. | Partial. The source/desired contract now points at a health-metadata-capable commit, but the desired registry artifact is absent, dev is absent and prod runtime is intentionally behind the new desired commit. | Publish the new desired artifact, create/verify `unidesk-dev/mdtodo-dev`, prove `/health.deploy.commit` and `/live.deploy.commit` in dev, then decide whether prod needs artifact replacement. | | `claudeqq` | `127.0.0.1:5000/unidesk/claudeqq:203b1f46684c91340ecbbd8a74502bd55e4f2011`; registry HEAD returned 404, so no digest was available. | k3s-managed artifact consumer on D601. CI producer uses the external Gitee source plus UniDesk adapter/overlay. | `unidesk-dev/claudeqq-dev` does not exist. | `unidesk/claudeqq` is ready 1/1. Deployment annotations and `/health` report commit/requested commit `203b1f46684c91340ecbbd8a74502bd55e4f2011`; health also reports NapCat `logged_in`. Focused API probes for `/api/events/recent` and `/api/events/subscriptions` returned 404. | Partial. Prod commit alignment and health are good, but the desired registry artifact is absent, dev is absent and the expected event API surface is not verified. | Publish the desired artifact, create/verify dev, and either fix or document the current event API paths before any prod artifact replacement. | | `todo-note` | `127.0.0.1:5000/unidesk/todo-note:a14ce0eb855a685fa17b47adacd54623e72cd2ff`; registry HEAD returned 404, so no digest was available. | Main-server Compose artifact consumer. CI producer uses the external Gitee source. CD plan is pull-only and no-build for Compose service `todo-note`, container `todo-note-backend`. | The dev/prod consumer plans resolve, but no live dev apply was attempted because the desired artifact is absent. | Runtime health returned 200 with PostgreSQL storage and running reminders. Private proxy `/api/instances` returned 200. The running container image is `unidesk-todo-note`; runtime labels do not expose `unidesk.ai/source-commit`, and health does not expose deploy metadata. | Not yet. Runtime behavior is healthy, but image digest/commit proof is missing and the desired registry artifact is absent. | Publish the desired artifact, then use the Compose artifact consumer to recreate only `todo-note` with no build/no deps and verify image labels plus health deploy metadata. | | `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. | @@ -37,13 +37,14 @@ Focused read-only evidence for this refresh: | 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. | +| `mdtodo` | `595de3d320b73ec006794440b32db48b3ad14d2b` | `75fb6757b2504ba86d61f2587fb34a9c9ed4019a` from prod Deployment annotations; that runtime predates `mdtodo` health deploy metadata | `false` | `missing-dev-service` | `healthy-prod-annotation-stale-after-health-metadata-repin` | `registry-artifact`, `dev-service`, `runtime-health-metadata-proof`, `prod-runtime-commit-drift` | Publish the desired artifact that includes `mdtodo` health deploy metadata, 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 contracts: ```bash +bun scripts/issue-9-mdtodo-health-metadata-contract-test.ts bun scripts/issue-9-user-service-artifact-gap-contract-test.ts bun scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts ``` @@ -53,6 +54,17 @@ bun scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts No live deployment or publish was executed in this pass. - `decision-center` drift was desired-state only: dev/prod live health and the registry artifact already matched `b5486a61...`, so `deploy.json` was repinned to that verified commit without deploying. -- `mdtodo`, `claudeqq`, `todo-note` and `project-manager` do not have the desired registry artifact tags, so live apply would not satisfy the artifact-consumer contract. +- `mdtodo`, `claudeqq`, `todo-note` and `project-manager` do not have the desired registry artifact tags, so live apply would not satisfy the artifact-consumer contract. For `mdtodo`, the desired tag is now `595de3d320b73ec006794440b32db48b3ad14d2b` because that is the already-merged commit that adds `/health.deploy` and `/live.deploy`. - `frontend` is the first batch sample that can be marked complete for the CI/CD artifact lane: desired commit, registry artifact digest, dev/prod health metadata, publish dry-run readiness and dev/prod CD no-build dry-runs are aligned. - Focused smoke stayed limited to health, deployment metadata, registry HEAD/tag checks and small private proxy API calls. + +## MDTODO Next Preconditions + +Before a real `mdtodo` artifact publish or dev deploy: + +- Run the read-only publish preflight for `595de3d320b73ec006794440b32db48b3ad14d2b`: `bun scripts/cli.ts ci publish-user-service --service mdtodo --commit 595de3d320b73ec006794440b32db48b3ad14d2b --dry-run`. It must report `runnerDisposition=ready` or clearly classify only infrastructure blockers. +- Publish only from the controlled D601 CI path: `bun scripts/cli.ts ci publish-user-service --service mdtodo --commit 595de3d320b73ec006794440b32db48b3ad14d2b --wait-ms 1200000`. +- Record the resulting `artifactSummary.imageRef`, `digest` and `digestRef`; verify registry `HEAD /v2/unidesk/mdtodo/manifests/595de3d320b73ec006794440b32db48b3ad14d2b` returns a digest. +- Keep `deploy apply --env dev --service mdtodo --dry-run` on the D601 k3s no-build artifact consumer and confirm it targets only `unidesk-dev/mdtodo-dev`. +- Run real dev apply only after the artifact exists, then verify `unidesk-dev/mdtodo-dev` readiness and service-proxy `/health.deploy.commit`, `/health.deploy.requestedCommit`, `/live.deploy.commit` all equal `595de3d320b73ec006794440b32db48b3ad14d2b`. +- Evaluate prod replacement only after dev proof is recorded; prod currently runs the older annotated `75fb6757b2504ba86d61f2587fb34a9c9ed4019a` runtime. diff --git a/docs/reference/cicd-standardization.md b/docs/reference/cicd-standardization.md index 8647712c..61ec35c6 100644 --- a/docs/reference/cicd-standardization.md +++ b/docs/reference/cicd-standardization.md @@ -302,7 +302,7 @@ This matrix describes the next promotion stage after dry-run coverage is in plac | `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 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 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 | +| `mdtodo` | `master` | source-build supported | dev + prod k3s consumer | dev service is absent until the health-metadata-capable desired artifact is published and `unidesk-dev/mdtodo-dev` is created/verified | prod is healthy at the previous annotated commit, but the desired artifact now points at the commit that added `/health.deploy` and `/live.deploy`; prod replacement remains blocked until dev proof exists | registry artifact, dev service, runtime health metadata proof and prod runtime commit drift; 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 | @@ -321,10 +321,11 @@ The contract fixes the current sample around one artifact lane: `deploy.json` de 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-mdtodo-health-metadata-contract-test.ts 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. +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. `mdtodo` also has a local health metadata contract that starts the service against a temporary Markdown workspace and verifies `/health.deploy` plus `/live.deploy` before publication. These tests do 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: diff --git a/docs/reference/user-service-delivery.md b/docs/reference/user-service-delivery.md index 03164b95..f8db13e6 100644 --- a/docs/reference/user-service-delivery.md +++ b/docs/reference/user-service-delivery.md @@ -161,6 +161,7 @@ MDTODO is a k3s-managed user-service artifact consumer. - The minimal standard artifact command is `bun scripts/cli.ts ci publish-user-service --service mdtodo --commit --wait-ms 1200000`. - The expected artifact is `127.0.0.1:5000/unidesk/mdtodo:` plus its registry digest from the CI output. +- The selected commit must include the `UNIDESK_DEPLOY_*` health metadata contract; `bun scripts/issue-9-mdtodo-health-metadata-contract-test.ts` is the focused local guard for `/health.deploy` and `/live.deploy`. - Dev CD must run before prod CD and lands in `unidesk-dev/mdtodo-dev`; production CD lands in `unidesk/mdtodo`. - Both paths must verify Deployment metadata and `/health` or `/live` deploy commit through the Kubernetes API service proxy. - No MDTODO release may add NodePort, hostPort, public business ports or provider-gateway direct business backends. diff --git a/scripts/issue-9-mdtodo-health-metadata-contract-test.ts b/scripts/issue-9-mdtodo-health-metadata-contract-test.ts new file mode 100644 index 00000000..3edaf53a --- /dev/null +++ b/scripts/issue-9-mdtodo-health-metadata-contract-test.ts @@ -0,0 +1,172 @@ +import { spawn, spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { createServer } from "node:net"; +import { rootPath } from "./src/config"; + +type JsonRecord = Record; + +const mdtodoCommit = "595de3d320b73ec006794440b32db48b3ad14d2b"; +const mdtodoRepo = "https://github.com/pikasTech/unidesk"; +const mdtodoSourcePath = "src/components/microservices/mdtodo/src/index.ts"; + +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 manifestService(manifest: JsonRecord, environment: "dev" | "prod", serviceId: string): JsonRecord { + const environments = asRecord(manifest.environments, "deploy.json.environments"); + const env = asRecord(environments[environment], `deploy.json.environments.${environment}`); + const services = Array.isArray(env.services) ? env.services.map((item, index) => asRecord(item, `${environment}.services[${index}]`)) : []; + const service = services.find((item) => item.id === serviceId); + assertCondition(service !== undefined, `deploy.json ${environment} must include ${serviceId}`, env); + return service as JsonRecord; +} + +async function reservePort(): Promise { + const server = createServer(); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(0, "127.0.0.1", resolve); + }); + const address = server.address(); + const port = typeof address === "object" && address !== null ? address.port : 0; + await new Promise((resolve, reject) => server.close((error) => error ? reject(error) : resolve())); + assertCondition(port > 0, "failed to reserve a local port", address); + return port; +} + +async function fetchJson(url: string): Promise { + const response = await fetch(url); + const text = await response.text(); + let body: unknown; + try { + body = JSON.parse(text) as unknown; + } catch { + body = { raw: text }; + } + assertCondition(response.ok, `request failed: ${url}`, { status: response.status, body }); + return asRecord(body, `response ${url}`); +} + +function assertDeployMetadata(body: JsonRecord, endpoint: "/health" | "/live"): void { + const deploy = asRecord(body.deploy, `${endpoint}.deploy`); + assertCondition(body.ok === true, `${endpoint} must be ok`, body); + assertCondition(body.service === "mdtodo", `${endpoint} service mismatch`, body); + assertCondition(deploy.serviceId === "mdtodo", `${endpoint} deploy service id mismatch`, deploy); + assertCondition(deploy.repo === mdtodoRepo, `${endpoint} deploy repo mismatch`, deploy); + assertCondition(deploy.commit === mdtodoCommit, `${endpoint} deploy commit mismatch`, deploy); + assertCondition(deploy.requestedCommit === mdtodoCommit, `${endpoint} requested commit mismatch`, deploy); +} + +function assertDesiredCommitIncludesHealthMetadata(): void { + const manifest = asRecord(JSON.parse(readFileSync(rootPath("deploy.json"), "utf8")) as unknown, "deploy.json"); + for (const environment of ["dev", "prod"] as const) { + const service = manifestService(manifest, environment, "mdtodo"); + assertCondition(service.repo === mdtodoRepo, `mdtodo ${environment} repo mismatch`, service); + assertCondition(service.commitId === mdtodoCommit, `mdtodo ${environment} desired commit must include health deploy metadata`, service); + } + + const desiredSource = spawnSync("git", ["show", `${mdtodoCommit}:${mdtodoSourcePath}`], { + cwd: rootPath(), + encoding: "utf8", + maxBuffer: 4 * 1024 * 1024, + }); + assertCondition(desiredSource.status === 0, "must be able to inspect the desired mdtodo source commit", { + status: desiredSource.status, + stderr: desiredSource.stderr.slice(-1000), + }); + assertCondition(desiredSource.stdout.includes("UNIDESK_DEPLOY_SERVICE_ID"), "desired mdtodo source must read deploy service id", {}); + assertCondition(desiredSource.stdout.includes("UNIDESK_DEPLOY_COMMIT"), "desired mdtodo source must read deploy commit", {}); + assertCondition(desiredSource.stdout.includes("UNIDESK_DEPLOY_REQUESTED_COMMIT"), "desired mdtodo source must read deploy requested commit", {}); + assertCondition(desiredSource.stdout.includes("deploy:"), "desired mdtodo source must expose deploy metadata in health/live payloads", {}); +} + +async function main(): Promise { + assertDesiredCommitIncludesHealthMetadata(); + + const port = await reservePort(); + const tempRoot = mkdtempSync(join(tmpdir(), "unidesk-mdtodo-health-")); + const workspace = join(tempRoot, "workspace"); + const logDir = join(tempRoot, "logs"); + mkdirSync(workspace, { recursive: true }); + mkdirSync(logDir, { recursive: true }); + writeFileSync(join(workspace, "issue-9-mdtodo.md"), "# Issue 9 MDTODO\n\n## R1 Health metadata proof\n\nLocal contract seed.\n", "utf8"); + + const stdout: string[] = []; + const stderr: string[] = []; + const child = spawn("bun", ["run", "src/index.ts"], { + cwd: rootPath("src", "components", "microservices", "mdtodo"), + env: { + ...process.env, + HOST: "127.0.0.1", + PORT: String(port), + MDTODO_ROOT_DIR: workspace, + LOG_FILE: join(logDir, "mdtodo.jsonl"), + UNIDESK_DEPLOY_SERVICE_ID: "mdtodo", + UNIDESK_DEPLOY_REPO: mdtodoRepo, + UNIDESK_DEPLOY_COMMIT: mdtodoCommit, + UNIDESK_DEPLOY_REQUESTED_COMMIT: mdtodoCommit, + }, + stdio: ["ignore", "pipe", "pipe"], + }); + child.stdout.setEncoding("utf8"); + child.stderr.setEncoding("utf8"); + child.stdout.on("data", (chunk) => stdout.push(String(chunk))); + child.stderr.on("data", (chunk) => stderr.push(String(chunk))); + + try { + let health: JsonRecord | null = null; + for (let attempt = 1; attempt <= 80; attempt += 1) { + if (child.exitCode !== null) break; + try { + health = await fetchJson(`http://127.0.0.1:${port}/health`); + break; + } catch { + await new Promise((resolve) => setTimeout(resolve, 100)); + } + } + assertCondition(health !== null, "mdtodo local health endpoint did not become ready", { + exitCode: child.exitCode, + stdout: stdout.join("").slice(-2000), + stderr: stderr.join("").slice(-2000), + }); + assertDeployMetadata(health as JsonRecord, "/health"); + assertCondition((health as JsonRecord).fileCount === 1, "/health should scan the seeded MDTODO file", health); + + const live = await fetchJson(`http://127.0.0.1:${port}/live`); + assertDeployMetadata(live, "/live"); + assertCondition(existsSync(join(logDir, "mdtodo.jsonl")) || stdout.join("").includes("service_started"), "local service should produce visible startup output", { + stdout: stdout.join("").slice(-1200), + stderr: stderr.join("").slice(-1200), + }); + } finally { + child.kill("SIGTERM"); + await new Promise((resolve) => setTimeout(resolve, 100)); + if (child.exitCode === null) child.kill("SIGKILL"); + rmSync(tempRoot, { recursive: true, force: true }); + } + + process.stdout.write(`${JSON.stringify({ + ok: true, + checks: [ + "deploy.json dev/prod mdtodo desired commit points at a health-metadata-capable source commit", + "desired mdtodo source commit reads UNIDESK_DEPLOY_* metadata", + "local /health exposes deploy.serviceId, repo, commit and requestedCommit", + "local /live exposes the same deploy metadata", + "local /health proves a readable seeded MDTODO markdown workspace", + ], + serviceId: "mdtodo", + desiredCommit: mdtodoCommit, + }, null, 2)}\n`); +} + +if (import.meta.main) { + await main(); +} diff --git a/scripts/issue-9-user-service-artifact-gap-contract-test.ts b/scripts/issue-9-user-service-artifact-gap-contract-test.ts index c8f786bd..4e5c6e4e 100644 --- a/scripts/issue-9-user-service-artifact-gap-contract-test.ts +++ b/scripts/issue-9-user-service-artifact-gap-contract-test.ts @@ -25,14 +25,14 @@ type ServiceContract = { const contracts: ServiceContract[] = [ { serviceId: "mdtodo", - desiredCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a", + desiredCommit: "595de3d320b73ec006794440b32db48b3ad14d2b", runtimeCommit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a", - runtimeCommitSource: "prod Deployment annotations; /health is ok but does not expose deploy metadata", + runtimeCommitSource: "prod Deployment annotations; desired artifact target was advanced because 75fb6757 predates mdtodo /health.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.", + prodStatus: "healthy-prod-annotation-stale-after-health-metadata-repin", + blockedScopes: ["registry-artifact", "dev-service", "runtime-health-metadata-proof", "prod-runtime-commit-drift"], + recommendedAction: "Publish the desired artifact that includes mdtodo health deploy metadata, 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", diff --git a/scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts b/scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts index d2d4a452..c59f6eda 100644 --- a/scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts +++ b/scripts/issue-9-user-service-deploy-apply-dry-run-contract-test.ts @@ -7,14 +7,14 @@ type ServiceCase = | { serviceId: "mdtodo"; environment: "dev"; - commit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a"; + commit: "595de3d320b73ec006794440b32db48b3ad14d2b"; sourceRepo: "https://github.com/pikasTech/unidesk"; dockerfile: "src/components/microservices/mdtodo/Dockerfile"; targetKind: "d601-k3s"; namespace: "unidesk-dev"; deployment: "mdtodo-dev"; service: "mdtodo-dev"; - runtimeImage: "unidesk-mdtodo:75fb6757b2504ba86d61f2587fb34a9c9ed4019a"; + runtimeImage: "unidesk-mdtodo:595de3d320b73ec006794440b32db48b3ad14d2b"; expectedValidationSnippets: string[]; rollbackType: "d601-k3s-previous-commit"; } @@ -166,14 +166,14 @@ const cases: ServiceCase[] = [ { serviceId: "mdtodo", environment: "dev", - commit: "75fb6757b2504ba86d61f2587fb34a9c9ed4019a", + commit: "595de3d320b73ec006794440b32db48b3ad14d2b", sourceRepo: "https://github.com/pikasTech/unidesk", dockerfile: "src/components/microservices/mdtodo/Dockerfile", targetKind: "d601-k3s", namespace: "unidesk-dev", deployment: "mdtodo-dev", service: "mdtodo-dev", - runtimeImage: "unidesk-mdtodo:75fb6757b2504ba86d61f2587fb34a9c9ed4019a", + runtimeImage: "unidesk-mdtodo:595de3d320b73ec006794440b32db48b3ad14d2b", expectedValidationSnippets: [ "D601 registry /v2 manifest exists", "native k3s containerd has the commit image and stable runtime image tag",