From 79e9288d5fa40d737e3b2ebab15517cc2fd4d3eb Mon Sep 17 00:00:00 2001 From: Codex Date: Sun, 5 Jul 2026 12:46:49 +0000 Subject: [PATCH] ci: finalize pac status and gh heredoc guard --- .agents/skills/unidesk-cicd/SKILL.md | 8 +- .../unidesk-cicd/references/agentrun.md | 20 +- .../references/branch-follower.md | 4 +- .../unidesk-cicd/references/platform-ops.md | 4 +- .agents/skills/unidesk-gh/SKILL.md | 2 +- .../skills/unidesk-gh/references/issues.md | 1 + docs/reference/agentrun.md | 8 +- docs/reference/cli.md | 6 +- docs/reference/platform-infra.md | 9 + scripts/src/agentrun/control-plane.ts | 181 ++++++++++++++++++ scripts/src/agentrun/public-exposure.ts | 54 ++++++ scripts/src/gh/auth-and-safety.ts | 11 +- scripts/src/gh/default-render.ts | 3 +- scripts/src/gh/help.ts | 8 +- scripts/src/gh/index.ts | 18 +- scripts/src/gh/issue-list.ts | 60 +++++- 16 files changed, 364 insertions(+), 33 deletions(-) diff --git a/.agents/skills/unidesk-cicd/SKILL.md b/.agents/skills/unidesk-cicd/SKILL.md index 55a3edc3..97581ccb 100644 --- a/.agents/skills/unidesk-cicd/SKILL.md +++ b/.agents/skills/unidesk-cicd/SKILL.md @@ -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/.../` stage ref;build/status/publish 只消费该 snapshot,host worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。 -- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRef:branch-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 authority:legacy lane 使用 k8s git-mirror snapshot,迁移 lane 使用 Gitea controlled mirror + immutable snapshot ref。受控命令先在 k8s 内同步/创建不可变 `refs/unidesk/snapshots/.../` stage ref;build/status/publish 只消费该 snapshot,host worktree、本地 `git fetch/pull`、可变 branch ref 或 Pipeline 内直连 GitHub 都不能作为 authoritative source。 +- GitHub/Git 相关 egress 必须走 YAML-first host proxy/sourceRef:branch-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 checkout;controller 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 读取并配置到满足该目标。 diff --git a/.agents/skills/unidesk-cicd/references/agentrun.md b/.agents/skills/unidesk-cicd/references/agentrun.md index 4e0016b1..89d42f32 100644 --- a/.agents/skills/unidesk-cicd/references/agentrun.md +++ b/.agents/skills/unidesk-cicd/references/agentrun.md @@ -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 |--source-commit ] [--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//`,再从该 snapshot 构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalog,flush 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//`,再从该 snapshot 构建并推送 YAML 声明的 image,渲染 GitOps/artifact catalog,flush git-mirror 并创建 provenance PipelineRun。迁移到 PaC/Gitea 的 lane 不再通过该自维护触发器提交 CI。 - `cleanup-runners`: 只清 YAML 选中 lane runtime namespace 中匹配 `deployment.runner.retention.selectors` 的 runner Job/Pod;runner 上限、最后活跃排序、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 ` 会通过 `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 URL,runtime 可能因 `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` 作为长期事实。 diff --git a/.agents/skills/unidesk-cicd/references/branch-follower.md b/.agents/skills/unidesk-cicd/references/branch-follower.md index b650e9df..a0e357d2 100644 --- a/.agents/skills/unidesk-cicd/references/branch-follower.md +++ b/.agents/skills/unidesk-cicd/references/branch-follower.md @@ -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 diff --git a/.agents/skills/unidesk-cicd/references/platform-ops.md b/.agents/skills/unidesk-cicd/references/platform-ops.md index b127c0c4..301b6378 100644 --- a/.agents/skills/unidesk-cicd/references/platform-ops.md +++ b/.agents/skills/unidesk-cicd/references/platform-ops.md @@ -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 diff --git a/.agents/skills/unidesk-gh/SKILL.md b/.agents/skills/unidesk-gh/SKILL.md index 355cedab..9167aa54 100644 --- a/.agents/skills/unidesk-gh/SKILL.md +++ b/.agents/skills/unidesk-gh/SKILL.md @@ -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`。 ## 常用入口 diff --git a/.agents/skills/unidesk-gh/references/issues.md b/.agents/skills/unidesk-gh/references/issues.md index ffae0c58..141f4180 100644 --- a/.agents/skills/unidesk-gh/references/issues.md +++ b/.agents/skills/unidesk-gh/references/issues.md @@ -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. diff --git a/docs/reference/agentrun.md b/docs/reference/agentrun.md index 956c472c..9b48f1e2 100644 --- a/docs/reference/agentrun.md +++ b/docs/reference/agentrun.md @@ -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//`,`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 ` 与 `--source-commit ` 定点查询;`--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 ` 轮询。`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//`,`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 ` 与 `--source-commit ` 定点查询;`--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 ` 轮询。`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 --tail-bytes 12000` 观察 `agentrun-yaml-lane-trigger` progress,再用 `agentrun control-plane status --node --lane --pipeline-run ` 观察 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 重新触发。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 9e0554b8..253084d7 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -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//` 和本地 `.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 语义失败。 - `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:/`、`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 可能混入 PR,CLI 会从 `.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 --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close ` / `gh issue reopen `(PR 用 `gh pr close|reopen `),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;人工正文读取优先使用 `trans gh:/owner/repo/issue/ 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 --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close --comment <短引用>` 关闭。issue 硬删除走 `close`,PR 硬删除走 `close`,两者都没有"delete"语义。 -- `gh issue comment create --repo owner/name --body-stdin`、`gh issue comment update|edit --repo owner/name --body-stdin`、`gh issue comment delete --repo owner/name`、`gh issue close --repo owner/name [--comment |--comment-stdin]`、`gh issue reopen --repo owner/name [--comment |--comment-stdin]`、`gh issue update --repo owner/name [--title ...] [--body-stdin]`、`gh issue edit ...`、`gh issue board-row get|update|add|move|delete|upsert --repo owner/name ...` 都接受与 `gh issue view|read`、`gh pr *` 一致的 `owner/repo#number` 位置 shorthand;shorthand 与显式 `--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` 已长期支持该 shorthand;comment 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 --state closed` 落到错命令上时,CLI 必须返回 `validation-failed` 并显式提示 `gh issue close ` / `gh issue reopen `(PR 用 `gh pr close|reopen `),并把 5 条受支持命令放进 `supportedCommands`,禁止把"无 `--state` 改 issue 状态"的命令升级为"接受 `--state`"。`gh issue close|reopen` 成功输出默认是 compact issue 摘要,不得回显完整 `issue.body`;人工正文读取优先使用 `trans gh:/owner/repo/issue/ 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 --body-stdin <<'EOF'` 写证据评论,再用 `gh issue close --comment-stdin <<'EOF'` 写简短中文收口。issue 硬删除走 `close`,PR 硬删除走 `close`,两者都没有"delete"语义。 +- `gh issue comment create --repo owner/name --body-stdin`、`gh issue comment update|edit --repo owner/name --body-stdin`、`gh issue comment delete --repo owner/name`、`gh issue close --repo owner/name [--comment-stdin]`、`gh issue reopen --repo owner/name [--comment-stdin]`、`gh issue update --repo owner/name [--title ...] [--body-stdin]`、`gh issue edit ...`、`gh issue board-row get|update|add|move|delete|upsert --repo owner/name ...` 都接受与 `gh issue view|read`、`gh pr *` 一致的 `owner/repo#number` 位置 shorthand;shorthand 与显式 `--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` 已长期支持该 shorthand;comment 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 [--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/` 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 --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 --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 payload,GitHub 返回不存在 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 payload,GitHub 返回不存在 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 北京时间` 段落发送给 ClaudeQQ,ClaudeQQ 失败不会回滚 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` 调整开关、目标和超时。 diff --git a/docs/reference/platform-infra.md b/docs/reference/platform-infra.md index 22c2cb09..ca7ef04d 100644 --- a/docs/reference/platform-infra.md +++ b/docs/reference/platform-infra.md @@ -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. diff --git a/scripts/src/agentrun/control-plane.ts b/scripts/src/agentrun/control-plane.ts index fa6ac8f0..4d948392 100644 --- a/scripts/src/agentrun/control-plane.ts +++ b/scripts/src/agentrun/control-plane.ts @@ -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)])); diff --git a/scripts/src/agentrun/public-exposure.ts b/scripts/src/agentrun/public-exposure.ts index 46109d76..bbdc5197 100644 --- a/scripts/src/agentrun/public-exposure.ts +++ b/scripts/src/agentrun/public-exposure.ts @@ -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 diff --git a/scripts/src/gh/auth-and-safety.ts b/scripts/src/gh/auth-and-safety.ts index 18e3f59f..d178484f 100644 --- a/scripts/src/gh/auth-and-safety.ts +++ b/scripts/src/gh/auth-and-safety.ts @@ -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 { diff --git a/scripts/src/gh/default-render.ts b/scripts/src/gh/default-render.ts index b1c1726e..005a6d27 100644 --- a/scripts/src/gh/default-render.ts +++ b/scripts/src/gh/default-render.ts @@ -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), ]]), diff --git a/scripts/src/gh/help.ts b/scripts/src/gh/help.ts index 837a4f7d..342fbded 100644 --- a/scripts/src/gh/help.ts +++ b/scripts/src/gh/help.ts @@ -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."); diff --git a/scripts/src/gh/index.ts b/scripts/src/gh/index.ts index aaf90076..283d688c 100644 --- a/scripts/src/gh/index.ts +++ b/scripts/src/gh/index.ts @@ -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"; diff --git a/scripts/src/gh/issue-list.ts b/scripts/src/gh/issue-list.ts index 212dafae..4a3d7097 100644 --- a/scripts/src/gh/issue-list.ts +++ b/scripts/src/gh/issue-list.ts @@ -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;