ci: finalize pac status and gh heredoc guard

This commit is contained in:
Codex
2026-07-05 12:46:49 +00:00
parent 3e2b28e26d
commit 79e9288d5f
16 changed files with 364 additions and 33 deletions
+5 -3
View File
@@ -15,6 +15,8 @@ bun scripts/cli.ts hwlab g14 control-plane status --lane v02
bun scripts/cli.ts hwlab g14 control-plane trigger-current --lane v02 --confirm --wait
bun scripts/cli.ts hwlab g14 git-mirror status --lane v02
bun scripts/cli.ts agentrun control-plane status
bun scripts/cli.ts platform-infra gitea mirror status --target JD01
bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01
bun scripts/cli.ts cicd branch-follower status
bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel-master --step state-read
```
@@ -31,10 +33,10 @@ bun scripts/cli.ts cicd branch-follower debug-step --follower web-probe-sentinel
## P0 边界
- CI/CD、GitOps、rollout、PipelineRun、Argo、git-mirror 和 AgentRun 部署必须走受控 CLI;不要用裸 `kubectl``argo``tkn``curl` 当正式控制入口。
- CI/CD source authority 只能来自 Kubernetes 托管的 git-mirror snapshot:受控命令先同步 GitHub refs 到 k8s git-mirror,再创建/读取不可变 `refs/unidesk/snapshots/.../<commit>` stage refbuild/status/publish 只消费该 snapshothost worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。
- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRefbranch-follower controller 读 `config/cicd-branch-followers.yaml#controller.source.githubSsh`runtime git-mirror 读 owning lane/control-plane YAML 的 host proxy 和 `githubTransport`;禁止依赖未声明 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
- CI/CD source authority 只能来自 YAML 声明的 Kubernetes 托管 source authoritylegacy lane 使用 k8s git-mirror snapshot,迁移 lane 使用 Gitea controlled mirror + immutable snapshot ref。受控命令先在 k8s 内同步/创建不可变 `refs/unidesk/snapshots/.../<commit>` stage refbuild/status/publish 只消费该 snapshothost worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。
- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRefbranch-follower controller 读 `config/cicd-branch-followers.yaml#controller.source.githubSsh`runtime legacy git-mirror 读 owning lane/control-plane YAML 的 host proxy 和 `githubTransport`Gitea/PaC 迁移 lane 读 `config/platform-infra/gitea.yaml``config/platform-infra/pipelines-as-code.yaml`;禁止依赖未声明 host env、trans proxy、裸直连 GitHub 或 CLI 输出解析。
- `cicd branch-follower` 的自动跟随全过程不得读取或挂载 host worktree、target dev dir、`.worktree/*` 或 local git checkoutcontroller pod/一次性 reconcile Job 只能用 k8s git-mirror cache、Tekton PipelineRun、Argo Application、runtime workload 和 EmptyDir 执行,状态以 K8s ConfigMap/Lease 承载的 native observation 为准,不得解析下游 CLI 输出。
- CI/CD、rollout、publish、image build 和部署链路禁止新引入 Docker 依赖;不得依赖 Docker socket、Docker daemon、host Docker、`docker build``docker push` 或等价 Docker-only 路径
- k8s 运行面从拉取已构建镜像开始必须 0 Docker;CI 构建面可以使用 YAML 声明的原生构建工具,但不得把 Docker socket、Docker daemon 或 host Docker 带入运行面
- 正式 CI/CD、publish、image build 和 rollout 必须走 Tekton Task/Pipeline/PipelineRun 承担 CI,并通过 GitOps/Argo 承担部署收敛;普通 Kubernetes Job 只允许用于 bounded helper、source sync、diagnostic、cleanup 或 bootstrap,不得作为正式发布、镜像构建或 rollout 入口。
- 正式 CI/CD 必须提供一键完成入口:同一受控命令应完成 source sync、构建、发布、GitOps/Argo 收敛、runtime provenance 校验和 `/health` 端点验证;不要要求操作者手动串联多个 publish/apply/status 命令才能完成一次交付。
- CI/CD 一键交付的端到端 wall-clock 目标是低于 2 分钟;计时从操作者触发受控命令开始,到 runtime ready 且 `/health` 端点验证完成为止。具体 wait/timeout/budget 字段必须从 YAML/source-of-truth 读取并配置到满足该目标。
@@ -12,13 +12,15 @@ bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 [--dry-
bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane cleanup-runners --node D601 --lane v02 [--force-active] [--dry-run|--confirm]
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 [--pipeline-run <name>|--source-commit <sha>] [--full|--raw]
bun scripts/cli.ts platform-infra gitea mirror status --target JD01 [--full|--raw]
bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01 [--full|--raw]
```
- `plan`: 只读解析 YAML,输出控制面、source、image build、GitOps、runtime 和 Secret plan,不打印 Secret value。
- `apply`: 按 YAML 渲染并 apply Tekton RBAC/Pipeline、Argo AppProject/Application 和 runtime namespace。
- `secret-sync`: 按 YAML 的 Secret sourceRef/keyMapping 同步 runtime Secret 和外置 DB Secret,只输出 fingerprint。
- `restart`: patch manager Deployment restart annotation 并等待 rollout,用于 Secret export/DB 连接串变化后让 workload 读取新 Secret;不要手工删除 Pod。
- `trigger-current`: v0.2 lane source authority 只读 k8s git-mirror snapshot。confirmed 运行先触发受控 `git-mirror sync`,为 source branch tip 创建 `refs/unidesk/snapshots/agentrun-yaml-lane/<branch>/<commit>`,再从该 snapshot 构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalogflush git-mirror 并创建 provenance PipelineRun。
- `trigger-current`: legacy v0.2 lane source authority 只读 k8s git-mirror snapshot。confirmed 运行先触发受控 `git-mirror sync`,为 source branch tip 创建 `refs/unidesk/snapshots/agentrun-yaml-lane/<branch>/<commit>`,再从该 snapshot 构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalogflush git-mirror 并创建 provenance PipelineRun。迁移到 PaC/Gitea 的 lane 不再通过该自维护触发器提交 CI。
- `cleanup-runners`: 只清 YAML 选中 lane runtime namespace 中匹配 `deployment.runner.retention.selectors` 的 runner Job/Podrunner 上限、最后活跃排序、active heartbeat 窗口、age-based cleanup 开关和 selector 都以 YAML 为准。
- `status`: 默认返回 compact commander JSON,关键结论在 `.data.summary``.data.alignment`;完整 YAML target、原始 source/runtime/gitMirror payload 和 probe tail 只在 `--full|--raw` 展开。
@@ -26,6 +28,22 @@ YAML-only lane 的长步骤必须由 CLI 拆成短提交和状态轮询:k8s gi
AgentRun YAML-only lane closeout 必须同时看当前 k8s git-mirror source snapshot、目标 PipelineRun、GitOps revision、Argo revision 和 manager source commit。发布过程中如果 source branch 被并行 PR 推进,`status --pipeline-run <name>` 会通过 `summary.branchDrift` / `alignment.branchDrift` 标记目标 PipelineRun 是否已被当前 snapshot tip supersede。最终只用最新 PipelineRun 的 `status``aligned=true``blockers=[]``argoSyncedToGitops=true``managerSourceMatchesExpected=true` 收口。
## JD01 v0.2 Gitea / Pipelines-as-Code Lane
JD01 `agentrun-jd01-v02` 的 CI 触发路径已经从 branch-follower、act_runner 和自维护 trigger-current 迁移为单一路径:Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime。不要保留 Gitea Actions、act_runner、legacy branch-follower 或第二套 trigger fallback。Gitea mirror 的 source authority 来自 `config/platform-infra/gitea.yaml`PaC controller、Repository、webhook 和 Tekton 参数来自 `config/platform-infra/pipelines-as-code.yaml`
Gitea 只作为受控 mirror/source authority 和 PaC webhook 源。公开 Web UI 是 `https://gitea.pikapython.com`k8s 内部 clone/read 必须走 `http://gitea-http.devops-infra.svc.cluster.local:3000/...`,不要让内部 CI、Argo 或 runtime 通过公网域名回环。需要 CI 匿名内部 read 的 mirror repo 必须在 YAML 中声明 `publicRead: true`bootstrap/apply 必须修正既有 repo/org visibility,而不只依赖 create-time 默认值。
PaC closeout 的首选状态入口是:
```bash
bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01
```
默认状态必须显示 controller/CRD/webhook ready、最新 PipelineRun 和 TaskRun duration、`IMAGE_STATUS`、env identity、image digest、GitOps commit、Argo revision/health 和 runtime source/env annotations。env reuse 的接受证据是 `IMAGE_STATUS=reused`、同一 env identity 和稳定 digest;首次 cache miss 构建可以超过 120s,但最终收口应以后续 env-reuse 的秒级 PipelineRun 为准。
JD01 v0.2 的 runtime boot repoURL、Argo repoURL 和 CI git read URL 必须使用 Gitea internal read URL。若仍指向 legacy git-mirror read URLruntime 可能因 `git-mirror-exact-commit-unavailable` 崩溃,因为对应 commit 只存在于 Gitea snapshot/source。GitOps publish 必须用 PaC/Gitea token Secret 写回 Gitea mirror;如果 `platform-infra pipelines-as-code status` 中 GitOps commit 已推进但 Argo revision 未追上,可以用 `bun scripts/cli.ts agentrun control-plane refresh --node JD01 --lane jd01-v02 --confirm` 触发受控 hard refresh,但持久修复是 Gitea write path 和 Argo repoURL 对齐,不是 legacy git-mirror flush。
Runner egress proxy、持久化、idle timeout 和 retention 只从 `config/agentrun.yaml``deployment.runner.*` 进入部署。验收不能只看 manager Deployment/Pod env;必须用 HWLAB/AgentRun 原入口创建新 turn 或 runner Job,并检查新 runner Job env、session PVC、`AGENTRUN_SOURCE_COMMIT` 和 trace/result 是否复用同一 run 且没有 `reuse-blocked`
Provider credential 的 `config.toml` 变更同样走 YAML `sourceRef``secret-sync``restart`lane config 只声明该 lane 需要的 Codex CLI runtime options。不要复制指挥机全局 `~/.codex/config.toml` 作为长期事实。
@@ -106,10 +106,10 @@ Timeout, TTL, retry/backoff, reconcile interval and end-to-end budget values mus
## First Followers
- `hwlab-jd01-v03`: follows `pikasTech/HWLAB@v0.3`, adapter `hwlab-node-runtime`, native trigger `Tekton PipelineRun -> Argo Application closeout -> runtime Deployment sourceCommit readiness`.
- `agentrun-jd01-v02`: follows `pikasTech/agentrun@v0.2`, adapter `agentrun-yaml-lane`, native trigger `build image Job -> GitOps publish Job -> git-mirror flush Job -> Tekton PipelineRun -> Argo Application closeout -> runtime Deployment sourceCommit readiness`. The same source commit must use deterministic Job names so a later controller loop can resume or reuse already completed stages.
- `agentrun-jd01-v02`: historical first follower only. This lane has migrated to Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> runtime readiness; do not re-enable branch-follower, act_runner or custom trigger fallback for it. Current operation lives in `config/platform-infra/gitea.yaml`, `config/platform-infra/pipelines-as-code.yaml` and [agentrun.md](agentrun.md).
- `web-probe-sentinel-master`: follows `pikasTech/unidesk@master`, adapter `web-probe-sentinel-cicd`, native trigger `Tekton PipelineRun -> Argo Application closeout -> runtime Deployment sourceCommit readiness`.
These three followers are the initial production set. HWLAB and AgentRun both run on JD01; there is no D601 target in the automatic follower set unless YAML is explicitly changed.
These entries describe the initial production set and migration history. HWLAB still runs on JD01 through branch-follower unless YAML changes; AgentRun JD01 v0.2 now uses the PaC/Gitea path.
## Reuse And Mirror Contract
@@ -40,11 +40,13 @@ bun scripts/cli.ts hwlab g14 observability status|apply|query|targets|boundary|c
```bash
bun scripts/cli.ts platform-infra sub2api plan|apply|status|validate
bun scripts/cli.ts platform-infra sub2api codex-pool plan|sync|validate|expose|configure-local
bun scripts/cli.ts platform-infra gitea plan|apply|status|validate|mirror --target JD01
bun scripts/cli.ts platform-infra pipelines-as-code plan|apply|status|webhook-test --target JD01
bun scripts/cli.ts platform-infra wechat-archive plan|apply|status|validate|pull
bun scripts/cli.ts platform-infra wechat-archive wcf-host-status|collector-plan|collector-apply|collector-status
```
`platform-infra` G14 k3s 上 UniDesk 运维的平台基础设施 namespace;新增平台服务优先进入该 namespace,旧 `devops-infra` 只作为渐进迁移来源。Sub2API 日常部署、Codex pool、FRP 暴露、master `~/.codex` 配置、验收和排障统一使用 `$unidesk-sub2api`。WeChat archive 是 platform-infra 的 YAML-first 工作流入口;只读 collector 的副本、镜像、WCF host、端口和版本 pin 都以 YAML 为准。
`platform-infra` 是 UniDesk 运维的平台基础设施控制面;新增平台服务优先进入该命名空间或对应 YAML 声明目标,旧 `devops-infra` 只作为渐进迁移来源。Sub2API 日常部署、Codex pool、FRP 暴露、master `~/.codex` 配置、验收和排障统一使用 `$unidesk-sub2api`Gitea mirror 和 Pipelines-as-Code 是迁移后的 CI source/trigger 平台服务,source-of-truth 分别是 `config/platform-infra/gitea.yaml``config/platform-infra/pipelines-as-code.yaml`PaC status 是 migrated lane closeout 入口,不用 Gitea Actions、act_runner、branch-follower 或自维护脚本兜底。WeChat archive 是 platform-infra 的 YAML-first 工作流入口;只读 collector 的副本、镜像、WCF host、端口和版本 pin 都以 YAML 为准。
## CI Tools Image
+1 -1
View File
@@ -16,7 +16,7 @@ GitHub issue/PR 正式读写必须走 `bun scripts/cli.ts gh ...` 或 `trans gh:
- 规划型、多阶段、架构/API/平台运维类 issue 第一阶段必须 `P0 SPEC 先行`;细则见 [references/issues.md](references/issues.md)。
- `P0 SPEC 先行` 段不得写入硬编码阈值、采样周期、重试次数、并发数等可调参数;必须写明这些参数由指定 YAML/source-of-truth 控制,issue 只列配置路径、字段族和验收读取方式。
- `gh` 默认输出是 k8s 风格 text/table/summary/Next/Disclosure;脚本消费或全量排障必须显式用 `--json``--full``--raw`
- 多行正文使用 quoted heredoc`--body-stdin <<'EOF'`;不要把 Markdown 塞进 shell 参数。
- 多行正文使用 quoted heredoc`--body-stdin <<'EOF'`issue close/reopen 生命周期评论只用 `--comment-stdin <<'EOF'`不要把 Markdown 塞进 shell 参数。
- PR merge 只走 guarded `gh pr merge``gh pr create` 的 Next 默认是 `--merge --delete-branch`,只有确认 ancestry 可丢弃时才显式 `--squash`
## 常用入口
@@ -8,4 +8,5 @@ Issue writes use `bun scripts/cli.ts gh ...` or the `trans gh:` virtual filesyst
- New issues include `目标合并分支`.
- Multi-stage architecture/API/platform issues begin with `P0 SPEC 先行`.
- Long body text uses `--body-stdin`.
- Issue close/reopen lifecycle comments use `--comment-stdin <<'EOF'`; inline `--comment` and lifecycle `--comment-file` are unsupported.
- Use bounded views first; request `--json`, `--full` or `--raw` only when needed.
+7 -1
View File
@@ -92,9 +92,15 @@ bun scripts/cli.ts agentrun control-plane restart --node D601 --lane v02 --confi
bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --dry-run
bun scripts/cli.ts agentrun control-plane trigger-current --node D601 --lane v02 --confirm
bun scripts/cli.ts agentrun control-plane status --node D601 --lane v02 --full
bun scripts/cli.ts platform-infra gitea mirror status --target JD01
bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01
```
`status` 只读观察 YAML 选中 lane 的 source authority、对应 PipelineRun、GitOps latest、Argo Application、runtime workload、manager source commit 和 git mirror 摘要,并报告 Argo revision 是否对齐该 lane 的 GitOps latest。`v0.2` lane source authority 只来自 k8s git-mirror snapshot:受控 sync 先为 branch tip 创建 `refs/unidesk/snapshots/agentrun-yaml-lane/<branch>/<commit>``status` / `trigger-current` / build 只读取该 snapshot 和 `sourceStageRef`,不得把 host source workspace、本地 fetch/pull、可变 branch ref 或 Pipeline 直连 GitHub 当 authoritative source。默认输出是 compact commander 视图:`target` 只保留 node/lane/source/runtime/CI/GitOps/git-mirror/database 摘要,关键结论在 `summary``alignment`,成功 probe 的 stdout/stderr tail、完整 YAML target、原始 `source``runtime``gitMirror` payload 默认省略;需要完整展开时使用返回的 `disclosure.fullCommand` 或显式加 `--full`,需要原始调试视图时加 `--raw``status` 额外支持 `--pipeline-run <name>``--source-commit <sha>` 定点查询;`--pipeline-run` 会读取 PipelineRun `revision` 参数作为 pinned source commit,并在 `alignment.branchDrift` / `summary.branchDrift` 中同时披露当前 snapshot tip、目标 source commit、PipelineRun source commit、是否已被当前 snapshot supersede 以及 `triggerLatest` 下一步。`status` 会向 stderr 输出 `agentrun.control-plane.status.progress` 阶段事件,覆盖 `source``runtime``git-mirror`,避免长时间聚合时无可见进展。`trigger-current` 会先执行 k8s git-mirror sync 并以 snapshot commit 创建 commit-pinned PipelineRun;同名 PipelineRun 正在运行或已经成功时必须拒绝重复触发,只允许在失败态或不存在时创建。该命令只提交 CI/CD 工作,不等待完整 PipelineRun 或 rollout 完成,后续用 `job status``status --pipeline-run <name>` 轮询。`refresh` 只对 YAML 声明的 Argo Application 执行 hard refresh,用于 GitOps promotion 已完成但 Argo 仍停留旧 revision 时的受控同步入口;它不直接 patch runtime workload。
`status` 只读观察 YAML 选中 lane 的 source authority、对应 PipelineRun、GitOps latest、Argo Application、runtime workload、manager source commit 和 git mirror/Gitea 摘要,并报告 Argo revision 是否对齐该 lane 的 GitOps latest。未迁移的 legacy `v0.2` lane source authority 只来自 k8s git-mirror snapshot:受控 sync 先为 branch tip 创建 `refs/unidesk/snapshots/agentrun-yaml-lane/<branch>/<commit>``status` / `trigger-current` / build 只读取该 snapshot 和 `sourceStageRef`,不得把 host source workspace、本地 fetch/pull、可变 branch ref 或 Pipeline 直连 GitHub 当 authoritative source。默认输出是 compact commander 视图:`target` 只保留 node/lane/source/runtime/CI/GitOps/git-mirror/database 摘要,关键结论在 `summary``alignment`,成功 probe 的 stdout/stderr tail、完整 YAML target、原始 `source``runtime``gitMirror` payload 默认省略;需要完整展开时使用返回的 `disclosure.fullCommand` 或显式加 `--full`,需要原始调试视图时加 `--raw``status` 额外支持 `--pipeline-run <name>``--source-commit <sha>` 定点查询;`--pipeline-run` 会读取 PipelineRun `revision` 参数作为 pinned source commit,并在 `alignment.branchDrift` / `summary.branchDrift` 中同时披露当前 snapshot tip、目标 source commit、PipelineRun source commit、是否已被当前 snapshot supersede 以及 `triggerLatest` 下一步。`status` 会向 stderr 输出 `agentrun.control-plane.status.progress` 阶段事件,覆盖 `source``runtime``git-mirror`,避免长时间聚合时无可见进展。legacy `trigger-current` 会先执行 k8s git-mirror sync 并以 snapshot commit 创建 commit-pinned PipelineRun;同名 PipelineRun 正在运行或已经成功时必须拒绝重复触发,只允许在失败态或不存在时创建。该命令只提交 CI/CD 工作,不等待完整 PipelineRun 或 rollout 完成,后续用 `job status``status --pipeline-run <name>` 轮询。`refresh` 只对 YAML 声明的 Argo Application 执行 hard refresh,用于 GitOps promotion 已完成但 Argo 仍停留旧 revision 时的受控同步入口;它不直接 patch runtime workload。
JD01 `agentrun-jd01-v02` 已迁移为 Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime 的单一路径。该 lane 不再使用 branch-follower、Gitea Actions、act_runner 或自维护 `trigger-current` 作为 CI 触发器;对应 source authority、repo visibility、public exposure 和 mirror credentials 属于 `config/platform-infra/gitea.yaml`PaC controller/Repository/webhook/Tekton 参数属于 `config/platform-infra/pipelines-as-code.yaml`。Gitea 的公开 Web UI 是 `https://gitea.pikapython.com`,但 CI、Argo 和 runtime 内部读取必须使用 `gitea-http.devops-infra.svc.cluster.local:3000` 的 ClusterIP URL,避免公网回环和 legacy git-mirror commit 缺失。
JD01 v0.2 PaC closeout 以 `bun scripts/cli.ts platform-infra pipelines-as-code status --target JD01` 为首选状态入口。默认输出必须能直接看到 webhook 是否存在、最新 PipelineRun/TaskRun duration、image status、env identity、digest、GitOps commit、Argo revision/health 和 runtime source/env annotation。env reuse 的通过证据是 `IMAGE_STATUS=reused`、同一 env identity 和稳定 digest;首次 cache miss 可以作为冷启动事实记录,但不能替代后续 env-reuse 秒级收口。
YAML-only lane 的 `trigger-current --confirm` 是受控长流程入口;k8s git-mirror snapshot sync、image build、GitOps publish、git-mirror flush 和 PipelineRun 创建必须拆成短提交与状态轮询,不得把 clone、build、push 或长时间 polling 放进一个顶层 `trans` 长连接。`trigger-current` 返回异步 job 时,先用 `bun scripts/cli.ts job status <jobId> --tail-bytes 12000` 观察 `agentrun-yaml-lane-trigger` progress,再用 `agentrun control-plane status --node <node> --lane <lane> --pipeline-run <name>` 观察 Tekton、GitOps 和 Argo 对齐。后台步骤的 `status``ok` 必须共同判定,`status=succeeded``ok=false` 是终态失败,不能继续轮询到超时。GitOps publish 必须使用隔离临时 clone/worktree,不能切换或污染任何固定 source workspace`v0.2` 历史失败 publish 若留下 dirty/detached/GitOps branch 状态,不得通过 host workspace repair 恢复 source authority,只清理已知发布残留并从 git-mirror snapshot 重新触发。
+3 -3
View File
@@ -107,11 +107,11 @@ PipelineRun 失败或长时间未完成时,先按定点 `control-plane status
- `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/<run-id>/` 和本地 `.state/hwlab-cd/<run-id>/` task dumpstdout 只返回有界摘要。默认 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 语义失败。
- `gh issue list [owner/repo] [--state open|closed|all] [--limit N] [--search text] [--title-prefix text] [--label label[,label...]]... [--repo owner/name] [--json number,title,state,url,updatedAt,createdAt,author,labels] [--raw|--full]` 通过 GitHub REST 列出 issue,默认 `state=open``limit=30`,输出稳定 JSON 且不依赖系统 `gh` binary。`owner/repo` 位置参数是 `--repo owner/repo` 的兼容别名;若位置 repo 与 `--repo` 冲突,或位置参数不是 `owner/repo`,必须结构化失败,禁止静默 fallback 到默认 repo。`--limit` 是 CLI 返回上限,不等同 GitHub 单页 `per_page`:当 `--limit > 100` 或默认页中混入 PR 时,CLI 必须分页抓取 GitHub REST/Search page,过滤 PR 后再返回 issue,并在输出中披露 `pagination.fetchedPages/rawCount/hasMore``hasMore=true` 时只能说明当前有界扫描未穷尽,禁止把它当作“仓库没有更多 issue”。`--search` 使用 GitHub Search Issues API,并自动追加 `repo:<owner>/<name>``type:issue` 和 state qualifier,用于创建新 issue 前做低摩擦查重;`--title-prefix` 在当前有界结果内按 issue 标题 `startsWith` 做本地过滤,输出 `titleFilter.inputCount/outputCount/filteredOut`,适合 `[FEEDBACK]` 等标题规范去重;未知 state 或未知 `--json` 字段必须结构化失败并带 `runnerDisposition=business-failed``--label` 是 GitHub REST `labels=label1,label2` 或 Search `label:` 服务端过滤,支持重复 `--label` 和逗号分隔;filter 不在本命令上下文中使用(如 `issue read``pr list`)必须结构化失败并指明 `gh issue create/list/stale-close` 才是合法作用域。GitHub issues API 可能混入 PRCLI 会从 `.data.issues` 中过滤 pull request。`--raw|--full``gh issue list` 上是绕过 20 KiB stdout 截断的显式开关:响应结果会带 `noDump=true``output.ts` 据此跳过 head/tail 替换并把完整数据 inline 输出;当响应未超阈值时 `--raw|--full` 行为等价默认。
- `gh issue lifecycle``--state` 只能作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `gh issue edit` 只写 body/title**不接受** `--state` 改 open/closed。把 `gh issue update <n> --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close <n>` / `gh issue reopen <n>`PR 用 `gh pr close|reopen <n>`),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;人工正文读取优先使用 `trans gh:/owner/repo/issue/<number> cat|rg`,需要 JSON 结构化元数据时再使用返回的 `readCommands``gh issue view --json body|--full|--raw`。生命周期 close/reopen 的评论推荐用 `--comment-stdin <<'EOF'` 直接写 heredoc/stdin短单行可用 `--comment`,已有复用文件才用 `--comment-file`。需要附长篇 CLI 验收证据时,先用 `gh issue comment create <n> --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close <n> --comment <短引用>` 关闭。issue 硬删除走 `close`PR 硬删除走 `close`,两者都没有"delete"语义。
- `gh issue comment create <number|owner/repo#number> --repo owner/name --body-stdin``gh issue comment update|edit <commentId> --repo owner/name --body-stdin``gh issue comment delete <commentId|owner/repo#number> --repo owner/name``gh issue close <number|owner/repo#number> --repo owner/name [--comment <text>|--comment-stdin]``gh issue reopen <number|owner/repo#number> --repo owner/name [--comment <text>|--comment-stdin]``gh issue update <number|owner/repo#number> --repo owner/name [--title ...] [--body-stdin]``gh issue edit <number|owner/repo#number> ...``gh issue board-row get|update|add|move|delete|upsert <number|owner/repo#number> --repo owner/name ...` 都接受与 `gh issue view|read``gh pr *` 一致的 `owner/repo#number` 位置 shorthandshorthand 与显式 `--repo` 冲突时结构化失败并把两者都回显到错误对象里,避免静默改写目标 repo。`gh issue view|read``gh pr view|read|files|diff|preflight|closeout|comment create|comment update|comment edit|comment delete|close|reopen|merge|edit|update` 已长期支持该 shorthandcomment update/edit/delete 的 `--number` 表示 commentId,不是 issue/PR number。issue 写命令对齐后整个 `gh` 子命令在 shorthand 行为上保持一致,不再需要把 `pikasTech/HWLAB#621` 拆成 `621 --repo pikasTech/HWLAB`。来源:HWLAB #621 CLI 验收 `gh issue comment create pikasTech/HWLAB#621` 摩擦改进。
- `gh issue lifecycle``--state` 只能作为 `gh issue list` / `gh issue board-row list` / `gh pr list` 的过滤参数;`gh issue update` / `gh issue edit` 只写 body/title**不接受** `--state` 改 open/closed。把 `gh issue update <n> --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close <n>` / `gh issue reopen <n>`PR 用 `gh pr close|reopen <n>`),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;人工正文读取优先使用 `trans gh:/owner/repo/issue/<number> cat|rg`,需要 JSON 结构化元数据时再使用返回的 `readCommands``gh issue view --json body|--full|--raw`。生命周期 close/reopen 的评论只支持 `--comment-stdin <<'EOF'` quoted heredoc/stdin内联 `--comment` 和生命周期 `--comment-file` 都必须结构化拒绝,避免 shell 转义污染 Markdown。需要附长篇 CLI 验收证据时,先用 `gh issue comment create <n> --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close <n> --comment-stdin <<'EOF'` 写简短中文收口。issue 硬删除走 `close`PR 硬删除走 `close`,两者都没有"delete"语义。
- `gh issue comment create <number|owner/repo#number> --repo owner/name --body-stdin``gh issue comment update|edit <commentId> --repo owner/name --body-stdin``gh issue comment delete <commentId|owner/repo#number> --repo owner/name``gh issue close <number|owner/repo#number> --repo owner/name [--comment-stdin]``gh issue reopen <number|owner/repo#number> --repo owner/name [--comment-stdin]``gh issue update <number|owner/repo#number> --repo owner/name [--title ...] [--body-stdin]``gh issue edit <number|owner/repo#number> ...``gh issue board-row get|update|add|move|delete|upsert <number|owner/repo#number> --repo owner/name ...` 都接受与 `gh issue view|read``gh pr *` 一致的 `owner/repo#number` 位置 shorthandshorthand 与显式 `--repo` 冲突时结构化失败并把两者都回显到错误对象里,避免静默改写目标 repo。`gh issue view|read``gh pr view|read|files|diff|preflight|closeout|comment create|comment update|comment edit|comment delete|close|reopen|merge|edit|update` 已长期支持该 shorthandcomment update/edit/delete 的 `--number` 表示 commentId,不是 issue/PR number。issue 写命令对齐后整个 `gh` 子命令在 shorthand 行为上保持一致,不再需要把 `pikasTech/HWLAB#621` 拆成 `621 --repo pikasTech/HWLAB`。来源:HWLAB #621 CLI 验收 `gh issue comment create pikasTech/HWLAB#621` 摩擦改进。
- `gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]` 是可复用批量生命周期清理入口,用于“超过 N 小时无回复或修改的 open issue 一律关闭”这类策略。判定基准固定为 GitHub `updatedAt < observedAt - inactiveHours`issue comment、body/title 修改和 state 变化都会刷新 `updatedAt` 并视为活跃;PR 必须过滤,不参与 issue 关闭。默认 `inactive-hours=48`,默认扫描预算为 issue list 上限,输出必须包含 `observedAt``cutoffAt``scannedCount``staleCount``pagination.hasMore`、候选/关闭 issue 的 compact 摘要和失败列表,不得打印完整正文。正式关闭前建议先跑 `--dry-run`;真实执行后用同一命令加 `--dry-run` 验证 `staleCount=0`,且只有 `hasMore=false` 才能把当前扫描视为完整穷尽。HWLAB 当前长期策略使用 `bun scripts/cli.ts gh issue stale-close --repo pikasTech/HWLAB --inactive-hours 48 --dry-run` 观察,再移除 `--dry-run` 关闭。
- `gh issue view <number|url|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]` 通过 GitHub REST 读取 issue title/body/state/url 和 comments,默认输出 JSON`read` 只保留为 UniDesk 兼容别名。`view` 对齐 GitHub CLI 标准位置参数:接受正整数、`https://github.com/owner/repo/issues/<number>` URL 或 `owner/repo#number` shorthand。`--number N` 也作为低摩擦兼容别名用于单 issue/comment 数字目标命令,并在成功响应里返回 `standardSyntaxHint` 提示标准位置参数写法;comment update/edit/delete 中的 `--number` 表示 commentId,不是 issue number`list/create/scan-escape/cleanup-plan/board-audit/board-row list` 这类没有单数字目标的命令仍拒绝 `--number`。URL 和 shorthand 会自动派生 `--repo owner/repo` 与 issue number;若同时提供冲突的显式 `--repo`CLI 必须结构化失败并给出 `gh issue view <number> --repo owner/repo --json body,title,state,comments` 与 shorthand raw 的可执行命令。兼容旧脚本的 `--json body``--json body,title,state,comments` 字段选择,且正文仍稳定暴露在 `.data.issue.body`,避免调用方因为 JSON 路径变化把空值当成正文。字段白名单是 `body,title,state,comments,number,url,author,createdAt,updatedAt`,未知字段必须结构化失败并带 `runnerDisposition=business-failed``--raw``--full` 是显式完整披露别名:view/read 会选择完整支持字段集;issue update/edit 只有显式传入时才在成功响应里包含完整 `.data.issue.body`。当最终 `gh` JSON 超过 20 KiB 时,CLI 必须把完整 JSON 写入 `/tmp/unidesk-cli-output/*.json`stdout 只返回 `outputTruncated=true`、dump path、总 bytes/lines 和 head/tail 预览。默认 list/view 输出仍不得扩散到无界非 JSON 文本。`gh issue create --title <title> --body-stdin [--label label[,label...]]... [--dry-run]``gh issue update <number> --mode replace|append --body-stdin [--title ...] [--dry-run] [--full|--raw]``gh issue comment create <number> (--body-stdin|--body <short-text>) [--dry-run]``gh issue comment update|edit <commentId> (--body-stdin|--body <short-text>) [--dry-run]``gh issue comment delete <commentId> [--dry-run]``gh issue close|reopen <number> [--comment <short-text>|--comment-stdin] [--dry-run]``gh issue stale-close [--inactive-hours N] [--dry-run]` 都走 REST,不依赖 `gh` binary。`--body-stdin``--comment-stdin` 是多行 Markdown 的第一等 heredoc/stdin 入口;`--body-file` / `--comment-file` 只在已有复用文件时使用`--body` 仅用于 issue comment 的短单行文本;空白、多行、疑似 shell 污染、secret-like 或过长 inline body 必须结构化失败。`comment update/edit` 使用 GitHub issue comment PATCH 端点并保留评论 ID,日常修正文案优先用 update/edit,delete 只用于确实需要删除的评论。`--label` 用于 `issue create``issue list``issue stale-close`,支持重复传入和逗号分隔;`issue create --dry-run` 会展示解析后的 labels 与 request plan,正式创建时把 labels 放入 GitHub REST create-issue payloadGitHub 返回不存在 label 等 422 校验失败时 CLI 结构化返回 `validation-failed`,不静默成功。`gh issue delete <number>` 是结构化 `unsupported-command`,因为 GitHub REST 不支持 issue 硬删除;生命周期删除语义请使用 `close`
- `gh issue view <number|url|owner/repo#number> [--repo owner/name] [--json body,title,state,comments] [--raw|--full]` 通过 GitHub REST 读取 issue title/body/state/url 和 comments,默认输出 JSON`read` 只保留为 UniDesk 兼容别名。`view` 对齐 GitHub CLI 标准位置参数:接受正整数、`https://github.com/owner/repo/issues/<number>` URL 或 `owner/repo#number` shorthand。`--number N` 也作为低摩擦兼容别名用于单 issue/comment 数字目标命令,并在成功响应里返回 `standardSyntaxHint` 提示标准位置参数写法;comment update/edit/delete 中的 `--number` 表示 commentId,不是 issue number`list/create/scan-escape/cleanup-plan/board-audit/board-row list` 这类没有单数字目标的命令仍拒绝 `--number`。URL 和 shorthand 会自动派生 `--repo owner/repo` 与 issue number;若同时提供冲突的显式 `--repo`CLI 必须结构化失败并给出 `gh issue view <number> --repo owner/repo --json body,title,state,comments` 与 shorthand raw 的可执行命令。兼容旧脚本的 `--json body``--json body,title,state,comments` 字段选择,且正文仍稳定暴露在 `.data.issue.body`,避免调用方因为 JSON 路径变化把空值当成正文。字段白名单是 `body,title,state,comments,number,url,author,createdAt,updatedAt`,未知字段必须结构化失败并带 `runnerDisposition=business-failed``--raw``--full` 是显式完整披露别名:view/read 会选择完整支持字段集;issue update/edit 只有显式传入时才在成功响应里包含完整 `.data.issue.body`。当最终 `gh` JSON 超过 20 KiB 时,CLI 必须把完整 JSON 写入 `/tmp/unidesk-cli-output/*.json`stdout 只返回 `outputTruncated=true`、dump path、总 bytes/lines 和 head/tail 预览。默认 list/view 输出仍不得扩散到无界非 JSON 文本。`gh issue create --title <title> --body-stdin [--label label[,label...]]... [--dry-run]``gh issue update <number> --mode replace|append --body-stdin [--title ...] [--dry-run] [--full|--raw]``gh issue comment create <number> (--body-stdin|--body <short-text>) [--dry-run]``gh issue comment update|edit <commentId> (--body-stdin|--body <short-text>) [--dry-run]``gh issue comment delete <commentId> [--dry-run]``gh issue close|reopen <number> [--comment-stdin] [--dry-run]``gh issue stale-close [--inactive-hours N] [--dry-run]` 都走 REST,不依赖 `gh` binary。`--body-stdin``--comment-stdin` 是多行 Markdown 的第一等 heredoc/stdin 入口;`--body-file` 只在已有复用文件时使用;生命周期 `--comment-file` 不支持`--body` 仅用于 issue comment 的短单行文本;空白、多行、疑似 shell 污染、secret-like 或过长 inline body 必须结构化失败。`comment update/edit` 使用 GitHub issue comment PATCH 端点并保留评论 ID,日常修正文案优先用 update/edit,delete 只用于确实需要删除的评论。`--label` 用于 `issue create``issue list``issue stale-close`,支持重复传入和逗号分隔;`issue create --dry-run` 会展示解析后的 labels 与 request plan,正式创建时把 labels 放入 GitHub REST create-issue payloadGitHub 返回不存在 label 等 422 校验失败时 CLI 结构化返回 `validation-failed`,不静默成功。`gh issue delete <number>` 是结构化 `unsupported-command`,因为 GitHub REST 不支持 issue 硬删除;生命周期删除语义请使用 `close`
- `gh issue update <number> --mode replace|append --body-stdin` 是正文更新主入口,`edit` 保留为兼容别名。`replace` 用 heredoc/stdin 正文替换现有 body`append` 先读取当前 body,再按 UTF-8 stdin 字节追加,保留真实换行、反引号和 Markdown 表格。更新默认拒绝字面量 `null`、空白正文和过短正文;只有真实需要写短正文时才允许显式加 `--allow-short-body`,返回 JSON 会报告该风险。#20 总看板和指挥简报类 issue 是长期 body-only issue`--body-profile auto` 会按 issue number 自动启用 #20/#24 legacy guard#20 必须包含 `## 看板(OPEN`#24 legacy 指挥简报必须包含 `## 常驻观察与长期建议`。显式 `--body-profile commander-brief` 不再固定 #24#24 仍兼容,标题为 `YYYY-MM-DD 指挥简报(北京时间)` 或既有正文首行/关键 heading 表明为每日滚动指挥简报的 issue 也合法,并仍必须包含 `## 常驻观察与长期建议`。对非简报 issue 显式使用 `commander-brief` 会结构化失败为 `profile-issue-mismatch``--dry-run` 不 PATCH GitHub,输出有界 `bodyPreview`/`bodyPreviewLines`、新正文长度、SHA、关键标题检查结果、字面量 `\n`、反引号、Markdown 表格、shell 污染信号、`guard``concurrency``bodyOnlySafety``wouldPatch`;若环境里有 `GH_TOKEN``GITHUB_TOKEN`,dry-run 还会只读抓取旧正文长度、SHA 和 `updatedAt` 作为更新前对照。正式写入默认先读取当前 issue,执行 guard 和显式 `--expect-*` 并发校验,再 PATCH;成功输出 compact issue 摘要、old/new body SHA、updatedAt、bodySource 和 drill-down `readCommands`,不包含完整 `issue.body`。完整正文必须显式 `--full|--raw` 或后续执行 `readCommands.body/full/raw` 获取。
- #20 只允许承担长期 UniDesk 指挥官 / Code Queue / CLI / infra 治理总看板职责;每日进展必须写入当天滚动指挥简报 issue,并由 #20 顶部“指挥简报索引”引用。HWLAB 用户反馈、Cloud Workbench、DEV-LIVE、M3 虚拟硬件可信闭环等产品 issue 必须写到 `pikasTech/HWLAB`#20 只可记录 UniDesk 侧 commander/Code Queue/CLI/infra 支撑工作。`gh issue view/read 20` 会返回 `codeQueueBoardHint``gh issue update/edit 20` 的 body guard 会拒绝 `## 更新 YYYY-MM-DD HH:mm 北京时间``## YYYY-MM-DD HH:mm 北京时间指挥更新``### YYYY-MM-DD HH:mm CST...` 这类简报段落;把 `pikasTech/HWLAB#N``HWLAB#N` 或 HWLAB 产品/live 验证行写入 #20 时只返回 warning 和 `codeQueueBoardHint`,不再拒绝正文 replace,以避免历史正文或治理交叉引用造成次生阻塞;`gh issue board-row list|get|update|add|move|delete|upsert --board-issue 20` 也会返回同一 hint,提醒不要把每日简报或 HWLAB 产品看板混入 #20
- `gh issue edit 24 --body-stdin --notify-claudeqq-brief-diff [--dry-run] <<'EOF'` 是 legacy #24 指挥简报的通知入口。正式执行会先读取 GitHub 上 #24 旧正文并通过 #24 body profile guard,再从 heredoc/stdin 读取新正文;随后先 PATCH issue 主体,再把本次新增的 `## 更新 YYYY-MM-DD HH:MM 北京时间` 段落发送给 ClaudeQQClaudeQQ 失败不会回滚 issue 正文,失败只体现在返回 JSON 的 `claudeqq.ok=false` 和结构化 `degradedReason`。每日滚动简报 issue 可用普通 `gh issue update <number> --body-profile commander-brief --dry-run` 和并发 guard 更新,但此通知 helper 仍只支持 #24。带通知 flag 的 `--dry-run` 不 PATCH、不发送;它按新正文做发送预览,并在输出中标明非 dry-run 才会读取旧正文做可靠 diff。默认 ClaudeQQ 目标是私聊 `645275593`,默认 base URL 是 UniDesk 受控入口 `http://backend-core:8080/api/microservices/claudeqq/proxy``UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_BASE_URL` 只接受 backend-core `/api/microservices/claudeqq/proxy` 等价路径,非 proxy URL 会结构化为 `notification-path-unavailable`。可用 `UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_ENABLED``UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TARGET_TYPE``UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_USER_ID``UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_GROUP_ID``UNIDESK_COMMANDER_BRIEF_CLAUDEQQ_TIMEOUT_MS` 调整开关、目标和超时。
+9
View File
@@ -10,6 +10,15 @@
- Do not hide image versions, namespace names, endpoint URLs, FRP ports, or profile lists in Python/TOML/JSON helper constants when they are UniDesk-owned choices. External tools may still require their own TOML/JSON/env file formats at the edge.
- Fresh node outbound bootstrap uses the zero-dependency host proxy boundary defined in `docs/reference/yaml-first-ops.md`. Platform-infra egress proxy settings may provide the benchmark-validated proxy source and workload consumer settings, but a new node must not depend on Docker, k3s, containerd or package-manager network access to install the initial host proxy client.
## Gitea And Pipelines-as-Code Boundary
- Gitea mirror and Pipelines-as-Code are platform-infra CI source/trigger services operated by UniDesk. Their durable configuration lives in `config/platform-infra/gitea.yaml` and `config/platform-infra/pipelines-as-code.yaml`; do not hide repo URLs, mirror repo names, webhook settings, public exposure, FRP/Caddy ports, token sourceRefs or PaC Repository params in helper constants.
- The canonical Gitea entrypoints are `bun scripts/cli.ts platform-infra gitea plan|apply|status|validate|mirror --target <node>` and `bun scripts/cli.ts platform-infra gitea mirror plan|bootstrap|sync|status --target <node>`. Mirror bootstrap/sync must repair declared repo/org visibility such as `publicRead: true`; create-time defaults alone are not enough for long-lived repos.
- The canonical PaC entrypoints are `bun scripts/cli.ts platform-infra pipelines-as-code plan|apply|status|webhook-test --target <node>`. PaC status is the operator-facing closeout surface for migrated CI lanes and must expose webhook count, latest PipelineRun/TaskRun duration, image status, env identity, digest, GitOps commit, Argo revision and runtime provenance without requiring raw `kubectl`, `tkn` or Gitea UI inspection.
- Public Gitea UI may use the YAML-declared HTTPS hostname, but k8s-internal consumers must use the ClusterIP service URL from YAML. Internal CI/Argo/runtime reads must not loop through public DNS/Caddy/FRP, and migrated lanes must not fall back to legacy git-mirror read URLs when the commit exists only in Gitea.
- A PaC-migrated lane must keep a single trigger path: Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime. Do not add Gitea Actions, `act_runner`, branch-follower or custom script fallback unless a later issue explicitly changes the architecture.
- k8s runtime remains Docker-free from the point it pulls already built images. CI build steps may use YAML-declared native build tooling, but Docker socket/daemon access must not become part of the runtime plane.
## Secret Distribution Boundary
- UniDesk-owned platform service credential distribution must be YAML-controlled: declare the sourceRef, source key, target Secret, and target key first, then use the controlled CLI to sync/apply it. Runtime Kubernetes Secrets, pod env, logs, and database state are observation surfaces, not credential source of truth.
+181
View File
@@ -10,6 +10,7 @@ import { spawnSync } from "node:child_process";
import { rootPath, type UniDeskConfig } from "../config";
import type { RenderedCliResult } from "../output";
import { applyLocalCaddyManagedSite } from "../pk01-caddy";
import { runPlatformInfraPipelinesAsCodeCommand } from "../platform-infra-pipelines-as-code";
import { runSshCommandCapture, type SshCaptureResult } from "../ssh";
import { runRemoteSshCommandCapture } from "../remote";
import { startJob } from "../jobs";
@@ -325,8 +326,188 @@ export async function status(config: UniDeskConfig, options: StatusOptions): Pro
return await statusYamlLane(config, options, resolveAgentRunLaneTarget(options));
}
function isPipelinesAsCodeMigratedLane(spec: AgentRunLaneSpec): boolean {
return spec.gitMirror.readUrl.includes("gitea-http.")
|| spec.gitops.repoURL.includes("gitea-http.")
|| spec.gitMirror.readUrl.includes("/mirrors/");
}
function pacStatusCommand(spec: AgentRunLaneSpec, full = false): string {
return `bun scripts/cli.ts platform-infra pipelines-as-code status --target ${spec.nodeId}${full ? " --full" : ""}`;
}
function giteaMirrorStatusCommand(spec: AgentRunLaneSpec): string {
return `bun scripts/cli.ts platform-infra gitea mirror status --target ${spec.nodeId}`;
}
export async function statusPipelinesAsCodeLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
const pacStatus = record(await runPlatformInfraPipelinesAsCodeCommand(config, ["status", "--target", spec.nodeId, "--full"]));
const pacSummary = record(pacStatus.summary);
const latestPipelineRun = record(pacSummary.latestPipelineRun);
const artifact = record(pacSummary.artifact);
const pacArgo = record(pacSummary.argo);
const pipelineRunName = options.pipelineRun ?? stringOrNull(latestPipelineRun.name);
const runtimeProbe = await timedStatusStage("runtime", () => capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneRuntimeStatusScript(spec, pipelineRunName)]));
const runtimePayload = captureJsonPayload(runtimeProbe.value);
const manager = record(runtimePayload.manager);
const database = record(runtimePayload.database);
const secrets = record(runtimePayload.secrets);
const localPostgres = record(runtimePayload.localPostgres);
const sourceCommit = options.sourceCommit ?? stringOrNull(artifact.sourceCommit) ?? stringOrNull(latestPipelineRun.sourceCommit);
const expectedGitopsRevision = stringOrNull(artifact.gitopsCommit);
const argoRevision = stringOrNull(pacArgo.revision);
const argoSyncedToGitops = Boolean(expectedGitopsRevision && argoRevision === expectedGitopsRevision);
const managerSourceMatchesExpected = Boolean(sourceCommit && manager.sourceCommit === sourceCommit);
const pipelineSucceeded = latestPipelineRun.status === "True";
const pacReady = pacStatus.ok === true && pacSummary.ready === true;
const blockers = [
...(pacReady ? [] : ["pac-not-ready"]),
...(pipelineRunName !== null ? [] : ["pac-pipelinerun-missing"]),
...(pipelineRunName === null || pipelineSucceeded ? [] : ["pac-pipelinerun-not-succeeded"]),
...(expectedGitopsRevision !== null ? [] : ["pac-gitops-revision-unresolved"]),
...(expectedGitopsRevision === null || argoSyncedToGitops ? [] : ["argo-revision-stale"]),
...(runtimePayload.runtimeNamespaceExists === true ? [] : ["runtime-namespace-missing"]),
...(manager.deploymentExists === true ? [] : ["manager-deployment-missing"]),
...(manager.deploymentExists !== true || sourceCommit === null || managerSourceMatchesExpected ? [] : ["manager-source-stale"]),
...(manager.serviceExists === true ? [] : ["manager-service-missing"]),
...(spec.database.mode === "external-postgres" && database.secretPresent !== true ? ["database-secret-missing"] : []),
...(secrets.ready === true ? [] : ["lane-secret-missing"]),
...(spec.database.localPostgresExpectedAbsent && localPostgres.absent !== true ? ["local-postgres-present"] : []),
];
const aligned = blockers.length === 0 && pipelineSucceeded && argoSyncedToGitops && managerSourceMatchesExpected;
const runtimeAlignment = {
pipelineSucceeded,
argoRevision,
argoSyncedToGitops,
managerSourceCommit: stringOrNull(manager.sourceCommit),
managerSourceMatchesExpected,
runtimeAligned: blockers.every((blocker) => blocker.startsWith("pac-") || blocker === "argo-revision-stale"),
};
const nextAction = aligned
? { code: "none", summary: "PaC/Gitea migrated lane aligned; no action required", command: null }
: blockers.includes("argo-revision-stale")
? { code: "argo-refresh", summary: "GitOps commit is published but Argo has not observed it yet", command: `bun scripts/cli.ts agentrun control-plane refresh --node ${spec.nodeId} --lane ${spec.lane} --confirm` }
: { code: "inspect-pac-status", summary: `PaC/Gitea closeout blocked: ${blockers.join(", ")}`, command: pacStatusCommand(spec, true) };
const migration = {
migrated: true,
sourceAuthority: "gitea-pipelines-as-code",
triggerPath: "Gitea webhook -> Pipelines-as-Code -> Tekton -> GitOps/Argo -> k8s runtime",
legacyGitMirrorDisposition: "migration-readonly",
legacyTriggerCurrentDisabled: true,
valuesPrinted: false,
};
const summary = {
aligned,
runtimeAligned: runtimeAlignment.runtimeAligned,
blockers,
warnings: [],
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
migration,
pac: {
ready: pacSummary.ready ?? false,
webhookCount: pacSummary.webhookCount ?? null,
latestPipelineRun: {
name: pipelineRunName,
status: latestPipelineRun.status ?? null,
reason: latestPipelineRun.reason ?? null,
durationSeconds: latestPipelineRun.durationSeconds ?? null,
sourceCommit: stringOrNull(latestPipelineRun.sourceCommit),
},
taskRuns: pacSummary.taskRuns ?? [],
artifact: {
imageStatus: artifact.imageStatus ?? null,
envIdentity: artifact.envIdentity ?? null,
digest: artifact.digest ?? null,
gitopsCommit: expectedGitopsRevision,
sourceCommit,
valuesPrinted: false,
},
},
argo: {
syncStatus: pacArgo.sync ?? null,
healthStatus: pacArgo.health ?? null,
revision: argoRevision,
syncedToGitops: argoSyncedToGitops,
},
runtime: {
namespaceExists: runtimePayload.runtimeNamespaceExists ?? false,
manager: {
deploymentExists: manager.deploymentExists ?? false,
serviceExists: manager.serviceExists ?? false,
sourceCommit: stringOrNull(manager.sourceCommit),
sourceMatchesExpected: managerSourceMatchesExpected,
envIdentity: manager.envIdentity ?? null,
},
databaseSecretPresent: database.secretPresent ?? null,
secretsReady: secrets.ready ?? null,
localPostgresAbsent: localPostgres.absent ?? null,
},
nextAction,
};
const result: Record<string, unknown> = {
ok: runtimeProbe.value.exitCode === 0 && pacStatus.ok === true && blockers.length === 0,
command: "agentrun control-plane status",
mode: "pipelines-as-code-migrated-lane",
configPath: target.configPath,
target: options.full || options.raw ? agentRunLaneSummary(spec) : compactAgentRunLaneStatusTarget(spec),
summary,
alignment: {
aligned,
blockers,
warnings: [],
sourceCommit,
expectedPipelineRun: pipelineRunName,
expectedGitopsRevision,
runtimeAlignment,
migration,
},
timings: {
runtimeMs: runtimeProbe.elapsedMs,
totalMs: runtimeProbe.elapsedMs,
},
disclosure: {
output: options.full || options.raw ? "full" : "compact-summary",
full: options.full,
raw: options.raw,
legacyGitMirror: "migration-readonly",
fullCommand: agentRunControlPlaneStatusCommand(spec, options, true),
pacStatusCommand: pacStatusCommand(spec, true),
},
next: {
action: nextAction,
pacStatus: pacStatusCommand(spec),
giteaMirrorStatus: giteaMirrorStatusCommand(spec),
refresh: `bun scripts/cli.ts agentrun control-plane refresh --node ${spec.nodeId} --lane ${spec.lane} --confirm`,
triggerCurrent: null,
},
valuesPrinted: false,
};
if (options.full || options.raw || runtimeProbe.value.exitCode !== 0) {
result.captures = {
runtime: compactCapture(runtimeProbe.value, { full: options.full || options.raw || runtimeProbe.value.exitCode !== 0 }),
};
}
if (options.full || options.raw) {
result.pipelinesAsCode = pacStatus;
result.runtime = runtimePayload;
result.legacy = {
gitMirror: {
disposition: "migration-readonly",
readUrl: spec.gitMirror.readUrl,
note: "Legacy git-mirror status is not used as a closeout blocker for PaC/Gitea migrated lanes.",
},
};
}
return result;
}
export async function statusYamlLane(config: UniDeskConfig, options: StatusOptions, target: { configPath: string; spec: AgentRunLaneSpec }): Promise<Record<string, unknown>> {
const spec = target.spec;
if (isPipelinesAsCodeMigratedLane(spec)) return await statusPipelinesAsCodeLane(config, options, target);
const sourceProbe = await timedStatusStage("source", () => spec.source.statusMode === "k3s-git-mirror"
? capture(config, spec.nodeKubeRoute, ["sh", "--", yamlLaneK3sSourceStatusScript(spec)])
: capture(config, `${spec.nodeRoute}:${spec.source.workspace}`, ["sh", "--", yamlLaneSourceStatusScript(spec)]));
+54
View File
@@ -69,6 +69,60 @@ export function renderAgentRunControlPlaneStatusSummary(result: Record<string, u
const timings = record(result.timings);
const blockers = Array.isArray(summary.blockers) ? summary.blockers.map(String) : [];
const warnings = Array.isArray(summary.warnings) ? summary.warnings.map(String) : [];
const migration = record(summary.migration);
if (migration.migrated === true && migration.sourceAuthority === "gitea-pipelines-as-code") {
const pac = record(summary.pac);
const latestPipelineRun = record(pac.latestPipelineRun);
const artifact = record(pac.artifact);
const taskRuns = Array.isArray(pac.taskRuns) ? pac.taskRuns.map(record) : [];
const taskRows = taskRuns.slice(0, 8).map((taskRun) => [
displayValue(taskRun.name),
displayValue(taskRun.status),
displayValue(taskRun.reason),
displayValue(taskRun.durationSeconds),
]);
const lines = [
"AGENTRUN CONTROL-PLANE STATUS",
renderTable(
["NODE", "LANE", "MODE", "SOURCE", "PIPELINE", "ALIGNED", "RUNTIME", "BLOCKERS"],
[[
displayValue(node.id ?? target.node ?? "-"),
displayValue(target.lane ?? "-"),
"PaC/Gitea",
shortSha(summary.sourceCommit),
displayValue(summary.expectedPipelineRun ?? "-"),
yesNo(summary.aligned),
yesNo(summary.runtimeAligned),
blockers.length === 0 ? "-" : blockers.join(","),
]],
),
"",
renderTable(
["COMPONENT", "STATUS", "DETAIL"],
[
["trigger", yesNo(pac.ready), `webhooks=${displayValue(pac.webhookCount)} path=${displayValue(migration.triggerPath)}`],
["pipelinerun", yesNo(latestPipelineRun.status === "True"), `run=${displayValue(latestPipelineRun.name)} status=${displayValue(latestPipelineRun.status)} reason=${displayValue(latestPipelineRun.reason)} duration=${displayValue(latestPipelineRun.durationSeconds)}s`],
["image", yesNo(artifact.imageStatus === "reused" || artifact.imageStatus === "built"), `status=${displayValue(artifact.imageStatus)} env=${displayValue(artifact.envIdentity)} digest=${shortSha(artifact.digest)} gitops=${shortSha(artifact.gitopsCommit)}`],
["argo", yesNo(argo.syncedToGitops), `sync=${displayValue(argo.syncStatus)} health=${displayValue(argo.healthStatus)} revision=${shortSha(argo.revision)}`],
["runtime", yesNo(runtimeManager.sourceMatchesExpected), `manager=${shortSha(runtimeManager.sourceCommit)} secrets=${yesNo(runtime.secretsReady)} dbSecret=${yesNo(runtime.databaseSecretPresent)} localPgAbsent=${yesNo(runtime.localPostgresAbsent)}`],
],
),
"",
"TASKRUN DURATIONS",
taskRows.length === 0 ? "-" : renderTable(["TASKRUN", "STATUS", "REASON", "DURATION_S"], taskRows),
"",
warnings.length === 0 ? "WARNINGS\n-" : ["WARNINGS", ...warnings.map((warning) => `- ${warning}`)].join("\n"),
"",
"NEXT",
` action: ${displayValue(nextAction.summary ?? "-")}`,
nextAction.command ? ` run: ${displayValue(nextAction.command)}` : null,
` pac: ${displayValue(next.pacStatus ?? disclosure.pacStatusCommand ?? "-")}`,
` full: ${displayValue(next.statusFull ?? disclosure.fullCommand ?? "bun scripts/cli.ts agentrun control-plane status --full")}`,
"",
`TIMINGS runtime=${displayValue(timings.runtimeMs)}ms total=${displayValue(timings.totalMs)}ms`,
].filter((line): line is string => line !== null);
return renderedCliResult(result.ok !== false, "agentrun control-plane status", `${lines.join("\n")}\n`);
}
const sourceSnapshotMode = source.sourceAuthority === "git-mirror-snapshot" || source.snapshotPresent !== null;
const sourceStatus = sourceSnapshotMode ? yesNo(source.snapshotPresent) : yesNo(source.workspaceClean);
const sourceDetail = sourceSnapshotMode
+7 -4
View File
@@ -9,16 +9,19 @@ import type { GitHubOptions, GitHubTokenProbe } from "./types";
export function readIssueLifecycleCommentBody(options: GitHubOptions, command: string): { body: string; bodySource: Record<string, unknown> } | null {
if (options.comment === undefined && options.commentFile === undefined) return null;
if (options.comment !== undefined && options.commentFile !== undefined) {
throw new Error(`${command} --comment and --comment-file/--comment-stdin are mutually exclusive`);
if (options.comment !== undefined) {
throw new Error(`${command} inline --comment is unsupported; use --comment-stdin with a quoted heredoc`);
}
if (options.body !== undefined || options.bodyFile !== undefined) {
throw new Error(`${command} --comment/--comment-file/--comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
throw new Error(`${command} --comment-stdin cannot be combined with --body/--body-file/--body-stdin`);
}
if (options.commentFile !== undefined) {
if (options.commentFile !== "-") {
throw new Error(`${command} --comment-file is unsupported for lifecycle comments; use --comment-stdin with a quoted heredoc`);
}
return readIssueCommentBody({ ...options, body: undefined, bodyFile: options.commentFile });
}
return readIssueCommentBody({ ...options, body: options.comment, bodyFile: undefined });
return null;
}
export function tokenFromEnvironment(): GitHubTokenProbe {
+2 -1
View File
@@ -537,13 +537,14 @@ function renderScanEscape(result: GitHubCommandResult): string {
function renderIssueLifecycle(result: GitHubCommandResult): string {
const issue = record(result.issue);
const comment = record(result.comment);
const commentStatus = comment.id ?? (comment.planned === true ? "planned" : "-");
return [
`gh ${result.command} (${result.dryRun === true ? "dry-run" : "updated"})`,
"",
ghTable(["ISSUE", "STATE", "COMMENT", "UPDATED", "TITLE"], [[
`#${ghText(result.issueNumber ?? issue.number)}`,
ghText(issue.state ?? record(result.wouldPatch).state),
ghText(comment.id ?? "-"),
ghText(commentStatus),
shortDate(issue.updatedAt),
ghShort(ghText(issue.title), 96),
]]),
+4 -4
View File
@@ -29,7 +29,7 @@ export function ghHelp(): unknown {
"bun scripts/cli.ts gh issue comment patch <commentId> --body-patch-stdin [--repo owner/name] [--number N compat] [--dry-run] [--expect-updated-at ts|--expect-body-sha sha256]",
"bun scripts/cli.ts gh issue comment edit <commentId> (--body-stdin|--body-file <file|->|--body <short-text>) [--repo owner/name] [--number N compat] [--dry-run] [compatibility alias for issue comment update]",
"bun scripts/cli.ts gh issue comment delete <commentId> [--repo owner/name] [--number N compat] [--dry-run]",
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment <short-text>|--comment-stdin|--comment-file <file|->] [--dry-run]",
"bun scripts/cli.ts gh issue close|reopen <number> [--repo owner/name] [--number N compat] [--comment-stdin] [--dry-run]",
"bun scripts/cli.ts gh issue stale-close [--repo owner/name] [--inactive-hours N] [--limit N] [--label label[,label...]]... [--dry-run]",
"bun scripts/cli.ts gh issue delete <number> [unsupported: use close]",
"bun scripts/cli.ts gh issue scan-escape [--repo owner/name] [--limit N] [--dry-run]",
@@ -86,7 +86,7 @@ export function ghHelp(): unknown {
"issue patch reads the current GitHub issue body, applies a Codex apply_patch envelope against virtual file issue.md from --body-patch-stdin or --body-patch-file <file|->, then runs the same issue body guard before PATCH. It returns old/new bodySha, updatedAt, patch summary, and bounded previews; context mismatch fails with redacted diagnostics and no GitHub write.",
"issue comment view/read reads one comment by commentId. Default output is compact metadata plus bodyChars/bodySha/preview; --full includes that one full comment body. issue comment create/update/edit accept --body-stdin or --body-file <file|-> for Markdown/generated content and --body only for short single-line text. Blank, multiline, shell-polluted, secret-like, and overlong inline bodies fail structurally. Use comment update/edit to correct existing wording in place; delete remains for intentional removal.",
"issue comment patch reads the current issue comment by commentId, applies a Codex apply_patch envelope against virtual file comment.md, then PATCHes only that comment. It returns comment id, old/new bodySha, updatedAt, patch summary, and redacted mismatch diagnostics without echoing the full comment body.",
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment <short-text>, --comment-stdin, or --comment-file <file|-> posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin is the first-class heredoc path for generated Markdown closeout evidence; --comment remains the short inline form. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
"issue close/reopen default success output is compact and omits full issue.body. Optional --comment-stdin posts a bounded lifecycle comment before the state change and aborts the state change if the comment POST fails. --comment-stdin with a quoted heredoc is the only lifecycle closeout comment path; inline --comment and --comment-file are unsupported. Use gh issue view <number> --json body or --full/--raw on view when full text is needed.",
"issue stale-close is the reusable lifecycle cleanup path for policies such as closing open issues inactive for more than 48 hours. It selects open issues by GitHub updatedAt older than observedAt - --inactive-hours, treats comments and state changes as activity, filters pull requests, supports --dry-run, and returns bounded candidate/closed/failure summaries without echoing full bodies.",
"For one-shot issue writes, prefer quoted heredoc stdin: bun scripts/cli.ts gh issue update <number> --repo owner/name --body-stdin <<'EOF' ... EOF or gh issue comment create <number> --body-stdin <<'EOF' ... EOF. --body-file <file|-> remains available for reusable files and pipes.",
"For JSON request bodies in other CLI namespaces, prefer --body-file or --body-stdin over long inline shell arguments. GitHub issue/PR Markdown writes use --body-stdin for heredoc/stdin or --body-file <file|-> for reusable files.",
@@ -164,8 +164,8 @@ export function ghScopedHelpNotes(tokens: string[]): string[] {
notes.push("Use `issue comments <number>` as the low-friction recent-progress path; default output is bounded and structured output is at `.data.comments`.");
notes.push("`--full` or `--raw` includes full bodies for the bounded recent list without changing the stable top-level `comments` path.");
} else if (key === "issue close" || key === "issue reopen") {
notes.push("Issue close/reopen can post a lifecycle comment with `--comment`, `--comment-stdin`, or `--comment-file <file|->` before changing state.");
notes.push("For long closeout evidence, prefer `--comment-stdin` with a quoted heredoc.");
notes.push("Issue close/reopen can post a lifecycle comment with `--comment-stdin` before changing state.");
notes.push("Use `--comment-stdin` with a quoted heredoc for closeout evidence; inline `--comment` and `--comment-file` are unsupported.");
} else if (key === "pr comment" || key.startsWith("pr comment ")) {
notes.push("PR comments are GitHub issue comments under the hood; use comment id targets for update/edit/delete.");
notes.push("Use `pr comment view <commentId> --full` to read one full comment body by id.");
+16 -2
View File
@@ -194,9 +194,23 @@ export async function runGhCommand(args: string[]): Promise<GitHubCommandResult
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
return validationError(command, options.repo, "--inactive-hours is only supported by gh issue stale-close");
}
if (optionWasProvided(args, "--comment") && !(top === "issue" && (sub === "close" || sub === "reopen"))) {
if (optionWasProvided(args, "--comment")) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
return validationError(command, options.repo, "--comment/--comment-file is only supported by gh issue close/reopen; use gh issue comment create for standalone comments");
return validationError(command, options.repo, "inline --comment is unsupported because shell quoting corrupts Markdown; use --comment-stdin with a quoted heredoc", {
supportedCommands: [
"bun scripts/cli.ts gh issue close <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
"bun scripts/cli.ts gh issue reopen <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
],
});
}
if (optionWasProvided(args, "--comment-file")) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
return validationError(command, options.repo, "--comment-file is unsupported for issue lifecycle comments; use --comment-stdin with a quoted heredoc", {
supportedCommands: [
"bun scripts/cli.ts gh issue close <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
"bun scripts/cli.ts gh issue reopen <number> --repo owner/name --comment-stdin <<'EOF'\n<comment markdown>\nEOF",
],
});
}
if ((optionWasProvided(args, "--description") || optionWasProvided(args, "--private") || optionWasProvided(args, "--public") || optionWasProvided(args, "--auto-init")) && !(top === "repo" && sub === "create")) {
const command = [top, sub].filter((value): value is string => value !== undefined).join(" ") || "gh";
+50 -10
View File
@@ -10,6 +10,8 @@ import { ghShort, ghTable, ghText } from "./render";
import { GITHUB_REST_PAGE_SIZE, ISSUE_LIST_JSON_FIELDS, MAX_ISSUE_LIST_LIMIT } from "./types";
import type { GitHubCommandResult, IssueListJsonField, IssueListState } from "./types";
const COMPACT_JSON_ISSUE_LIMIT = 40;
export function shellWord(value: string): string {
return JSON.stringify(value);
}
@@ -82,6 +84,21 @@ export async function issueList(repo: string, token: string, state: IssueListSta
? listedIssues.filter((issue) => issue.title.startsWith(normalizedTitlePrefix))
: listedIssues;
const fields = jsonFields ?? ISSUE_LIST_JSON_FIELDS.slice();
const selectedIssues = issues.map((issue) => issueListSummary(issue, fields));
const compactJson = jsonFields !== undefined && !noDump;
const returnedIssues = compactJson ? selectedIssues.slice(0, COMPACT_JSON_ISSUE_LIMIT) : selectedIssues;
const omittedIssues = Math.max(0, selectedIssues.length - returnedIssues.length);
const fullCommand = [
"bun scripts/cli.ts gh issue list",
"--repo", shellWord(repo),
"--state", state,
"--limit", String(limit),
...(normalizedSearch.length > 0 ? ["--search", shellWord(normalizedSearch)] : []),
...(normalizedTitlePrefix.length > 0 ? ["--title-prefix", shellWord(normalizedTitlePrefix)] : []),
...labels.flatMap((label) => ["--label", shellWord(label)]),
"--json", shellWord(fields.join(",")),
"--full",
].join(" ");
const payload: GitHubCommandResult = {
ok: true,
command: "issue list",
@@ -109,10 +126,29 @@ export async function issueList(repo: string, token: string, state: IssueListSta
rawCount: result.rawCount,
searchTotalCount: result.searchTotalCount,
searchIncomplete: result.searchIncomplete,
pagination: issueListPaginationSummary(result),
...(compactJson ? {} : { pagination: issueListPaginationSummary(result) }),
hasMore: result.hasMore,
jsonFields: fields,
issues: issues.map((issue) => issueListSummary(issue, fields)),
issues: returnedIssues,
...(compactJson
? {
issueListDisclosure: {
mode: "bounded-json",
returned: returnedIssues.length,
omitted: omittedIssues,
totalMatchedInBoundedScan: issues.length,
inlineLimit: COMPACT_JSON_ISSUE_LIMIT,
fullCommand,
note: "Default --json issue list output is bounded for interactive use; add --full for complete selected fields and pagination/request metadata.",
},
}
: {}),
...(omittedIssues > 0
? {
omittedIssues,
fullCommand,
}
: {}),
...(result.hasMore && limit < MAX_ISSUE_LIST_LIMIT
? {
next: {
@@ -129,14 +165,18 @@ export async function issueList(repo: string, token: string, state: IssueListSta
},
}
: {}),
request: {
method: "GET",
path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues",
query: normalizedSearch
? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE }
: { state, labels, per_page: GITHUB_REST_PAGE_SIZE },
localTitlePrefixFilter: normalizedTitlePrefix || null,
},
...(compactJson
? {}
: {
request: {
method: "GET",
path: normalizedSearch ? "/search/issues" : "/repos/{owner}/{repo}/issues",
query: normalizedSearch
? { q: `${normalizedSearch} repo:${repo} type:issue${state === "all" ? "" : ` state:${state}`}${labels.map((label) => ` ${githubSearchLabelQualifier(label)}`).join("")}`, per_page: GITHUB_REST_PAGE_SIZE }
: { state, labels, per_page: GITHUB_REST_PAGE_SIZE },
localTitlePrefixFilter: normalizedTitlePrefix || null,
},
}),
note: "GitHub's issues endpoint may include pull requests; this command filters pull requests from .issues.",
};
return jsonFields === undefined && !noDump ? withIssueListRendered(payload) : payload;