diff --git a/docs/reference/cli.md b/docs/reference/cli.md index ad1a7b5d..75bed729 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -43,7 +43,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 DEV/PROD 滚动、P - `artifact-registry plan|render|status|health|install|deploy-backend-core|deploy-service` 管理 D601 host-managed CNCF Distribution registry 的声明、安装、只读检查和 pull-only artifact CD。该 registry 固定为 D601 loopback `127.0.0.1:5000`,由 systemd + Docker Compose 管理,位于 native k3s 故障域外;`deploy-service` 只拉取 CI 已发布的 commit-pinned 镜像、retag/recreate 或导入 native k3s,并做 live commit 验证,不构建 runtime source。`deploy-backend-core` 是 deprecated 兼容名,标准 backend-core prod CD 入口是 `deploy apply --env prod --service backend-core`。长期规则见 `docs/reference/artifact-registry.md`。 - `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|trigger-current|runtime-migration --lane v02 [--dry-run|--confirm]`、`cleanup-runs --lane v02|g14|all [--dry-run|--confirm]` 和 `cleanup-released-pvs --lane all [--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` 只读汇总 `hwlab-v02-ci-image-publish`、v02 RBAC/ServiceAccount、`hwlab-g14-v02`、当前 commit PipelineRun 和遗留 `hwlab-v02-branch-poller`/`hwlab-v02-control-plane-reconciler` CronJob 清理状态。`apply` 先在 G14 `/root/hwlab-v02` 快进并执行 `scripts/g14-gitops-render.mjs --lane v02 --check`,再只通过 UniDesk `G14:k3s` route server-side apply `tekton-v02/rbac.yaml`、`pipeline.yaml`、`argocd/project.yaml` 和 `argocd/application-v02.yaml`;默认 dry-run,真实写入必须加 `--confirm`,confirmed apply 会删除遗留 v02 CronJob,但不会应用 runtime-v02 workload、Secret 或数据迁移。`trigger-current` 是 v02 标准手动触发入口:它解析当前 `origin/v0.2` full SHA,直接创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun manifest,读 Git 走 `git-mirror-http.devops-infra.svc.cluster.local`,写 GitOps 仍走 `hwlab-git-ssh`;同名 PipelineRun 成功或运行中时拒绝重复触发,失败或不存在时才删除旧对象并重新创建,默认 dry-run 返回完整 manifest,真实动作必须加 `--confirm`。旧 `rerun-current` 仅作为输入别名保留,长期文档和人工操作必须使用 `trigger-current`。`runtime-migration` 只通过 `hwlab-v02` namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内的 `bun cmd/hwlab-cloud-api/migrate.ts` 执行 repo-owned migration CLI:默认 `--dry-run` 是 source-only `--check`,`--allow-live-db-read --dry-run` 才做 redacted live DB verification,`--confirm` 才执行 DEV-only `--apply --confirm-dev --confirmed-non-production`;该入口不读取或打印 Secret 值、不触碰 PROD、不绕到手工 `psql`。`cleanup-runs --lane v02|g14|all [--min-age-minutes N] [--limit N]` 是完成态 PipelineRun 工作区 retention 入口,默认 dry-run 通过 `kubectl get -o jsonpath` 列候选 PipelineRun 与 ownerReference 关联的 hwlab-ci 临时 PVC;真实清理必须加 `--confirm`,只删除已完成 PipelineRun,让 Tekton/local-path 按对象关系回收临时 PVC,不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。`cleanup-released-pvs --lane all [--limit N]` 是 local-path 未自动回收后的补充 retention 入口,只列并删除 `Released`、`local-path`、`Delete`、`claimNamespace=hwlab-ci` 且 claim 名称形如 Tekton 临时 `pvc-*` 的 PV;真实清理必须加 `--confirm`,不触碰 registry storage、业务 namespace PVC、Secret、runtime workload 或 GitOps desired state。`hwlab g14 tools-image status|build --name ci-node-tools --tag [--dockerfile deploy/ci/hwlab-ci-node-tools.Dockerfile] [--dry-run|--confirm]` 是 G14 固定 HWLAB CI tools image 的受控 host build/push 入口:镜像内容必须来自 HWLAB repo 内 Dockerfile,`build` 默认 dry-run,真实写入必须加 `--confirm`,构建和 push 只发生在 G14 host 与本地 registry,不在 master server 构建,也不把 `apk add`/runtime install 塞进 Tekton PipelineRun。 +- `hwlab g14 control-plane status|apply|trigger-current|runtime-migration --lane v02 [--dry-run|--confirm]`、`cleanup-runs --lane v02|g14|all [--dry-run|--confirm]` 和 `cleanup-released-pvs --lane all [--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` 只读汇总 `hwlab-v02-ci-image-publish`、v02 RBAC/ServiceAccount、`hwlab-g14-v02`、当前 commit PipelineRun 和遗留 `hwlab-v02-branch-poller`/`hwlab-v02-control-plane-reconciler` CronJob 清理状态。`apply` 先在 G14 `/root/hwlab-v02` 快进并执行 `scripts/g14-gitops-render.mjs --lane v02 --check`,再只通过 UniDesk `G14:k3s` route server-side apply `tekton-v02/rbac.yaml`、`pipeline.yaml`、`argocd/project.yaml` 和 `argocd/application-v02.yaml`;默认 dry-run,真实写入必须加 `--confirm`,confirmed apply 会删除遗留 v02 CronJob,但不会应用 runtime-v02 workload、Secret 或数据迁移。`trigger-current` 是 v02 标准手动触发入口:它解析当前 `origin/v0.2` full SHA,直接创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun manifest,读 Git 走 `git-mirror-http.devops-infra.svc.cluster.local`,写 GitOps 仍走 `hwlab-git-ssh`;同名 PipelineRun 成功或运行中时拒绝重复触发,失败或不存在时才删除旧对象并重新创建,默认 dry-run 返回完整 manifest,真实动作必须加 `--confirm`。旧 `rerun-current` 仅作为输入别名保留,长期文档和人工操作必须使用 `trigger-current`。`runtime-migration` 只通过 `hwlab-v02` namespace 当前 `deployment/hwlab-cloud-api -c hwlab-cloud-api` 内的 `bun cmd/hwlab-cloud-api/migrate.ts` 执行 repo-owned migration CLI:默认 `--dry-run` 是 source-only `--check`,`--allow-live-db-read --dry-run` 才做 redacted live DB verification,`--confirm` 才执行 DEV-only `--apply --confirm-dev --confirmed-non-production`;该入口不读取或打印 Secret 值、不触碰 PROD、不绕到手工 `psql`。`cleanup-runs --lane v02|g14|all [--min-age-minutes N] [--limit N]` 是完成态 PipelineRun 工作区 retention 入口,默认 dry-run 通过 `kubectl get -o jsonpath` 列候选 PipelineRun 与 ownerReference 关联的 hwlab-ci 临时 PVC;真实清理必须加 `--confirm`,只删除已完成 PipelineRun,让 Tekton/local-path 按对象关系回收临时 PVC,不触碰 registry storage、业务 PVC、Secret、runtime workload 或 GitOps desired state。`cleanup-released-pvs --lane all [--limit N]` 是 local-path 未自动回收后的补充 retention 入口,只列并删除 `Released`、`local-path`、`Delete`、`claimNamespace=hwlab-ci` 且 claim 名称形如 Tekton 临时 `pvc-*` 的 PV;真实清理必须加 `--confirm`,不触碰 registry storage、业务 namespace PVC、Secret、runtime workload 或 GitOps desired state。`hwlab g14 git-mirror status|apply|sync [--dry-run|--confirm]` 是 `devops-infra` git mirror 的受控维护入口:`apply` 从 G14 `/root/hwlab-v02` 渲染并 server-side apply `devops-infra/git-mirror.yaml`,同时删除遗留 `git-mirror-hwlab-sync` CronJob;`sync` 创建一次性 manual Job,等待完成并返回 sync 日志、published refs 和 cache status;mirror 不再设置 CronJob,CI 触发前需要时手动 sync。`hwlab g14 tools-image status|build --name ci-node-tools --tag [--dockerfile deploy/ci/hwlab-ci-node-tools.Dockerfile] [--dry-run|--confirm]` 是 G14 固定 HWLAB CI tools image 的受控 host build/push 入口:镜像内容必须来自 HWLAB repo 内 Dockerfile,`build` 默认 dry-run,真实写入必须加 `--confirm`,构建和 push 只发生在 G14 host 与本地 registry,不在 master server 构建,也不把 `apk add`/runtime install 塞进 Tekton PipelineRun。 - `ssh gh:/owner/repo ...` 把 GitHub issue/PR 映射成只读/受控写入的虚拟文本目录,适合日报、PR 正文和 issue 正文的小补丁维护:`ssh gh:/pikasTech/HWLAB ls` 展示 `pr/` 与 `issue/`,`ssh gh:/pikasTech/HWLAB/pr ls [--limit N] [--full]` 和 `ssh gh:/pikasTech/HWLAB/issue ls [--limit N] [--full]` 展示条目状态、楼层数、正文长度和标题,`ssh gh:/pikasTech/HWLAB/pr/507 ls` 展示单个 PR 的一楼正文文件,`ssh gh:/pikasTech/HWLAB/505/1 cat|rg|patch-apply` 兼容旧式 issue/PR number route。`patch-apply` 使用 UniDesk 默认 apply-patch v2 的虚拟文件 executor,把正文一楼映射为 `body.md`,写回仍走 `bun scripts/cli.ts gh issue/pr update` 的 guard/concurrency 规则;`rm` 对正文一楼结构化拒绝,避免误删 issue/PR 正文。大正文读取必须展开 UniDesk gh dump 文件,否则 `cat/rg/patch-apply` 会误读为空,这是 `gh:` 虚拟文件接口的 P0 可见性契约。 - `hwlab cd status|audit|preflight|apply --env dev [--dry-run]` 是旧 D601 HWLAB DEV CD 指挥侧 wrapper,仅用于显式 legacy 诊断和迁移对照。默认通过 UniDesk provider `host.ssh` 进入 D601,再调用 HWLAB repo-owned `scripts/dev-cd-apply.mjs`,不内嵌发布 kubectl 逻辑:`status` 汇总固定 CD mirror、Git clean/main/origin-main、`deploy/deploy.json`/artifact catalog/report、D601 native k3s guard 和 CD Lease lock,并用 `scripts/dev-cd-apply.mjs --status --skip-live-verify` 取得 target/promotion 摘要;`audit` 在 k3s/CD 恢复后做只读健康审计,返回有界 JSON 的 blocker 分类、D601 guard/node、SecretRef 存在性、registry 可达性、Lease phase/holder/staleness、deploy.json 与 artifact/workload image 收敛、current Deployment image/revision/rollout、16666/16667 public health commit/readiness 和 DB/runtime durability 摘要;`preflight` 进一步检查必需 SecretRef 对象/键存在性并运行 HWLAB `scripts/dev-cd-apply.mjs --dry-run --skip-live-verify` 受控事务摘要。完整远端 stdout/stderr 写入 D601 `~/.state/unidesk-hwlab-cd//` 和本地 `.state/hwlab-cd//` task dump,stdout 只返回有界摘要。默认 HWLAB CD repo 是 `/home/ubuntu/hwlab_cd`,`/home/ubuntu/hwlab` runner 历史目录不得作为发布真相。wrapper 强制 `KUBECONFIG=/etc/rancher/k3s/k3s.yaml` 并只以这个显式目标作为 gate;显式目标出现 `docker-desktop`、`desktop-control-plane` 或 `127.0.0.1:11700` 信号会结构化拒绝,audit/preflight/apply --dry-run 都必须观察到 node `d601`。真实 apply 只暴露 `scripts/dev-cd-apply.mjs --apply --confirm-dev --confirmed-non-production --write-report` 命令形状并标注 host-commander-only,本 runner 不执行 live apply、rollout、Lease mutation 或 DEV deploy apply。长期规则见 `docs/reference/hwlab.md`。 - `gh auth status [--repo owner/name]` 探测 GitHub 操作前置条件并输出脱敏 JSON:是否存在 `gh` binary、是否存在 `GH_TOKEN`/`GITHUB_TOKEN` 或可用 `gh auth token` fallback、REST API 是否可达、目标 repo 是否可见、issue 是否可读。degraded reason 必须归类为 `missing-binary`、`missing-token`、`auth-failed`、`github-transient`、`network-proxy-failed`、`permission-denied`、`repo-not-found`、`repo-forbidden`、`issue-not-found`、`pr-not-found`、`scope-insufficient`、`validation-failed`、`invalid-response` 或 `unsupported-command`,不得打印 token;失败对象必须包含 `runnerDisposition=infra-blocked|business-failed`,runner 应优先用该字段分流。`github-transient` 表示 GitHub DNS/API 连接在收到 HTTP 状态前失败,输出应带 `retryable=true` 或等价 commander action;这不是缺 token、认证失败、权限不足或 PR 语义失败。 diff --git a/scripts/hwlab-g14-contract-test.ts b/scripts/hwlab-g14-contract-test.ts index f89cdc92..d2dd7ac2 100644 --- a/scripts/hwlab-g14-contract-test.ts +++ b/scripts/hwlab-g14-contract-test.ts @@ -1,9 +1,13 @@ -import { hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14"; +import { gitMirrorSyncJobManifest, hwlabG14MonitorStateFileName, parsePipelineTaskRunMetrics, rolloutRecordBody, semanticChangelogBullets } from "./src/hwlab-g14"; function assertCondition(condition: unknown, message: string, detail: unknown = {}): void { if (!condition) throw new Error(`${message}: ${JSON.stringify(detail)}`); } +function record(value: unknown): Record { + return typeof value === "object" && value !== null && !Array.isArray(value) ? value as Record : {}; +} + assertCondition( hwlabG14MonitorStateFileName({ once: false, dryRun: false }) === "latest-monitor-job.json", "long-running monitor should own latest-monitor-job.json", @@ -21,6 +25,12 @@ assertCondition( "once dry-runs need a distinct pointer for low-noise diagnostics", ); +const gitMirrorJob = gitMirrorSyncJobManifest("git-mirror-hwlab-sync-manual-test"); +assertCondition(gitMirrorJob.kind === "Job", "git mirror sync must be a manual Job, not a CronJob", gitMirrorJob); +assertCondition(record(gitMirrorJob.metadata).namespace === "devops-infra", "git mirror sync Job must target devops-infra", gitMirrorJob); +assertCondition(record(record(gitMirrorJob.metadata).labels)["hwlab.pikastech.local/trigger"] === "manual-cli", "git mirror sync Job must be labeled as manual CLI triggered", gitMirrorJob); +assertCondition(record(gitMirrorJob.spec).backoffLimit === 0, "git mirror sync Job should fail visibly instead of retrying in the background", gitMirrorJob); + const prBody = [ "## 背景", "", @@ -124,6 +134,7 @@ console.log(JSON.stringify({ "once jobs do not overwrite long-running monitor state", "dry-run jobs do not overwrite live monitor state", "once dry-run jobs have a distinct diagnostic state file", + "git mirror sync is a manual devops-infra Job, not a CronJob", "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/help.ts b/scripts/src/help.ts index 5abc490b..092a2aab 100644 --- a/scripts/src/help.ts +++ b/scripts/src/help.ts @@ -57,7 +57,7 @@ export function rootHelp(): unknown { { command: "auth-broker contract|health --dry-run|credential-request --dry-run|pr-preflight --dry-run", description: "Inspect the P0 Rust auth broker and CLI adapter contract without reading token values, writing GitHub, or starting services." }, { command: "gh preflight|auth|issue|pr", description: "Run safe GitHub issue and PR CRUD/lifecycle operations through REST with body-file update replace/append, comment delete, token diagnostics, PR closeout preflight, hard delete unsupported, and guarded PR merge." }, { command: "commander contract|plan --dry-run|smoke --dry-run|approval request --dry-run|prompt-lint --kind gpt55-pr", description: "Host Codex commander skeleton contract, no-daemon smoke plan, dry-run approval preview, and advisory GPT-5.5 PR prompt boundary lint without live bridges, message sends, or submit gating." }, - { command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, and CI workspace retention actions, or build/status fixed HWLAB CI tools images through UniDesk G14 routes." }, + { command: "hwlab g14 monitor-prs | hwlab g14 control-plane status|apply|trigger-current|runtime-migration|cleanup-runs|cleanup-released-pvs | hwlab g14 git-mirror status|apply|sync | hwlab g14 tools-image status|build", description: "Start the G14 PR monitor, run bounded v0.2 Tekton/Argo control-plane, manual PipelineRun trigger, runtime migration, CI workspace retention, manual devops-infra git mirror maintenance, or fixed HWLAB CI tools image actions through UniDesk G14 routes." }, { command: "hwlab cd audit --env dev | hwlab cd status --env dev | hwlab cd apply --env dev --dry-run", description: "Legacy D601 HWLAB DEV CD wrapper kept for explicit old-path diagnostics; current HWLAB rollout uses G14 GitOps." }, { command: "code-agent-sandbox", description: "Independent Code Agent Sandbox service skeleton for adapter, mode, and credential-boundary diagnostics." }, { command: "schedule list|get|runs|run|retry-run|delete", description: "Manage backend-core scheduled tasks and run history; schedule run supports --wait-ms N and retry-run reuses the failed run's schedule." }, diff --git a/scripts/src/hwlab-g14.ts b/scripts/src/hwlab-g14.ts index 19648633..ae92ad59 100644 --- a/scripts/src/hwlab-g14.ts +++ b/scripts/src/hwlab-g14.ts @@ -27,6 +27,10 @@ const V02_CATALOG_PATH = "deploy/artifact-catalog.v02.json"; const V02_RUNTIME_PATH = "deploy/gitops/g14/runtime-v02"; const V02_REGISTRY_PREFIX = "127.0.0.1:5000/hwlab"; const V02_BASE_IMAGE = "127.0.0.1:5000/hwlab/hwlab-node20-base:20-bookworm-slim"; +const GIT_MIRROR_NAMESPACE = "devops-infra"; +const GIT_MIRROR_MANIFEST_FIELD_MANAGER = "unidesk-hwlab-git-mirror"; +const GIT_MIRROR_SYNC_JOB_PREFIX = "git-mirror-hwlab-sync-manual"; +const GIT_MIRROR_LEGACY_CRONJOB = "git-mirror-hwlab-sync"; const V02_SERVICE_IDS = [ "hwlab-cloud-api", "hwlab-cloud-web", @@ -87,6 +91,13 @@ interface G14ToolsImageOptions { timeoutSeconds: number; } +interface G14GitMirrorOptions { + action: "status" | "apply" | "sync"; + dryRun: boolean; + confirm: boolean; + timeoutSeconds: number; +} + interface CommandJsonResult { ok: boolean; command: string[]; @@ -244,6 +255,22 @@ function parseToolsImageOptions(args: string[]): G14ToolsImageOptions { }; } +function parseGitMirrorOptions(args: string[]): G14GitMirrorOptions { + const [actionRaw] = args; + if (actionRaw !== "status" && actionRaw !== "apply" && actionRaw !== "sync") { + throw new Error("git-mirror usage: status|apply|sync [--dry-run|--confirm]"); + } + const confirm = args.includes("--confirm"); + const explicitDryRun = args.includes("--dry-run"); + if (confirm && explicitDryRun) throw new Error("git-mirror accepts only one of --confirm or --dry-run"); + return { + action: actionRaw, + confirm, + dryRun: actionRaw === "status" ? true : explicitDryRun || !confirm, + timeoutSeconds: positiveIntegerOption(args, "--timeout-seconds", actionRaw === "sync" ? 300 : 120, 900), + }; +} + function positiveIntegerOption(args: string[], name: string, defaultValue: number, maxValue: number): number { const index = args.indexOf(name); if (index === -1) return defaultValue; @@ -946,6 +973,267 @@ function runV02ControlPlane(options: G14ControlPlaneOptions): Record { + return { + apiVersion: "batch/v1", + kind: "Job", + metadata: { + name, + namespace: GIT_MIRROR_NAMESPACE, + labels: { + "app.kubernetes.io/name": "git-mirror", + "app.kubernetes.io/part-of": "devops-infra", + "app.kubernetes.io/component": "sync-controller", + "hwlab.pikastech.local/trigger": "manual-cli", + }, + }, + spec: { + backoffLimit: 0, + activeDeadlineSeconds: 300, + ttlSecondsAfterFinished: 3600, + template: { + metadata: { + labels: { + "app.kubernetes.io/name": "git-mirror", + "app.kubernetes.io/part-of": "devops-infra", + "app.kubernetes.io/component": "sync-controller", + "hwlab.pikastech.local/trigger": "manual-cli", + }, + }, + spec: { + restartPolicy: "Never", + hostNetwork: true, + dnsPolicy: "ClusterFirstWithHostNet", + volumes: [ + { name: "cache", persistentVolumeClaim: { claimName: "git-mirror-cache" } }, + { name: "git-ssh", secret: { secretName: "git-mirror-github-ssh", defaultMode: 0o400 } }, + { name: "script", configMap: { name: "git-mirror-sync-script", defaultMode: 0o755 } }, + ], + containers: [{ + name: "sync", + image: "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1", + imagePullPolicy: "IfNotPresent", + command: ["/script/sync.sh"], + volumeMounts: [ + { name: "cache", mountPath: "/cache" }, + { name: "git-ssh", mountPath: "/git-ssh", readOnly: true }, + { name: "script", mountPath: "/script", readOnly: true }, + ], + }], + }, + }, + }, + }; +} + +function runGitMirrorStatus(): Record { + const resources = g14K3s([ + "kubectl", + "get", + "deploy,svc,pvc,cm", + "-n", + GIT_MIRROR_NAMESPACE, + "-l", + "app.kubernetes.io/name=git-mirror", + "-o", + "name", + ], 60_000); + const legacyCronJob = g14K3s([ + "kubectl", + "get", + "cronjob", + "-n", + GIT_MIRROR_NAMESPACE, + GIT_MIRROR_LEGACY_CRONJOB, + "--ignore-not-found", + "-o", + "name", + ], 60_000); + const jobs = g14K3s([ + "kubectl", + "get", + "job", + "-n", + GIT_MIRROR_NAMESPACE, + "-l", + "app.kubernetes.io/name=git-mirror,app.kubernetes.io/component=sync-controller", + "--sort-by=.metadata.creationTimestamp", + "-o", + "custom-columns=NAME:.metadata.name,SUCCEEDED:.status.succeeded,FAILED:.status.failed,START:.status.startTime,COMPLETION:.status.completionTime", + "--no-headers", + ], 60_000); + const cache = g14K3s([ + "kubectl", + "exec", + "-n", + GIT_MIRROR_NAMESPACE, + "deploy/git-mirror-http", + "--", + "sh", + "-lc", + "cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf '\\n'; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2 refs/heads/v0.2-gitops refs/heads/G14 2>/dev/null || true", + ], 60_000); + return { + ok: isCommandSuccess(resources), + command: "hwlab g14 git-mirror status", + namespace: GIT_MIRROR_NAMESPACE, + readUrl: V02_GIT_READ_URL, + resources: { + ok: isCommandSuccess(resources), + names: statusText(resources).split(/\r?\n/u).map((line) => line.trim()).filter(Boolean), + stderr: resources.stderr.trim().slice(0, 2000), + }, + legacyCronJob: { + ok: isCommandSuccess(legacyCronJob), + exists: statusText(legacyCronJob).trim().length > 0, + name: statusText(legacyCronJob).trim() || null, + cleanupCommand: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm", + }, + jobs: { + ok: isCommandSuccess(jobs), + raw: statusText(jobs), + stderr: jobs.stderr.trim().slice(0, 2000), + }, + cache: { + ok: isCommandSuccess(cache), + raw: statusText(cache), + stderr: cache.stderr.trim().slice(0, 2000), + }, + next: { + apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm", + sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm", + }, + }; +} + +function runGitMirrorApply(options: G14GitMirrorOptions): Record { + const sourceCommit = getV02Head(); + if (sourceCommit === null) { + return { ok: false, command: "hwlab g14 git-mirror apply", degradedReason: "v02-head-unresolved", workspace: V02_WORKSPACE }; + } + const renderCheck = runV02RenderCheck(sourceCommit); + if (!isCommandSuccess(renderCheck)) { + return { + ok: false, + command: "hwlab g14 git-mirror apply", + phase: "source-render-check", + sourceCommit, + renderCheck, + }; + } + const apply = applyGitMirrorManifestFile(sourceCommit, options.dryRun, options.timeoutSeconds); + const cleanupLegacyCronJob = isCommandSuccess(apply) + ? deleteLegacyGitMirrorCronJob(options.dryRun) + : { + ok: false, + command: [], + exitCode: null, + stdout: "", + stderr: "skipped because apply failed", + parsed: null, + }; + return { + ok: isCommandSuccess(apply) && isCommandSuccess(cleanupLegacyCronJob), + command: "hwlab g14 git-mirror apply", + mode: options.dryRun ? "dry-run" : "confirmed-apply", + sourceCommit, + manifest: `${v02RenderDir(sourceCommit)}/devops-infra/git-mirror.yaml`, + renderCheck: commandData(renderCheck), + apply, + cleanupLegacyCronJob, + status: runGitMirrorStatus(), + next: options.dryRun + ? { apply: "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm" } + : { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" }, + }; +} + +function runGitMirrorSync(options: G14GitMirrorOptions): Record { + const jobName = gitMirrorSyncJobName(); + const manifest = gitMirrorSyncJobManifest(jobName); + const manifestB64 = Buffer.from(JSON.stringify(manifest), "utf8").toString("base64"); + const script = [ + "set -eu", + `job=${shellQuote(jobName)}`, + `manifest_b64=${shellQuote(manifestB64)}`, + "manifest_path=\"/tmp/$job.json\"", + "printf '%s' \"$manifest_b64\" | base64 -d > \"$manifest_path\"", + options.dryRun + ? "kubectl create --dry-run=server -f \"$manifest_path\" -o name" + : [ + `kubectl delete job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" --ignore-not-found=true >/dev/null`, + "kubectl create -f \"$manifest_path\"", + `deadline=$(( $(date +%s) + ${options.timeoutSeconds} ))`, + "while :; do", + ` status=$(kubectl get job -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "$job" -o jsonpath='{.status.succeeded} {.status.failed}' 2>/dev/null || true)`, + " succeeded=$(printf '%s\\n' \"$status\" | awk '{print $1}')", + " failed=$(printf '%s\\n' \"$status\" | awk '{print $2}')", + " if [ \"${succeeded:-0}\" = \"1\" ]; then break; fi", + " if [ \"${failed:-0}\" != \"\" ] && [ \"${failed:-0}\" != \"0\" ]; then", + ` kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`, + " exit 44", + " fi", + " if [ \"$(date +%s)\" -ge \"$deadline\" ]; then", + ` kubectl get job,pod -n ${shellQuote(GIT_MIRROR_NAMESPACE)} -l job-name="$job" -o wide || true`, + " exit 45", + " fi", + " sleep 2", + "done", + `kubectl logs -n ${shellQuote(GIT_MIRROR_NAMESPACE)} "job/$job" --tail=200 || true`, + `kubectl exec -n ${shellQuote(GIT_MIRROR_NAMESPACE)} deploy/git-mirror-http -- sh -lc 'cat /cache/HWLAB.last-sync.json 2>/dev/null || true; printf "\\n"; git --git-dir=/cache/pikasTech/HWLAB.git rev-parse refs/heads/v0.2 refs/heads/v0.2-gitops refs/heads/G14 2>/dev/null || true'`, + ].join("\n"), + ].join("\n"); + const result = g14K3s(["script", "--", script], options.timeoutSeconds * 1000 + 30_000); + return { + ok: isCommandSuccess(result), + command: "hwlab g14 git-mirror sync", + mode: options.dryRun ? "dry-run" : "confirmed-sync", + namespace: GIT_MIRROR_NAMESPACE, + jobName, + manifest: options.dryRun ? manifest : undefined, + result, + status: options.dryRun ? undefined : runGitMirrorStatus(), + next: options.dryRun ? { sync: "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm" } : { triggerCurrent: "bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm" }, + }; +} + +function runG14GitMirror(options: G14GitMirrorOptions): Record { + if (options.action === "status") return runGitMirrorStatus(); + if (options.action === "apply") return runGitMirrorApply(options); + return runGitMirrorSync(options); +} + function g14HostScript(script: string, timeoutMs = 120_000): CommandJsonResult { return cliJson(["ssh", G14_PROVIDER, "script", "--", script], timeoutMs); } @@ -1804,11 +2092,14 @@ export function hwlabG14Help(): Record { "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --dry-run", "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --allow-live-db-read --dry-run", "bun scripts/cli.ts hwlab g14 control-plane runtime-migration --lane v02 --confirm", + "bun scripts/cli.ts hwlab g14 git-mirror status", + "bun scripts/cli.ts hwlab g14 git-mirror apply --confirm", + "bun scripts/cli.ts hwlab g14 git-mirror sync --confirm", "bun scripts/cli.ts hwlab g14 tools-image status --name ci-node-tools --tag node22-alpine-bun-v1", "bun scripts/cli.ts hwlab g14 tools-image build --name ci-node-tools --tag node22-alpine-bun-v1 --confirm", "bun scripts/cli.ts job status --tail-bytes 30000", ], - description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; control-plane status/apply/trigger-current/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, manual PipelineRun trigger, runtime migration, and completed CI workspace retention only.", + description: "G14 HWLAB PR monitor, DEV rollout command, bounded v0.2 control-plane bootstrap/cleanup/runtime-migration helper, devops-infra git mirror maintenance, and controlled CI tools image build/status entry. The public monitor starts a fire-and-forget job; control-plane status/apply/trigger-current/cleanup-runs/cleanup-released-pvs/runtime-migration uses UniDesk G14:k3s routes for v0.2 Tekton/Argo control resources, manual PipelineRun trigger, runtime migration, and completed CI workspace retention only. git-mirror status/apply/sync is the manual devops-infra mirror control path and does not install a CronJob.", defaults: { repo: HWLAB_REPO, base: G14_SOURCE_BRANCH, @@ -1845,8 +2136,12 @@ export async function runHwlabG14Command(_config: Config, args: string[]): Promi const options = parseToolsImageOptions(args.slice(1)); return runG14ToolsImage(options); } + if (action === "git-mirror") { + const options = parseGitMirrorOptions(args.slice(1)); + return runG14GitMirror(options); + } if (action !== "monitor-prs") { - return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 tools-image" }; + return { ok: false, command: `hwlab g14 ${action ?? ""}`.trim(), degradedReason: "unsupported-command", message: "supported commands: hwlab g14 monitor-prs, hwlab g14 record-rollout, hwlab g14 control-plane, hwlab g14 git-mirror, hwlab g14 tools-image" }; } const options = parseOptions(args.slice(1)); if (options.worker) return runMonitorWorker(options);