diff --git a/AGENTS.md b/AGENTS.md index b0448b9f..f8f13d2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -58,9 +58,10 @@ HWLAB 是硬件实验室运行面和控制面项目。本文是 agent、指挥 - Cloud Web M3 只读护栏:`npm run web:m3-readonly` - Cloud Workbench 布局/遮挡 smoke:`npm run web:layout`;local-build 用 `npm run web:layout:build`;DEV deploy 后用 `npm run web:layout:live`。 - DEV artifact 发布预检:`npm run dev-artifact:preflight` -- DEV artifact CI 发布:`HWLAB_CI_ARTIFACT_RUN_ID= npm run dev-artifact:publish` +- DEV artifact CI 发布:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind ci-publish --concurrency 4` - DEV 依赖 runtime base 构建:`npm run dev-runtime-base:build` -- DEV CD 单事务发布/应用/验证:`npm run dev-cd:apply` +- DEV CD 单事务发布/应用/验证:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4` +- CI/CD job 查询:`node tools/hwlab-cli/bin/hwlab-cli.mjs cicd status|logs|report ` - runner GitHub 可见性预检:`npm run runner:issue-visibility:preflight` - D601 k3s 只读观测:`npm run d601:k3s:readonly` - Gateway 主动出站本地 smoke:`npm run gateway:demo:smoke`;经本地 edge-proxy 验证用 `npm run gateway:demo:edge-smoke`。 diff --git a/docs/reference/deployment-publish.md b/docs/reference/deployment-publish.md index 8ac9138e..94cb88c0 100644 --- a/docs/reference/deployment-publish.md +++ b/docs/reference/deployment-publish.md @@ -43,18 +43,45 @@ truth. | Stage | Current HWLAB state | Required next behavior | | --- | --- | --- | | 1. Long-term reference | This file records DEV deployment, Cloud Web publish, API/edge/health, frp, D601 registry, k3s rollout, rollback, and verification rules. `AGENTS.md` indexes it. | Keep process issues as sources only; update this reference when a deployment rule changes. | -| 2. CLI or controlled scripts | `scripts/dev-artifact-publish.mjs` is the formal CI artifact publish entrypoint under CI identity; `scripts/dev-cd-apply.mjs` is the formal DEV CD side-effect entrypoint for desired-state apply and live verify. Other controlled scripts still cover contract planning, dry-run planning, preflight, single-step internals, k3s read-only visibility, and Cloud Web checks. Rollback is documented as commands/report hints but is not yet one unified CLI command. | Converge check, plan, rollback, status, and verify into HWLAB CLI while preserving CI/CD separation and the single CD transaction lock backed by `deploy/deploy.json`. | +| 2. CLI or controlled scripts | `hwlab-cli cicd submit/status/logs/report` 是正式 CI/CD 外层控制入口;`scripts/dev-artifact-publish.mjs` 和 `scripts/dev-cd-apply.mjs` 只作为后台 worker 或只读状态脚本使用。Other controlled scripts still cover contract planning, dry-run planning, preflight, single-step internals, k3s read-only visibility, and Cloud Web checks. Rollback is documented as commands/report hints but is not yet one unified CLI command. | Continue converging check, plan, rollback, status, and verify into HWLAB CLI while preserving CI/CD separation and the single CD transaction lock backed by `deploy/deploy.json`. | | 3. UniDesk CI/CD plus images | DEV services are imageized and published to the D601 registry. D601 rollout still includes manual bridge steps for Cloud Web and frp/k3s execution. | Move build, publish, desired-state refresh, rollout, post-deploy validation, and report upload into standard UniDesk CI/CD jobs without replacing HWLAB runtime. | +## CI/CD CLI 非阻塞合同 + +HWLAB CI/CD 正式 CLI 必须遵循 `Skill(cli-spec)`:可能超过短时间的 build、 +push、apply、rollout、runtime Job、smoke 和 postflight 都不得以前台阻塞命令 +暴露给指挥官、runner 或 master-side wrapper。正式入口必须使用 +Fire-and-Forget 模式: + +```sh +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind ci-publish --concurrency 4 +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4 +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd status +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd logs --tail-bytes 8000 +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd report +``` + +`submit` 只创建 `.state/hwlab-cicd/jobs//`,写入 `spec.json`、 +`state.json`、`stdout.log`、`stderr.log` 和 `report.json` 路径,然后启动后台 +worker 并立即返回有界 JSON。`status` 仅读 `state.json`,`logs` 默认只返回 +尾部摘要,`report` 默认只返回报告摘要;完整日志和完整报告通过返回的文件路径 +渐进查看。任何正式 CLI 调用都不得因为 Docker build、registry push、 +`kubectl wait --timeout` 或 rollout status 在前台长时间占用上下文。 + +`scripts/dev-artifact-publish.mjs`、`scripts/dev-cd-apply.mjs` 和其他内部脚本仍 +保留为 worker/backend:它们可以在后台 job 内执行长流程,但不能作为指挥面常规 +入口直接运行。临时手动排障可以直接调用底层脚本,但排障结论必须回填到正式 +CLI、长期参考文档或 issue,避免把一次性命令固化为新流程。 + ## DEV CD Transaction -正式 DEV CD 写路径只有一个入口: +正式 DEV CD 写路径只有一个外层入口: ```sh -node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json +node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4 ``` -该命令是唯一允许按 `deploy.json` 对 `hwlab-dev` 执行 desired-state apply、并复验 +该命令后台调用 `scripts/dev-cd-apply.mjs`,是唯一允许按 `deploy.json` 对 `hwlab-dev` 执行 desired-state apply、并复验 公开 `16666/16667` 的正式 CD 写路径。镜像发布属于 CI artifact 职责;过渡期 latest-main 兼容路径仍可由该命令调用 artifact publish,但正式 rollout 默认应消费 已经发布并经过 registry manifest 验证的 artifact。 @@ -150,7 +177,9 @@ repo-owned 入口,不能绕过它们直接拼接 publish、apply、rollout 或 | 读取 DEV CD 状态 | `node scripts/dev-cd-apply.mjs --status` | 不写入、不获取 Lease、不读取 Secret value。 | | 事务 dry-run preflight | `node scripts/dev-cd-apply.mjs --dry-run` | 不 publish、不 apply、不 rollout。 | | 检查 desired-state 收敛 | `node scripts/deploy-desired-state-plan.mjs --promotion-commit --check` | 只读 source/report。 | -| 执行 DEV CD 事务 | `node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json` | 仅 DEV,在 `Lease/hwlab-dev/hwlab-dev-cd-lock` 下 apply/verify;legacy latest-main publish 仅作过渡兼容。 | +| 提交 CI artifact publish | `node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind ci-publish --concurrency 4` | 立刻返回 jobId;后台 build/push,报告写入 `.state/hwlab-cicd/jobs//report.json`。 | +| 提交 DEV CD 事务 | `node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4` | 立刻返回 jobId;后台在 `Lease/hwlab-dev/hwlab-dev-cd-lock` 下 apply/verify;legacy latest-main publish 仅作过渡兼容。 | +| 查询 CI/CD job | `node tools/hwlab-cli/bin/hwlab-cli.mjs cicd status|logs|report ` | 只读 `.state/hwlab-cicd/jobs//`;日志和报告默认有界摘要。 | | 读取 D601 k3s 证据 | `node scripts/d601-k3s-readonly-observability.mjs` | 只读可见性报告;不 rollout、不输出 Secret value。 | 使用 master-side wrapper 时,稳定形态是: @@ -158,7 +187,7 @@ repo-owned 入口,不能绕过它们直接拼接 publish、apply、rollout 或 ```sh cd /root/unidesk bun scripts/cli.ts ssh D601 argv bash -lc 'cd && KUBECONFIG=/etc/rancher/k3s/k3s.yaml node scripts/dev-cd-apply.mjs --status' -bun scripts/cli.ts ssh D601 argv bash -lc 'cd && KUBECONFIG=/etc/rancher/k3s/k3s.yaml node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/report.json' +bun scripts/cli.ts ssh D601 argv bash -lc 'cd && KUBECONFIG=/etc/rancher/k3s/k3s.yaml node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4' ``` `` 必须指向干净工作区。该工作区可以是固定 clean diff --git a/package.json b/package.json index 8129f46b..bf2acda5 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "type": "module", "scripts": { "validate": "node scripts/repo-reports-guard.mjs && node scripts/validate-contract.mjs && node scripts/deploy-contract-plan.mjs --check && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --test scripts/artifact-runtime-readiness-guard.test.mjs", - "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", + "check": "node scripts/repo-reports-guard.mjs && node --check scripts/repo-reports-guard.mjs && node --check scripts/src/report-paths.mjs && node --check internal/protocol/index.mjs && node --check internal/build-metadata.mjs && node --check internal/agent/index.mjs && node --check internal/agent/runtime.mjs && node --check internal/audit/index.mjs && node --check internal/db/runtime-store.mjs && node --check internal/db/runtime-store.test.mjs && node --check internal/mvp-gate/summary.mjs && node --check internal/cloud/db-contract.mjs && node --check internal/dev-entrypoint/http.mjs && node --check internal/dev-entrypoint/http.test.mjs && node --check internal/cloud/code-agent-contract.mjs && node --check internal/cloud/code-agent-session-registry.mjs && node --check internal/cloud/code-agent-session-registry.test.mjs && node --check internal/cloud/code-agent-trace-store.mjs && node --check internal/cloud/codex-stdio-session.mjs && node --check internal/cloud/json-rpc.mjs && node --check internal/cloud/code-agent-chat.mjs && node --check internal/cloud/m3-io-control.mjs && node --check internal/cloud/server.mjs && node --check internal/sim/model.mjs && node --check internal/sim/model.test.mjs && node --check internal/sim/http.mjs && node --check internal/sim/l2-runtime.mjs && node --check internal/patchpanel/model.mjs && node --check internal/patchpanel/runtime.mjs && node --check cmd/hwlab-cloud-api/main.mjs && node --check cmd/hwlab-cloud-api/provision.mjs && node --check cmd/hwlab-cloud-api/migrate.mjs && node --check cmd/hwlab-edge-proxy/main.mjs && node --check cmd/hwlab-agent-mgr/main.mjs && node --check cmd/hwlab-agent-worker/main.mjs && node --check cmd/hwlab-box-simu/main.mjs && node --check cmd/hwlab-gateway/main.mjs && node --check cmd/hwlab-gateway-simu/main.mjs && node --check scripts/cloud-api-runtime-smoke.mjs && node --check scripts/code-agent-chat-smoke.mjs && node --check scripts/dev-edge-health-smoke.mjs && node --check scripts/src/dev-edge-health-smoke-lib.mjs && node --check scripts/validate-contract.mjs && node --check scripts/deploy-contract-plan.test.mjs && node --check scripts/deploy-desired-state-plan.mjs && node --check scripts/src/deploy-desired-state-plan.mjs && node --check scripts/artifact-runtime-readiness-guard.mjs && node --check scripts/src/artifact-runtime-readiness-guard.mjs && node --check scripts/artifact-runtime-readiness-guard.test.mjs && node --check scripts/report-lifecycle.mjs && node --check scripts/report-lifecycle.test.mjs && node --check scripts/validate-dev-gate-report.mjs && node --check scripts/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.mjs && node --check scripts/src/dev-cd-apply.test.mjs && node --check scripts/dev-m3-hardware-loop-smoke.test.mjs && node --check scripts/m3-io-control-e2e.mjs && node --check scripts/src/m3-io-control-e2e.mjs && node --check scripts/m3-io-control-e2e.test.mjs && node --check scripts/dev-cloud-workbench-smoke.test.mjs && node --check scripts/dev-cloud-workbench-layout-smoke.mjs && node --check scripts/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.mjs && node --check scripts/src/rpt004-mvp-e2e-harness.test.mjs && node --check scripts/validate-dev-m3-cardinality.mjs && node --check scripts/validate-dev-m3-cardinality.test.mjs && node --check scripts/validate-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.mjs && node --check scripts/refresh-artifact-catalog.test.mjs && node --check scripts/dev-artifact-publish.mjs && node --check scripts/dev-runtime-base-image.mjs && node --check scripts/src/dev-artifact-services.mjs && node --check scripts/src/registry-capabilities.mjs && node --check scripts/preflight-dev-base-image.mjs && node --check scripts/src/dev-base-image-preflight.mjs && node --check scripts/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.mjs && node --check scripts/src/dev-evidence-blocker-aggregator.test.mjs && node --check scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs && node --check scripts/d601-k3s-readonly-observability.mjs && node --check scripts/src/d601-k3s-readonly-observability.mjs && node --check scripts/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.mjs && node --check scripts/src/dev-runtime-provisioning.test.mjs && node --check scripts/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.mjs && node --check scripts/src/dev-runtime-migration.test.mjs && node --check scripts/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.mjs && node --check scripts/src/dev-runtime-postflight.test.mjs && node --check scripts/l2-runtime-contract-smoke.mjs && node --check scripts/patch-panel-runtime-smoke.mjs && node --check scripts/export-web-gate-summary.mjs && node --check scripts/l6-cli-web-smoke.mjs && node --check skills/hwlab-agent-runtime/scripts/hwlab-agent-runtime-cli.mjs && node --check skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs && node --check skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs && node --check tools/hwlab-cli/bin/hwlab-cli.mjs && node --check tools/hwlab-cli/lib/cli.mjs && node --check tools/hwlab-cli/lib/cicd-jobs.mjs && node --check tools/hwlab-cli/lib/cli.test.mjs && node --check web/hwlab-cloud-web/app.mjs && node --check web/hwlab-cloud-web/code-agent-facts.mjs && node --check web/hwlab-cloud-web/code-agent-facts.test.mjs && node --check web/hwlab-cloud-web/code-agent-status.mjs && node --check web/hwlab-cloud-web/code-agent-status.test.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.mjs && node --check web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs && node --check web/hwlab-cloud-web/live-status.mjs && node --check web/hwlab-cloud-web/wiring-status.mjs && node --check web/hwlab-cloud-web/gate-summary.mjs && node --check web/hwlab-cloud-web/scripts/check.mjs && node --check web/hwlab-cloud-web/scripts/live-status-contract.test.mjs && node --check web/hwlab-cloud-web/scripts/build.mjs && node --check web/hwlab-cloud-web/scripts/dist-contract.mjs && node scripts/validate-contract.mjs && node scripts/validate-dev-gate-report.mjs && node scripts/report-lifecycle.test.mjs && node scripts/validate-dev-m3-cardinality.mjs && node scripts/validate-artifact-catalog.mjs && node scripts/deploy-desired-state-plan.mjs --check && node scripts/dev-runtime-provisioning.mjs --check && node scripts/dev-runtime-migration.mjs --check && node cmd/hwlab-cloud-api/provision.mjs --check && node cmd/hwlab-cloud-api/migrate.mjs --check && node scripts/dev-runtime-postflight.mjs --check && node scripts/rpt004-mvp-e2e-harness.mjs --check --no-write && node scripts/dev-evidence-blocker-aggregator.mjs --check && node scripts/l2-runtime-contract-smoke.mjs && node scripts/l6-cli-web-smoke.mjs && node web/hwlab-cloud-web/scripts/check.mjs && node scripts/code-agent-chat-smoke.mjs && node --test web/hwlab-cloud-web/code-agent-facts.test.mjs web/hwlab-cloud-web/code-agent-status.test.mjs web/hwlab-cloud-web/wiring-status.test.mjs web/hwlab-cloud-web/code-agent-m3-evidence.test.mjs scripts/artifact-runtime-readiness-guard.test.mjs scripts/deploy-contract-plan.test.mjs scripts/dev-m3-hardware-loop-smoke.test.mjs scripts/validate-dev-m3-cardinality.test.mjs scripts/dev-cloud-workbench-smoke.test.mjs web/hwlab-cloud-web/scripts/live-status-contract.test.mjs scripts/deploy-desired-state-plan.test.mjs scripts/refresh-artifact-catalog.test.mjs scripts/src/dev-cd-apply.test.mjs scripts/src/dev-deploy-apply.test.mjs scripts/src/dev-runtime-provisioning.test.mjs scripts/src/dev-runtime-migration.test.mjs scripts/src/dev-runtime-postflight.test.mjs scripts/src/dev-m4-agent-loop-smoke-lib.test.mjs scripts/src/dev-evidence-blocker-aggregator.test.mjs skills/hwlab-agent-runtime/scripts/m3-io-skill-client.test.mjs tools/hwlab-cli/lib/cli.test.mjs internal/agent/index.test.mjs internal/audit/index.test.mjs internal/db/schema.test.mjs internal/db/runtime-store.test.mjs internal/cloud/json-rpc.test.mjs internal/cloud/m3-io-control.test.mjs internal/cloud/code-agent-session-registry.test.mjs internal/cloud/server.test.mjs internal/dev-entrypoint/http.test.mjs internal/sim/model.test.mjs internal/patchpanel/model.test.mjs internal/patchpanel/runtime.test.mjs && node scripts/cloud-api-runtime-smoke.mjs && sh -n scripts/bootstrap-skills.sh scripts/worker-entrypoint.sh", "dev-base-image:preflight": "node scripts/preflight-dev-base-image.mjs", "dev-runtime-base:build": "node scripts/dev-runtime-base-image.mjs", "cloud-api:smoke": "node scripts/cloud-api-runtime-smoke.mjs", @@ -30,8 +30,9 @@ "web:build": "node web/hwlab-cloud-web/scripts/build.mjs", "artifact-catalog:refresh-blocked": "node scripts/refresh-artifact-catalog.mjs --target-ref origin/main --blocked", "dev-artifact:preflight": "node scripts/dev-artifact-publish.mjs --preflight", - "dev-cd:apply": "node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-cd-apply.json", - "dev-artifact:publish": "node scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --report /tmp/hwlab-dev-gate/dev-cd-apply.json", + "dev-cd:apply": "node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4", + "dev-artifact:publish": "node tools/hwlab-cli/bin/hwlab-cli.mjs cicd submit --kind ci-publish --concurrency 4", + "cicd:jobs": "node tools/hwlab-cli/bin/hwlab-cli.mjs cicd jobs", "docs:validate:m3-rollout": "node scripts/validate-m3-rollout-runbook.mjs", "d601:k3s:readonly": "node scripts/d601-k3s-readonly-observability.mjs", "runner:issue-visibility:preflight": "node scripts/runner-issue-visibility-preflight.mjs", diff --git a/tools/hwlab-cli/lib/cicd-job-worker.mjs b/tools/hwlab-cli/lib/cicd-job-worker.mjs new file mode 100644 index 00000000..87d972fb --- /dev/null +++ b/tools/hwlab-cli/lib/cicd-job-worker.mjs @@ -0,0 +1,9 @@ +import { runCicdWorker } from "./cicd-jobs.mjs"; + +const jobId = process.argv[2]; +if (!jobId) { + process.stderr.write(`${JSON.stringify({ ok: false, code: "missing-job-id", message: "usage: node tools/hwlab-cli/lib/cicd-job-worker.mjs " }, null, 2)}\n`); + process.exitCode = 1; +} else { + process.exitCode = await runCicdWorker({ repoRoot: process.cwd(), jobId }); +} diff --git a/tools/hwlab-cli/lib/cicd-jobs.mjs b/tools/hwlab-cli/lib/cicd-jobs.mjs new file mode 100644 index 00000000..3a246852 --- /dev/null +++ b/tools/hwlab-cli/lib/cicd-jobs.mjs @@ -0,0 +1,396 @@ +import { spawn } from "node:child_process"; +import { randomBytes } from "node:crypto"; +import { createWriteStream } from "node:fs"; +import { mkdir, open, readdir, readFile, stat, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; + +const stateRoot = ".state/hwlab-cicd/jobs"; +const knownKinds = new Set(["ci-publish", "dev-cd-apply"]); + +function isoNow(now) { + return (now ? now() : new Date()).toISOString(); +} + +function compactTimestamp(value) { + return value.replace(/[-:.]/gu, "").slice(0, 15); +} + +function makeJobId(kind, now) { + return `${kind}-${compactTimestamp(isoNow(now))}-${randomBytes(3).toString("hex")}`; +} + +function jobDir(repoRoot, jobId) { + return path.join(repoRoot, stateRoot, jobId); +} + +function jobPaths(repoRoot, jobId) { + const dir = jobDir(repoRoot, jobId); + return { + dir, + specPath: path.join(dir, "spec.json"), + statePath: path.join(dir, "state.json"), + stdoutPath: path.join(dir, "stdout.log"), + stderrPath: path.join(dir, "stderr.log"), + reportPath: path.join(dir, "report.json") + }; +} + +async function readJson(filePath) { + return JSON.parse(await readFile(filePath, "utf8")); +} + +async function writeJsonFile(filePath, value) { + await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, "utf8"); +} + +function result(exitCode, payload, stream = "stdout") { + return { exitCode, payload, stream }; +} + +function error(code, message, details = {}) { + return { ok: false, code, message, ...details }; +} + +function followup(jobId) { + return { + status: `hwlab-cli cicd status ${jobId}`, + logs: `hwlab-cli cicd logs ${jobId}`, + report: `hwlab-cli cicd report ${jobId}` + }; +} + +function optionValue(options, name, fallback = null) { + return options.get(name) ?? fallback; +} + +function positiveInteger(value, fallback) { + if (value == null) return fallback; + const parsed = Number.parseInt(value, 10); + return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; +} + +function buildJobSpec({ kind, flags, options, repoRoot, env, now }) { + if (!knownKinds.has(kind)) { + return { error: error("unsupported-cicd-kind", "Use --kind ci-publish or --kind dev-cd-apply.", { kind }) }; + } + + const jobId = makeJobId(kind, now); + const paths = jobPaths(repoRoot, jobId); + const envPatch = {}; + const args = []; + + if (kind === "ci-publish") { + args.push("scripts/dev-artifact-publish.mjs", "--publish", "--report", paths.reportPath, "--quiet-build"); + const services = optionValue(options, "--services"); + if (services) args.push("--services", services); + const concurrency = positiveInteger(optionValue(options, "--concurrency"), 4); + args.push("--concurrency", String(concurrency)); + envPatch.HWLAB_CI_ARTIFACT_RUN_ID = optionValue(options, "--owner-task-id", jobId); + envPatch.HWLAB_CI_ARTIFACT_RUN_OWNER = optionValue(options, "--owner", "hwlab-cli"); + } else { + if (!flags.has("--confirm-dev") || !flags.has("--confirmed-non-production")) { + return { + error: error("confirmation-required", "DEV CD apply requires --confirm-dev --confirmed-non-production.", { + requiredFlags: ["--confirm-dev", "--confirmed-non-production"] + }) + }; + } + args.push( + "scripts/dev-cd-apply.mjs", + "--apply", + "--confirm-dev", + "--confirmed-non-production", + "--owner-task-id", + optionValue(options, "--owner-task-id", jobId), + "--report", + paths.reportPath + ); + if (flags.has("--break-stale-lock")) args.push("--break-stale-lock"); + const concurrency = optionValue(options, "--concurrency"); + if (concurrency) envPatch.HWLAB_DEV_CD_CONCURRENCY = String(positiveInteger(concurrency, 4)); + } + + if (optionValue(options, "--target-ref")) args.push("--target-ref", optionValue(options, "--target-ref")); + if (optionValue(options, "--kubeconfig")) args.push("--kubeconfig", optionValue(options, "--kubeconfig")); + + return { + spec: { + jobId, + kind, + cwd: repoRoot, + node: process.execPath, + command: process.execPath, + args, + envPatch, + createdAt: isoNow(now), + host: os.hostname(), + paths + }, + paths, + env + }; +} + +async function defaultSpawnDetached({ repoRoot, jobId, env }) { + const workerPath = path.join(repoRoot, "tools/hwlab-cli/lib/cicd-job-worker.mjs"); + const child = spawn(process.execPath, [workerPath, jobId], { + cwd: repoRoot, + env, + detached: true, + stdio: "ignore" + }); + child.unref(); + return { pid: child.pid }; +} + +async function submitJob(ctx) { + const kind = optionValue(ctx.options, "--kind"); + const built = buildJobSpec({ ...ctx, kind }); + if (built.error) return result(1, built.error, "stderr"); + + await mkdir(built.paths.dir, { recursive: true }); + await writeJsonFile(built.paths.specPath, built.spec); + const initialState = { + ok: true, + jobId: built.spec.jobId, + kind: built.spec.kind, + status: "submitted", + nonBlocking: true, + createdAt: built.spec.createdAt, + updatedAt: built.spec.createdAt, + cwd: built.spec.cwd, + command: [path.basename(built.spec.command), ...built.spec.args].join(" "), + statePath: built.paths.statePath, + stdoutPath: built.paths.stdoutPath, + stderrPath: built.paths.stderrPath, + reportPath: built.paths.reportPath + }; + await writeJsonFile(built.paths.statePath, initialState); + + const launcher = ctx.spawnDetached ?? defaultSpawnDetached; + const launched = await launcher({ repoRoot: ctx.repoRoot, jobId: built.spec.jobId, env: ctx.env, spec: built.spec }); + const submitted = { + ...initialState, + pid: launched?.pid ?? null, + followup: followup(built.spec.jobId) + }; + await writeJsonFile(built.paths.statePath, submitted); + return result(0, submitted); +} + +async function appendLog(filePath, line) { + await writeFile(filePath, `${line}\n`, { flag: "a" }); +} + +function reportSummary(report) { + if (report?.artifactPublish) { + return { + kind: "ci-publish", + status: report.artifactPublish.status ?? report.status ?? null, + sourceCommitId: report.sourceCommitId ?? report.commitId ?? null, + serviceCount: report.artifactPublish.serviceCount ?? null, + requiredServiceCount: report.artifactPublish.requiredServiceCount ?? null, + publishedCount: report.artifactPublish.publishedCount ?? null, + timings: report.artifactPublish.timings ?? null, + blockers: report.blockers ?? [] + }; + } + if (report?.devCdApply) { + return { + kind: "dev-cd-apply", + status: report.status ?? null, + commitId: report.commitId ?? null, + target: report.target ?? null, + transaction: report.transaction + ? { + transactionId: report.transaction.transactionId ?? null, + ownerTaskId: report.transaction.ownerTaskId ?? null, + phases: report.transaction.phases ?? [], + release: report.transaction.release ?? null + } + : null, + steps: report.devCdApply.steps?.map((step) => ({ id: step.id, status: step.status, reportPath: step.reportPath })) ?? [], + blockers: report.blockers ?? [] + }; + } + return { + kind: "generic", + status: report?.status ?? report?.ok ?? null, + keys: report && typeof report === "object" ? Object.keys(report).slice(0, 20) : [] + }; +} + +async function runJob(ctx) { + const jobId = ctx.rest[0]; + if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd run-job "), "stderr"); + const paths = jobPaths(ctx.repoRoot, jobId); + const spec = await readJson(paths.specPath); + const startedAt = isoNow(ctx.now); + await writeJsonFile(paths.statePath, { + ok: true, + jobId, + kind: spec.kind, + status: "running", + nonBlocking: true, + createdAt: spec.createdAt, + startedAt, + updatedAt: startedAt, + cwd: spec.cwd, + pid: process.pid, + command: [path.basename(spec.command), ...spec.args].join(" "), + statePath: paths.statePath, + stdoutPath: paths.stdoutPath, + stderrPath: paths.stderrPath, + reportPath: paths.reportPath + }); + await appendLog(paths.stdoutPath, `[hwlab-cli] job ${jobId} started at ${startedAt}`); + + const child = spawn(spec.command, spec.args, { + cwd: spec.cwd, + env: { ...process.env, ...spec.envPatch }, + stdio: ["ignore", "pipe", "pipe"] + }); + const stdoutStream = createWriteStream(paths.stdoutPath, { flags: "a" }); + const stderrStream = createWriteStream(paths.stderrPath, { flags: "a" }); + child.stdout.pipe(stdoutStream); + child.stderr.pipe(stderrStream); + const exitCode = await new Promise((resolve) => { + child.on("close", resolve); + }); + stdoutStream.end(); + stderrStream.end(); + + const finishedAt = isoNow(ctx.now); + const state = await readJson(paths.statePath); + let summary = null; + try { + summary = reportSummary(await readJson(paths.reportPath)); + } catch { + summary = null; + } + const finalState = { + ...state, + status: exitCode === 0 ? "succeeded" : "failed", + ok: exitCode === 0, + exitCode, + updatedAt: finishedAt, + finishedAt, + durationMs: Date.parse(finishedAt) - Date.parse(startedAt), + reportSummary: summary, + followup: followup(jobId) + }; + await writeJsonFile(paths.statePath, finalState); + return result(exitCode === 0 ? 0 : 2, finalState, exitCode === 0 ? "stdout" : "stderr"); +} + +async function readState(repoRoot, jobId) { + const paths = jobPaths(repoRoot, jobId); + const state = await readJson(paths.statePath); + return { paths, state }; +} + +async function statusJob(ctx) { + const jobId = ctx.rest[0]; + if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd status "), "stderr"); + try { + const { state } = await readState(ctx.repoRoot, jobId); + return result(0, { ...state, followup: followup(jobId) }); + } catch (err) { + return result(2, error("job-not-found", `No CI/CD job state found for ${jobId}.`, { detail: err.message }), "stderr"); + } +} + +async function tailFile(filePath, bytes) { + try { + const info = await stat(filePath); + const length = Math.min(info.size, bytes); + const handle = await open(filePath, "r"); + try { + const buffer = Buffer.alloc(length); + await handle.read(buffer, 0, length, info.size - length); + return { path: filePath, totalBytes: info.size, returnedBytes: length, truncated: info.size > length, text: buffer.toString("utf8") }; + } finally { + await handle.close(); + } + } catch { + return { path: filePath, totalBytes: 0, returnedBytes: 0, truncated: false, text: "" }; + } +} + +async function logsJob(ctx) { + const jobId = ctx.rest[0]; + if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd logs "), "stderr"); + const bytes = Math.min(positiveInteger(optionValue(ctx.options, "--tail-bytes"), 8000), 20000); + try { + const { paths, state } = await readState(ctx.repoRoot, jobId); + return result(0, { + ok: true, + jobId, + status: state.status, + tailBytes: bytes, + stdout: await tailFile(paths.stdoutPath, bytes), + stderr: await tailFile(paths.stderrPath, bytes), + followup: followup(jobId) + }); + } catch (err) { + return result(2, error("job-not-found", `No CI/CD job state found for ${jobId}.`, { detail: err.message }), "stderr"); + } +} + +async function reportJob(ctx) { + const jobId = ctx.rest[0]; + if (!jobId) return result(1, error("missing-job-id", "usage: hwlab-cli cicd report "), "stderr"); + try { + const { paths, state } = await readState(ctx.repoRoot, jobId); + const report = await readJson(paths.reportPath); + return result(0, { + ok: true, + jobId, + status: state.status, + reportPath: paths.reportPath, + summary: reportSummary(report), + report: ctx.flags.has("--full") ? report : undefined, + followup: followup(jobId) + }); + } catch (err) { + return result(2, error("report-not-ready", `No report is available for ${jobId}.`, { detail: err.message }), "stderr"); + } +} + +async function listJobs(ctx) { + const root = path.join(ctx.repoRoot, stateRoot); + const limit = Math.min(positiveInteger(optionValue(ctx.options, "--limit"), 20), 100); + try { + const names = (await readdir(root)).sort().reverse().slice(0, limit); + const jobs = []; + for (const name of names) { + try { + jobs.push((await readState(ctx.repoRoot, name)).state); + } catch { + // Ignore incomplete job directories; logs/report still remain on disk. + } + } + return result(0, { ok: true, command: "hwlab-cli cicd jobs", count: jobs.length, jobs }); + } catch { + return result(0, { ok: true, command: "hwlab-cli cicd jobs", count: 0, jobs: [] }); + } +} + +export async function runCicdCommand(ctx) { + if (ctx.subcommand === "submit") return submitJob(ctx); + if (ctx.subcommand === "status") return statusJob(ctx); + if (ctx.subcommand === "logs") return logsJob(ctx); + if (ctx.subcommand === "report") return reportJob(ctx); + if (ctx.subcommand === "jobs") return listJobs(ctx); + return result(1, error("usage", "usage: hwlab-cli cicd submit --kind | status|logs|report | jobs"), "stderr"); +} + +export async function runCicdWorker({ repoRoot = process.cwd(), jobId, now }) { + const outcome = await runJob({ + repoRoot, + rest: [jobId], + now + }); + return outcome.exitCode; +} diff --git a/tools/hwlab-cli/lib/cli.mjs b/tools/hwlab-cli/lib/cli.mjs index 147a2850..1a077ba0 100644 --- a/tools/hwlab-cli/lib/cli.mjs +++ b/tools/hwlab-cli/lib/cli.mjs @@ -1,5 +1,6 @@ import { DEV_ENDPOINT, loadMvpGateSummary } from "../../../internal/mvp-gate/summary.mjs"; import { runM3IoSkillCommand } from "../../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; +import { runCicdCommand } from "./cicd-jobs.mjs"; function formatJson(value) { return JSON.stringify(value, null, 2); @@ -173,6 +174,12 @@ function writeHelp(stdout) { "m3 status --api-base-url URL", "m3 io --action do.write --value true --approved --api-base-url URL", "m3 io --action di.read --api-base-url URL", + "cicd submit --kind ci-publish --concurrency 4", + "cicd submit --kind dev-cd-apply --confirm-dev --confirmed-non-production --concurrency 4", + "cicd status JOB_ID", + "cicd logs JOB_ID --tail-bytes 8000", + "cicd report JOB_ID", + "cicd jobs --limit 20", "test e2e --env dev --mvp", "test e2e --env dev --mvp --dry-run", "test e2e --env dev --mvp --live --confirm-dev --confirmed-non-production" @@ -194,6 +201,21 @@ export async function runCli(argv, io) { const [command = "help", subcommand, ...rest] = argv; const { flags, options } = parseArgs(rest); + if (command === "cicd") { + const outcome = await runCicdCommand({ + subcommand, + rest, + flags, + options, + repoRoot, + env, + now: io.now, + spawnDetached: io.spawnDetached + }); + writeJson(outcome.stream === "stderr" ? stderr : stdout, outcome.payload); + return outcome.exitCode; + } + if (command === "m3" && ["io", "status"].includes(subcommand)) { const result = await runM3IoSkillCommand(["m3", subcommand, ...rest], { env, diff --git a/tools/hwlab-cli/lib/cli.test.mjs b/tools/hwlab-cli/lib/cli.test.mjs index 7ba989d7..34e62955 100644 --- a/tools/hwlab-cli/lib/cli.test.mjs +++ b/tools/hwlab-cli/lib/cli.test.mjs @@ -1,4 +1,7 @@ import assert from "node:assert/strict"; +import { mkdtemp, readFile, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; import test from "node:test"; import { @@ -16,6 +19,7 @@ async function captureCli(args, options = {}) { PATH: process.env.PATH }, requestJson: options.requestJson, + spawnDetached: options.spawnDetached, stdout: { write(chunk) { stdout += chunk; @@ -30,6 +34,82 @@ async function captureCli(args, options = {}) { return { exitCode, stdout, stderr }; } +test("repo hwlab-cli cicd submit is non-blocking and records follow-up commands", async () => { + const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-")); + const launched = []; + const result = await captureCli( + ["cicd", "submit", "--kind", "ci-publish", "--services", "hwlab-cloud-web", "--concurrency", "2"], + { + cwd, + spawnDetached: async (job) => { + launched.push(job); + return { pid: 123456 }; + } + } + ); + + assert.equal(result.exitCode, 0, result.stderr); + const body = JSON.parse(result.stdout); + assert.equal(body.ok, true); + assert.equal(body.nonBlocking, true); + assert.equal(body.kind, "ci-publish"); + assert.match(body.jobId, /^ci-publish-/u); + assert.equal(body.pid, 123456); + assert.equal(body.followup.status, `hwlab-cli cicd status ${body.jobId}`); + assert.equal(launched.length, 1); + + const spec = JSON.parse(await readFile(path.join(cwd, ".state", "hwlab-cicd", "jobs", body.jobId, "spec.json"), "utf8")); + assert.deepEqual(spec.args.slice(0, 4), ["scripts/dev-artifact-publish.mjs", "--publish", "--report", body.reportPath]); + assert.ok(spec.args.includes("--quiet-build")); + assert.equal(spec.envPatch.HWLAB_CI_ARTIFACT_RUN_ID, body.jobId); +}); + +test("repo hwlab-cli cicd status/logs/report disclose bounded job state", async () => { + const cwd = await mkdtemp(path.join(os.tmpdir(), "hwlab-cli-cicd-")); + const submit = await captureCli(["cicd", "submit", "--kind", "ci-publish"], { + cwd, + spawnDetached: async () => ({ pid: 123457 }) + }); + const submitted = JSON.parse(submit.stdout); + const jobDir = path.join(cwd, ".state", "hwlab-cicd", "jobs", submitted.jobId); + await writeFile(path.join(jobDir, "stdout.log"), "build line 1\nbuild line 2\n", "utf8"); + await writeFile(path.join(jobDir, "stderr.log"), "warning line\n", "utf8"); + await writeFile( + path.join(jobDir, "report.json"), + `${JSON.stringify({ + sourceCommitId: "abc123", + artifactPublish: { + status: "published", + serviceCount: 1, + requiredServiceCount: 1, + publishedCount: 1, + timings: { durationMs: 42 } + }, + blockers: [] + })}\n`, + "utf8" + ); + + const status = await captureCli(["cicd", "status", submitted.jobId], { cwd }); + assert.equal(status.exitCode, 0, status.stderr); + assert.equal(JSON.parse(status.stdout).status, "submitted"); + + const logs = await captureCli(["cicd", "logs", submitted.jobId, "--tail-bytes", "12"], { cwd }); + assert.equal(logs.exitCode, 0, logs.stderr); + const logBody = JSON.parse(logs.stdout); + assert.equal(logBody.stdout.returnedBytes, 12); + assert.equal(logBody.stdout.truncated, true); + assert.ok(logBody.stdout.text.includes("line 2")); + + const report = await captureCli(["cicd", "report", submitted.jobId], { cwd }); + assert.equal(report.exitCode, 0, report.stderr); + const reportBody = JSON.parse(report.stdout); + assert.equal(reportBody.summary.kind, "ci-publish"); + assert.equal(reportBody.summary.status, "published"); + assert.equal(reportBody.summary.publishedCount, 1); + assert.equal(Object.hasOwn(reportBody, "report"), false); +}); + test("repo hwlab-cli m3 status uses Skill CLI and calls only HWLAB API /v1/m3/status", async () => { const calls = []; const result = await captureCli( @@ -104,4 +184,3 @@ test("repo hwlab-cli m3 io blocks direct hardware targets before request", async assert.equal(body.hwlabApi.redactedUrl, null); assert.equal(JSON.stringify(body).includes("patch-panel.hwlab-dev"), false); }); - diff --git a/tools/hwlab-cli/package.json b/tools/hwlab-cli/package.json index acf13a57..483ff8b1 100644 --- a/tools/hwlab-cli/package.json +++ b/tools/hwlab-cli/package.json @@ -7,7 +7,7 @@ "hwlab-cli": "./bin/hwlab-cli.mjs" }, "scripts": { - "check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node ../../scripts/l6-cli-web-smoke.mjs", + "check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs && node --check lib/cicd-jobs.mjs && node --check lib/cicd-job-worker.mjs && node --test lib/cli.test.mjs && node ../../scripts/l6-cli-web-smoke.mjs", "health": "node bin/hwlab-cli.mjs health" } }