diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 840e4ff8..41a6ef9c 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -44,7 +44,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P - `commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr` 是 host Codex 指挥官直管微服务 skeleton 入口。当前命令返回 `phase=source-contract`、service/API/state/bridge/prompt/trace/#20/#46/ClaudeQQ 审批边界、.state/commander/ 状态模型、dev 无 daemon smoke contract、dry-run 计划和 GPT-5.5 PR prompt 边界辅助 lint,不接 live bridge、不注入 prompt、不发送 ClaudeQQ。`approval request --dry-run` 会生成 200 字以内中文纯文本 ClaudeQQ 审批草案、`notification-path-unavailable` blocker 和授权后唯一可用的 `bun scripts/cli.ts microservice proxy claudeqq /api/push/text --method POST --body-json '' --raw` 命令;不得提示使用本机 ClaudeQQ skill、powershell 或本地 server。`prompt-lint` 支持 `--prompt-file` 与 `--stdin`,输出 `ok`、`missingClauses`、`riskLevel`、`suggestedPatchSnippet` 且不回显完整 prompt;它是 commander 辅助检查,不是业务 PR 门禁,也不改变 `codex submit` 默认行为。`plan`、`smoke` 与 `approval request` 必须带 `--dry-run`;缺少时返回 `error=dry-run-required`。长期规则见 `docs/reference/host-codex-commander.md`。 - `hwlab g14 monitor-prs [--once] [--dry-run] [--interval-seconds N] [--max-cycles N] [--timeout-seconds N]` 是当前 HWLAB G14 PR -> CI/CD -> DEV rollout 的一行式入口。普通调用创建 `.state/jobs/` 异步 job 并立刻返回 `job.id`、`statusCommand` 和 stdout/stderr 路径;后台 worker 每轮通过 UniDesk `gh pr list/preflight/merge` 监控 `pikasTech/HWLAB` base=`G14` 的 open PR,ready 时合并,然后通过 UniDesk `ssh G14:k3s` 观察 `hwlab-g14-ci-poll-`、Argo `hwlab-g14-dev` 和 DEV `/health/live`,直到 DEV `Synced/Healthy` 且 Deployment/StatefulSet ready;历史 `Completed` smoke/debug pod 不作为 rollout blocker。每次成功 DEV rollout 后,worker 会定位或创建 #7“指挥简报索引”中的北京日期每日简报 issue,并追加 CI/CD 耗时、CI/CD 关键指标、语义化上线 changelog、自动 diff 摘要、PipelineRun、GitOps revision 和 DEV 验证摘要;关键指标来自 G14 Tekton TaskRun results,固定包含 `lazy build reused: x/y`、reused services、rebuild services 和每个 service 的独立耗时/状态/backend,用于观察 lazy build 机制效果。语义化 changelog 优先从 PR body 的 `## 修改`/`## 变更`/`## Changelog` 等段落提取,diff 摘要只作为文件和统计证据保留,不替代 changelog。也可用 `hwlab g14 record-rollout --pr --source-commit ` 手动补记,手动补记同样会按 PipelineRun 采集 TaskRun 指标。状态指针按用途分离:长期监控只写 `.state/hwlab-g14/latest-monitor-job.json`,`--once` 写 `latest-once-job.json`,`--dry-run` 写 `latest-dry-run-job.json`,`--once --dry-run` 写 `latest-once-dry-run-job.json`,避免一次性收口覆盖持续监控入口。`--once --dry-run` 只做单轮监控和 merge plan,不写 GitHub、不等待 rollout。该命令禁止使用原生 `gh` 或手拼 GitHub 请求;如果 UniDesk `gh` 子命令字段或行为不够,必须先改进 `scripts/src/gh.ts` 后再使用。 - `hwlab g14 control-plane status|apply --lane v02 [--dry-run|--confirm]` 是 HWLAB `v0.2` 加法 lane 的受控 Tekton/Argo 控制面维护入口,只面向 G14 `/root/hwlab-v02`、branch `v0.2`、namespace `hwlab-ci` 和 Argo application `hwlab-g14-v02`;`status` 只读汇总 pipeline、RBAC/ServiceAccount、Argo、当前 commit PipelineRun 和遗留 v02 CronJob 清理状态;`apply` 先在 G14 workspace 快进并执行 render check,再经 `G14:k3s` server-side apply `tekton-v02/rbac.yaml`、`pipeline.yaml`、`argocd/project.yaml` 和 `argocd/application-v02.yaml`,confirmed apply 会删除遗留 v02 CronJob,但不会应用 runtime-v02 workload、Secret 或数据迁移。 -- `hwlab g14 control-plane trigger-current --lane v02 [--dry-run|--confirm]` 是 v02 标准手动触发入口:解析当前 `origin/v0.2` full SHA,创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun;读 Git 走 `git-mirror-http.devops-infra.svc.cluster.local`,GitOps promotion 写 `git-mirror-write.devops-infra.svc.cluster.local`;同名 PipelineRun 成功或运行中时拒绝重复触发,失败或不存在时才删除旧对象并重新创建;旧 `rerun-current` 只作为输入别名保留。 +- `hwlab g14 control-plane trigger-current --lane v02 [--dry-run|--confirm]` 是 v02 标准手动触发入口:解析当前 `origin/v0.2` full SHA,创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun;读 Git 走 `git-mirror-http.devops-infra.svc.cluster.local`,GitOps promotion 写 `git-mirror-write.devops-infra.svc.cluster.local`;同名 PipelineRun 成功或运行中时拒绝重复触发,失败或不存在时才删除旧对象并重新创建;创建 PipelineRun 前会读取 `devops-infra` mirror refs,若 `localV02` 未等于当前 source commit,则自动执行一次受控 manual `git-mirror sync` Job 并复核 ref,复核失败时停止触发,避免 Tekton `prepare-source` 已知失败;services 参数只包含 v02 runtime service matrix,`hwlab-cli` 是固定 repo 短连接源码工具,不进入 PipelineRun service build;`--dry-run` 只报告是否会 pre-sync,不创建 Job;旧 `rerun-current` 只作为输入别名保留。 - `hwlab g14 control-plane runtime-migration --lane v02 [--dry-run|--allow-live-db-read --dry-run|--confirm]` 只通过 `hwlab-v02` namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内 repo-owned migration CLI 执行;不读取或打印 Secret 值、不触碰 PROD、不绕到手工 `psql`。 - `hwlab g14 control-plane cleanup-runs --lane v02|g14|all [--min-age-minutes N] [--limit N] [--dry-run|--confirm]` 是完成态 PipelineRun 工作区 retention 入口;真实清理只删除已完成 PipelineRun,让 Tekton/local-path 回收临时 PVC,不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。 - `hwlab g14 control-plane cleanup-released-pvs --lane all [--limit N] [--dry-run|--confirm]` 是 local-path 未自动回收后的补充 retention 入口;只列并删除 `Released`、`local-path`、`Delete`、`claimNamespace=hwlab-ci` 且 claim 名称形如 Tekton 临时 `pvc-*` 的 PV。 diff --git a/docs/reference/g14.md b/docs/reference/g14.md index 4364d6f9..3523463c 100644 --- a/docs/reference/g14.md +++ b/docs/reference/g14.md @@ -65,6 +65,8 @@ Master-side FRP server maintenance for HWLAB public ports is documented in `docs The `v0.2` CI/CD integration must be additive: add a `v0.2` source watcher, GitOps desired-state lane, Argo CD Application, namespace resources, artifact catalog, and `deploy.json` environment only when they target `v0.2`/`hwlab-v02` explicitly. Do not retarget the existing `G14` poller, DEV/PROD Argo Applications, DEV/PROD runtime paths, or existing namespace resources to bootstrap `v0.2`. +The `devops-infra` git mirror remains manual and CLI-controlled, not CronJob-driven. The standard `v0.2` CI/CD trigger is `bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm`; this command must check the mirror's `localV02` ref before creating the PipelineRun, run one bounded manual `git-mirror sync` Job when the mirror is stale, and only continue after the mirror ref matches the current source commit. Use `hwlab g14 git-mirror sync --confirm` directly only for explicit mirror maintenance or diagnosis. + Do not turn `v0.2` expansion governance into a stack of broad compatibility gates. The stable control points are branch, GitOps branch, namespace, runtime path, Argo Application, FRP ports and generated-output ownership. Legacy DEV/D601/main preflights that block the `v0.2` lane should be removed from that lane, not patched with fallback or legacy modes. Naming, RBAC scope, cleanup policy, resource quota and rollback order are design decisions or runbook entries unless they protect a concrete high-value risk that cannot be enforced by the fixed boundaries above. ## Node-Local VPN Proxy diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index 7fb752c8..afd514d5 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -1,4 +1,4 @@ -import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14"; +import { gitMirrorFlushJobManifest, gitMirrorSyncJobManifest, gitMirrorV02SyncRequirement, hwlabG14MonitorStateFileName, parseGitMirrorStatusRefs, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets, v02PipelineServiceIds } from "./src/hwlab-g14"; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); @@ -40,6 +40,33 @@ const flushPodSpec = record(flushTemplate.spec); const flushContainer = record(Array.isArray(flushPodSpec.containers) ? flushPodSpec.containers[0] : null); assertCondition(JSON.stringify(flushContainer.command) === JSON.stringify(["/script/flush.sh"]), "git mirror flush Job must run the flush script", gitMirrorFlushJob); +const gitMirrorStatusRaw = [ + 'lastSync={"ok":true}', + "lastWrite=", + "lastFlush=", + 'refs={"refs":{"localV02":"0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc","localG14":"1111111111111111111111111111111111111111","localGitops":"2222222222222222222222222222222222222222","githubGitops":"3333333333333333333333333333333333333333"},"pendingFlush":true}', +].join("\n"); +const parsedGitMirrorRefs = parseGitMirrorStatusRefs(gitMirrorStatusRaw); +assertCondition( + parsedGitMirrorRefs.refs.localV02 === "0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", + "git mirror status parser must expose local v0.2 ref for trigger pre-sync", + parsedGitMirrorRefs, +); +assertCondition(parsedGitMirrorRefs.pendingFlush === true, "git mirror status parser must preserve pending flush signal", parsedGitMirrorRefs); +assertCondition( + gitMirrorV02SyncRequirement("0cf79ecc9d8a784b7712b0b3a58d5e39025ba0dc", gitMirrorStatusRaw).required === false, + "trigger-current must not sync mirror when local v0.2 already matches source commit", +); +assertCondition( + gitMirrorV02SyncRequirement("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", gitMirrorStatusRaw).required === true, + "trigger-current must sync mirror before creating PipelineRun when local v0.2 is stale", +); +assertCondition( + !v02PipelineServiceIds().includes("hwlab-cli"), + "v0.2 PipelineRun service matrix must not build hwlab-cli because cli is short-connection source tool", + v02PipelineServiceIds(), +); + const prBody = [ "## 背景", "", @@ -145,6 +172,8 @@ console.log(JSON.stringify({ "once dry-run jobs have a distinct diagnostic state file", "git mirror sync is a manual devops-infra Job, not a CronJob", "git mirror flush is a manual devops-infra Job, not a CronJob", + "trigger-current can decide whether v0.2 git mirror pre-sync is required", + "v0.2 PipelineRun service matrix excludes hwlab-cli", "rollout brief includes natural-language changelog before automatic diff summary", "semantic changelog extracts Chinese summary sections", "rollout brief includes lazy-build reused/rebuild metrics and service durations", diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index 21f33c55..efb48db2 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -40,9 +40,13 @@ const V02_SERVICE_IDS = [ "hwlab-device-pod", "hwlab-gateway", "hwlab-edge-proxy", - "hwlab-cli", "hwlab-agent-skills", ]; + +export function v02PipelineServiceIds(): string[] { + return [...V02_SERVICE_IDS]; +} + const G14_CI_TOOLS_IMAGE_REPO = "127.0.0.1:5000/hwlab/hwlab-ci-node-tools"; const G14_CI_TOOLS_BASE_TAG = "node22-alpine-v1"; const DEFAULT_INTERVAL_SECONDS = 600; @@ -313,6 +317,10 @@ function record(value: unknown): Record { return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; } +function stringOrNull(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value.trim() : null; +} + function nested(value: unknown, keys: string[]): unknown { let current = value; for (const key of keys) current = record(current)[key]; @@ -438,6 +446,11 @@ function commandErrorSummary(result: CommandJsonResult): string { return text.slice(0, 4000); } +function tailText(value: unknown, maxBytes = 2000): string { + const text = String(value ?? "").trim(); + return text.length <= maxBytes ? text : text.slice(-maxBytes); +} + function listCleanupPipelineRuns(options: G14ControlPlaneOptions): Record[] { const result = g14K3s([ "kubectl", @@ -932,6 +945,7 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record return manifest; } +export function parseGitMirrorStatusRefs(raw: string): { refs: Record; pendingFlush: boolean | null } { + const line = raw.split(/\r?\n/u).find((item) => item.startsWith("refs=")); + if (line === undefined) return { refs: {}, pendingFlush: null }; + try { + const parsed = record(JSON.parse(line.slice("refs=".length)) as unknown); + const refs = record(parsed.refs); + return { + refs: { + localV02: stringOrNull(refs.localV02), + localG14: stringOrNull(refs.localG14), + localGitops: stringOrNull(refs.localGitops), + githubGitops: stringOrNull(refs.githubGitops), + }, + pendingFlush: typeof parsed.pendingFlush === "boolean" ? parsed.pendingFlush : null, + }; + } catch { + return { refs: {}, pendingFlush: null }; + } +} + +export function gitMirrorV02SyncRequirement(sourceCommit: string, rawStatus: string): Record { + const parsed = parseGitMirrorStatusRefs(rawStatus); + const localV02 = parsed.refs.localV02 ?? null; + return { + required: localV02 !== sourceCommit, + sourceCommit, + localV02, + pendingFlush: parsed.pendingFlush, + refs: parsed.refs, + reason: localV02 === sourceCommit ? "local-v02-current" : localV02 === null ? "local-v02-unresolved" : "local-v02-stale", + }; +} + +function gitMirrorStatusCacheRaw(status: Record): string { + return String(nested(status, ["cache", "raw"]) ?? ""); +} + +function compactGitMirrorStatus(status: Record, sourceCommit: string): Record { + const requirement = gitMirrorV02SyncRequirement(sourceCommit, gitMirrorStatusCacheRaw(status)); + return { + ok: status.ok === true, + readUrl: status.readUrl ?? V02_GIT_READ_URL, + writeUrl: status.writeUrl ?? V02_GIT_WRITE_URL, + legacyCronJobExists: nested(status, ["legacyCronJob", "exists"]) === true, + required: requirement.required, + sourceCommit, + localV02: requirement.localV02 ?? null, + pendingFlush: requirement.pendingFlush ?? null, + reason: requirement.reason, + cacheOk: nested(status, ["cache", "ok"]) === true, + cacheStderr: tailText(nested(status, ["cache", "stderr"]), 1000), + resourcesOk: nested(status, ["resources", "ok"]) === true, + }; +} + +function compactGitMirrorSync(sync: Record): Record { + return { + ok: sync.ok === true, + command: sync.command ?? "hwlab g14 git-mirror sync", + mode: sync.mode ?? null, + namespace: sync.namespace ?? GIT_MIRROR_NAMESPACE, + jobName: sync.jobName ?? null, + exitCode: nested(sync, ["result", "exitCode"]) ?? null, + stdoutTail: tailText(nested(sync, ["result", "stdout"]), 3000), + stderrTail: tailText(nested(sync, ["result", "stderr"]), 3000), + }; +} + +function preSyncV02GitMirror(sourceCommit: string, options: Pick): Record { + const before = runGitMirrorStatus(); + const beforeSummary = compactGitMirrorStatus(before, sourceCommit); + if (options.dryRun) { + return { + ok: true, + mode: "dry-run", + sourceCommit, + before: beforeSummary, + action: beforeSummary.required === true ? "would-sync-before-trigger" : "already-current", + next: beforeSummary.required === true ? { syncAndTrigger: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" } : undefined, + }; + } + if (beforeSummary.required !== true) { + return { + ok: true, + mode: "already-current", + sourceCommit, + before: beforeSummary, + }; + } + const sync = runGitMirrorSync({ + action: "sync", + confirm: true, + dryRun: false, + timeoutSeconds: options.timeoutSeconds, + }); + const after = runGitMirrorStatus(); + const afterSummary = compactGitMirrorStatus(after, sourceCommit); + const ok = sync.ok === true && afterSummary.required === false; + return { + ok, + mode: "auto-sync-before-trigger", + sourceCommit, + before: beforeSummary, + sync: compactGitMirrorSync(sync), + after: afterSummary, + degradedReason: ok ? undefined : "git-mirror-local-v02-not-current-after-sync", + }; +} + function runGitMirrorStatus(): Record { const resources = g14K3s([ "kubectl",