diff --git a/docs/reference/agentrun.md b/docs/reference/agentrun.md index 968cd599..77119f2a 100644 --- a/docs/reference/agentrun.md +++ b/docs/reference/agentrun.md @@ -133,7 +133,7 @@ HWLAB 负责自身产品和接入层,包括用户鉴权、Cloud Web/CLI 对外 直接通过 AgentRun manager、`dispatchHwlabAgentRun()` 或手写 runner job 发起的 canary 只能证明 AgentRun 基础设施和凭据投影本身可用,不能证明 HWLAB Cloud Web/Cloud API 的产品入口已经正确请求这些能力。涉及 Cloud Web Workbench、用户会话、conversation/session/thread、AgentRun runtime assembly 或业务授权的 issue,必须用 HWLAB 的 Web dispatcher 原入口,或调用同一 dispatcher 的 CLI 验证。当前 HWLAB v0.2 到 AgentRun 的资源装配权威是 HWLAB `docs/reference/agentrun-code-agent-dispatch.md` 和 AgentRun `docs/reference/spec-v01-runtime-assembly.md`:`ResourceBundleRef.kind="gitbundle"` 通过 `bundles[]` 装配 `tools/` 和 `.agents/skills`,旧 `toolAliases` / `skillRefs` / `workspaceFiles` 不再是有效接入口。若消费侧 Web dispatcher 没有按该契约传递 `gitbundle`、tool credential 或 transient env,应归为 HWLAB 接入层问题;若 dispatcher 已正确请求但 AgentRun runner 没有装配,应归为 AgentRun 执行基础设施问题。 -HWLAB 的 `gitbundle` checkout authority 是 repo URL + workspace ref,而不是 cloud-api artifact revision。默认路径必须通过 G14 git mirror 拉取 HWLAB `v0.2` ref,AgentRun runner 物化后记录实际 commit;cloud-api、CI/CD 或 rollout 注入的 `commitId` 只可作为 requested hint 或显式 pin 的输入,不得作为默认 materialization 来源。关闭相关 issue 时,证据必须同时显示 `repoUrl`、`requestedRef`、actual `commitId`,以及 `bundles/tools/promptRefs/skillDirs` 摘要;若 actual `commitId` 仍等于旧 cloud-api rollout commit 且不是显式 pin,应继续归为 AgentRun bundle 物化问题。 +HWLAB 与 UniDesk/Artificer 的 `gitbundle` checkout authority 是 repo URL + workspace ref,而不是 cloud-api artifact revision、AipodSpec mirror 开关或运行时 prompt。`ResourceBundleRef` / AipodSpec 必须继续声明无明文凭据的 GitHub repo URL;Git mirror 是 G14/AgentRun 基础设施能力,由 runner 在物化阶段自动把 GitHub URL 改写到受控 mirror read URL。不得在 AipodSpec、Queue task、prompt 或业务 adapter 中声明 `gitMirror`、mirror base URL 或 direct/mirror 分支开关。AgentRun runner 物化后必须记录原始 `repoUrl`、实际 `fetchRepoUrl`、`mirrorUsed`、`mirrorBaseUrl`、requested ref/commit 和 actual `commitId`;devops-infra mirror cache 必须覆盖 Artificer 和 HWLAB 常用 bundle repo,缺 cache 属于基础设施缺口,不能通过让 AipodSpec 直连 GitHub 来绕过。cloud-api、CI/CD 或 rollout 注入的 `commitId` 只可作为 requested hint 或显式 pin 的输入,不得作为默认 materialization 来源。关闭相关 issue 时,证据必须同时显示 `repoUrl`、`requestedRef`、actual `commitId`,以及 `bundles/tools/promptRefs/skillDirs` 摘要;若 actual `commitId` 仍等于旧 cloud-api rollout commit 且不是显式 pin,应继续归为 AgentRun bundle 物化问题。 HWLAB CaseRun 需要专用 skill 时,skill 必须通过 AgentRun `gitbundle` resource bundle 装配给 Code Agent,subject repo 只作为待修改源码来源,不能携带 `.agents/skills` 副本。收口证据应同时包含正向装配和负向隔离:AgentRun trace 或 CaseRun 归档显示 `resource-bundle-materialized`、`resourceBundlePolicy` 和 `.agents/skills//SKILL.md` 读取;subject repo diff 或 artifact 中没有新增 `.agents/skills`。若 runner 已按 `gitbundle` 装配但 HWLAB case 仍把 skill 复制进 subject repo,应归为 HWLAB CaseRun 接入层问题;若 HWLAB 已按契约请求而 runner 未物化 skill,则归为 AgentRun bundle 物化问题。 diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 51eefe43..0b555998 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -55,7 +55,7 @@ CI/CD、GitOps、rollout、artifact 发布、PR 合并后的 runtime lane 滚动 - `hwlab g14 monitor-prs --lane v02` 是 HWLAB `v0.2` 的 PR -> CI -> CD 自动化入口。它只监控 base=`v0.2` 的 open PR:每轮先用 UniDesk `gh pr preflight` 读取 GitHub CI/checks、mergeability 和冲突状态;pending 时在 PR 下写等待评论,blocked/conflict 时写阻塞评论;ready 时直接用 UniDesk `gh pr merge` 合并,不因为其他 commit 的运行中 PipelineRun 阻塞 merge 或 CI 启动。合并后执行受控 `control-plane trigger-current --lane v02 --confirm --wait`、轮询定点 `control-plane status --lane v02 --source-commit `,必要时执行 `git-mirror flush --confirm --wait`。v0.2 CD 采用 latest-only:旧 PipelineRun 不取消、不等待,但 promotion 写 `v0.2-gitops` 前必须重新确认 source head,stale commit 只能以 superseded/no-op 收口,不能回滚 runtime。不管 CD 成功、superseded、失败或超时,都在原 PR 下用 `gh pr comment create --body-stdin <<'EOF'` 追加语义化状态,正文固定包含起止时间、总耗时、冲突状态、CI/preflight conclusion、source commit、PipelineRun、targetValidation、Argo/webAssets 和 git mirror pendingFlush/githubInSync。评论去重状态写入 `.state/hwlab-g14/v02-pr-comment-signatures.json`,同一状态签名不会重复刷评论;v0.2 monitor 指针使用 `.state/hwlab-g14/latest-v02-monitor-job.json`、`latest-v02-once-job.json`、`latest-v02-dry-run-job.json` 和 `latest-v02-once-dry-run-job.json`,不会覆盖默认 G14 monitor 指针。`--lane v02 --once --dry-run` 只做单轮 preflight/merge/CD/comment plan,不写 GitHub、不触发 CD。 - `hwlab g14 monitor-prs --lane v03` 是 HWLAB `v0.3` 的 PR -> CI -> 自动合并 -> CD 入口。它只监控 base=`v0.3` 的 open PR:每轮先通过 UniDesk `gh pr preflight` 读取 GitHub checks、mergeability 和冲突状态;pending 时只在 PR 下写等待评论;失败 check、preflight blocker 或 conflict 时在 PR 下写阻塞评论,并按标题去重创建或更新 HWLAB failure issue。ready 时通过 UniDesk `gh pr merge` 合并,随后执行 runtime lane `control-plane trigger-current --lane v03 --confirm --wait`,轮询 `control-plane status --lane v03 --source-commit `,判定 PipelineRun `True`、Argo `Synced/Healthy`、`hwlab-v03` runtime workload 可见、20666/20667 public probes 通过,并在必要时执行 `git-mirror flush --lane v03 --confirm --wait`。CD 成功、失败或超时都会在原 PR 下写语义化状态评论;失败和超时同时创建或更新 failure issue,正文必须包含 PR、base/head、commit、PipelineRun、失败阶段、preflight/CD 摘要和下一步 CLI。评论去重状态写入 `.state/hwlab-g14/v03-pr-comment-signatures.json`,monitor 指针使用 `.state/hwlab-g14/latest-v03-monitor-job.json`、`latest-v03-once-job.json`、`latest-v03-dry-run-job.json` 和 `latest-v03-once-dry-run-job.json`。`--lane v03 --once --dry-run` 只做单轮 preflight/merge/CD/comment/issue plan,不写 GitHub、不触发 CD。 - `agentrun control-plane status|trigger-current|refresh|cleanup-runs|cleanup-released-pvs [--dry-run|--confirm]` 是 AgentRun `v0.1` 在 G14 k3s 的受控 Tekton/Argo 入口。`status` 只读汇总固定 source worktree commit、对应 commit-pinned PipelineRun、GitOps latest、Argo Application、`agentrun-v01` manager source commit、`planArtifacts.summary`、env image result 和 git mirror 摘要,并报告 manager/Argo/GitOps 是否对齐当前 source commit。默认输出是 compact commander 视图:`summary` 给出 source、PipelineRun、Argo、manager image、git mirror 和 `aligned` 结论;`timings` 给出 `sourceMs`、`runtimeMs`、`gitMirrorMs` 和 `totalMs`;远端 stdout/stderr tail 默认省略,失败时仍展开必要 tail,完整 tail 用 `--full`,原始 git mirror cache 用 `--raw`。`status` 聚合 source 后会并行读取 runtime 和 git mirror,并向 stderr 输出 `agentrun.control-plane.status.progress` JSON 事件,覆盖 `source`、`runtime`、`git-mirror` 的 started/succeeded/failed 和 elapsedMs,避免 10s 以上状态聚合期间无可见进展;`trigger-current` 先快进 `G14:/root/agentrun-v01` 到 `origin/v0.1`,检查 `devops-infra` mirror 的 `localV01` 是否等于目标 source commit,必要时先执行受控 mirror sync,再创建 `agentrun-v01-ci-` PipelineRun。confirmed trigger 只提交 CI/CD 工作并返回后续 `status` 命令,不等待完整 PipelineRun;同名 PipelineRun 运行中或已成功时拒绝重复触发,只允许失败态重建或首次创建。`refresh` 只对 `argocd/agentrun-g14-v01` 执行 hard refresh,用于 GitOps promotion 已完成但 Argo 仍停留旧 revision 时的受控同步入口;它不直接 patch runtime workload。`cleanup-runs` 只清理 `agentrun-ci` 中已完成且超过 `--min-age-minutes` 的 `agentrun-v01-ci-*` PipelineRun,通过 Tekton ownerRef 回收临时 workspace PVC;dry-run 必须输出候选 PipelineRun、owned PVC、active mount 保护、local-path 实际估算 bytes 和 confirm 命令。`cleanup-released-pvs` 只处理 `agentrun-ci`、`local-path`、`Delete` reclaim policy 的 `Released` PV,用于 PipelineRun 删除后残留 PV 的二次回收;它不触碰 runtime namespace、业务 PVC、Secret、registry storage 或 GitOps desired state。AgentRun 运行时和 SPEC 事实来源仍在 AgentRun 仓库,UniDesk 只维护受控运维入口。 -- `agentrun git-mirror status|sync|flush [--dry-run|--confirm]` 是 AgentRun `v0.1` 使用 `devops-infra` git mirror/relay 的受控维护入口。`status` 默认返回 read/write URL、`localV01`、`githubV01`、`localGitops`、`githubGitops`、`pendingFlush`、`githubInSync` 和 exact full-SHA shallow fetch 摘要,不默认展开完整 cache stdout;需要探测 tail 时用 `--full`,需要原始 cache 输出时用 `--raw`。`sync` 创建 manual Job,把 GitHub `v0.1` 和 `v0.1-gitops` refs 拉入 `/cache/pikasTech/agentrun.git`;`flush` 把本地 `v0.1-gitops` 快进推回 GitHub。confirmed `sync`/`flush` 默认创建 `.state/jobs/` 异步 job 并立刻返回 `job.id`、`statusCommand` 和日志路径;只有现场同步调试才显式加 `--wait`。该入口与 HWLAB v0.2 mirror 共用 `devops-infra` 服务和 cache PVC,但 repo path、refs、status 文件和 CLI 命令彼此独立。 +- `agentrun git-mirror status|sync|flush [--dry-run|--confirm]` 是 AgentRun `v0.1` 使用 `devops-infra` git mirror/relay 的受控维护入口。`status` 默认返回 read/write URL、`localV01`、`githubV01`、`localGitops`、`githubGitops`、`pendingFlush`、`githubInSync`、exact full-SHA shallow fetch 摘要和 Artificer bundle repo mirror 覆盖状态,不默认展开完整 cache stdout;需要探测 tail 时用 `--full`,需要原始 cache 输出时用 `--raw`。`sync` 创建 manual Job,把 GitHub `pikasTech/agentrun` 的 `v0.1` / `v0.1-gitops` refs 以及 Artificer 默认 bundle repo `pikasTech/unidesk`、`pikasTech/agent_skills` 的 `master` refs 拉入 `/cache/pikasTech/*.git`;`flush` 仍只把本地 `pikasTech/agentrun` 的 `v0.1-gitops` 快进推回 GitHub。AgentRun runner 的 `ResourceBundleRef` / AipodSpec 只写 GitHub URL,不写 `gitMirror` 字段;mirror 自动改写属于基础设施能力。confirmed `sync`/`flush` 默认创建 `.state/jobs/` 异步 job 并立刻返回 `job.id`、`statusCommand` 和日志路径;只有现场同步调试才显式加 `--wait`。该入口与 HWLAB v0.2 mirror 共用 `devops-infra` 服务和 cache PVC,但 repo path、refs、status 文件和 CLI 命令彼此独立。 - `hwlab g14 control-plane status|apply --lane v02 [--dry-run|--confirm]` 是 HWLAB `v0.2` 加法 lane 的受控 Tekton/Argo 控制面维护入口,source commit 只来自 G14 专用 bare repo `/root/hwlab-v02-cicd.git` 的 `refs/remotes/origin/v0.2`;`/root/hwlab-v02` 只作为人工开发和短连接源码工具 workspace 被观测,dirty/stale 状态必须输出为 isolated warning 而不能阻塞 CI/CD。该入口面向 branch `v0.2`、namespace `hwlab-ci` 和 Argo application `hwlab-g14-v02`;默认 `status` 只读汇总最新 source head 的 pipeline、RBAC/ServiceAccount、Argo、当前 commit PipelineRun、当前 PipelineRun 的 TaskRun 条件摘要、最近 PipelineRun 摘要、活跃 PipelineRun、遗留 v02 CronJob 清理状态、commit alignment,以及 19666/19667 的 Cloud Web 静态资源和 API live 探针。分支被后续提交推进后,要复查已完成 run 时使用 `status --lane v02 --pipeline-run hwlab-v02-ci-poll-`;已知完整 source SHA 但不想依赖最新 head 时使用 `status --lane v02 --source-commit `。定点 `status` 输出 `statusTarget.mode` 和 `targetValidation`,只检查指定 PipelineRun/source commit 的证据;`targetValidation.state=passed` 表示该目标已满足 PipelineRun succeeded、Argo `Synced/Healthy`、19666/19667 探针、Git mirror flushed,并且该 run 的 `planArtifacts.rolloutServices` 运行时 source commit 对齐;`planArtifacts.reusedServices` 作为 runtime/provenance 证据呈现,但不能被强制要求等于目标 source commit。`targetValidation.state=superseded` 表示该目标已成功且 runtime 已被同一分支后续成功 PipelineRun 取代,`falseGreenGuard` 在该状态下应标为 superseded/not-applicable。两种状态都不得因为 `origin/v0.2` 后续推进而把历史 run 判为失败;默认不带定点参数时仍严格判定最新 source head alignment。TaskRun 摘要的 `performance` 字段会把超过 120s 的 build TaskRun 标为慢任务、超过 180s 标为 critical warning,用于暴露 env reuse/git mirror 命中率回归,但不作为阻断门禁;CI/CD 性能验收应同时看 `planArtifacts.summary`、`taskRuns.performance.warningCount` 和 PipelineRun duration,纯 CLI/文档或无 runtime 重建需求的后续提交应稳定表现为 `build=0 reuse=` 且无 build TaskRun warning,首次引入或切换 env image 时允许只构建必要 env image 一次。`webAssets` 必须直接给出 `readonly-rpc` 删除、sidebar/workspace/event panel 关键 CSS、`/app.js` 是否可读取和字节数、`/health/live` 与 API revision;`apiRevision` 是 cloud-api 服务自身 revision,Cloud Web 静态资源变更时允许它与 source commit 不同,不能把这种差异误判成 Cloud Web 未发布。默认只读取必要字段,禁止把完整 PipelineRun spec、Tekton 内联脚本、历史大对象或整份 CSS/HTML/JS 展开到默认输出;`apply` 先自动 fetch `/root/hwlab-v02-cicd.git` 并从 commit-pinned detached worktree 执行 render check,再经 `G14:k3s` server-side apply `tekton-v02/rbac.yaml`、`pipeline.yaml`、`argocd/project.yaml` 和 `argocd/application-v02.yaml`,confirmed apply 会删除遗留 v02 CronJob,但不会应用 runtime-v02 workload、Secret 或数据迁移。 - `hwlab nodes control-plane status|apply|refresh|trigger-current --node G14 --lane v03` 使用 `/root/unidesk/config/hwlab-node-lanes.yaml` 生成 runtime lane spec。该 YAML 同时声明 `nodes.G14`、`lanes.v02/v03`、`networkProfiles` 和 `downloadProfiles`;status 默认输出 `expected.configPath`、node、network/download profile、有效 `NO_PROXY`、Docker build proxy、Argo Application 和 runtime workload 摘要,confirmed trigger 创建的 PipelineRun annotation 记录 node/profile id。`apply` 和 `trigger-current` 的控制面 apply 只安装标准 node-scoped Argo Application,并通过同一受控入口清理旧式 `hwlab-g14-vNN` runtime lane Application,避免两个 Argo Application 同时管理同一 runtime namespace;该清理不删除 runtime workload、Secret 或 GitOps desired state。`refresh` 是受控 Argo 恢复入口,只终止 stale Application operation 并执行 hard refresh,不直接 patch runtime workload;用于 GitOps revision 已更新但 Argo 仍卡在旧 hook/operation 时让 Argo 重新从当前 desired state 自愈。新增 `v0.4+` lane 时应先加 YAML,再让 GitOps render、CI prepare-source/catalog fetch、runtime env 注入和 Secret/DB/bootstrap helper 消费同一 spec;不要在脚本、CI manifest 或 helper 默认值里新增散落的 G14 代理、下载源或端口硬编码。`hyueapi.com` / `.hyueapi.com` 是强制保留的 `NO_PROXY` 条目,lane 覆盖不得移除。 - `hwlab g14 control-plane trigger-current --lane v02 [--dry-run|--confirm]` 是 v02 标准手动触发入口:先自动 fetch `/root/hwlab-v02-cicd.git`,解析当前 `origin/v0.2` full SHA,创建 commit-pinned `hwlab-v02-ci-poll-` PipelineRun;读 Git 走 `git-mirror-http.devops-infra.svc.cluster.local`,GitOps promotion 写 `git-mirror-write.devops-infra.svc.cluster.local`;confirmed trigger 在创建 PipelineRun 前会先按当前 source commit 在 G14 临时 detached worktree 中 render,再 server-side apply v02 Tekton RBAC、Pipeline 与 Argo Application,避免 CI/CD 脚本或 runtime-ready 逻辑已合并但集群仍执行旧 Pipeline 定义;该 render 不要求固定 `/root/hwlab-v02` 工作树 clean,也不得因 `.worktree/` 或其他并行未提交修改阻塞;同名 PipelineRun 存在时默认复用现有状态,不删除重建,失败 run 的重试策略必须显式设计,不能恢复默认 delete/create。 diff --git a/scripts/src/agentrun.ts b/scripts/src/agentrun.ts index 7f30d1ee..5a29d105 100644 --- a/scripts/src/agentrun.ts +++ b/scripts/src/agentrun.ts @@ -16,13 +16,18 @@ const argoNamespace = "argocd"; const argoApplication = "agentrun-g14-v01"; const gitopsBranch = "v0.1-gitops"; const gitMirrorNamespace = "devops-infra"; -const gitMirrorReadUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git"; -const gitMirrorWriteUrl = "http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git"; -const gitMirrorRepoPath = "/cache/pikasTech/agentrun.git"; +const gitMirrorReadBaseUrl = "http://git-mirror-http.devops-infra.svc.cluster.local"; +const gitMirrorWriteBaseUrl = "http://git-mirror-write.devops-infra.svc.cluster.local"; +const gitMirrorReadUrl = `${gitMirrorReadBaseUrl}/pikasTech/agentrun.git`; +const gitMirrorWriteUrl = `${gitMirrorWriteBaseUrl}/pikasTech/agentrun.git`; const gitMirrorSyncJobPrefix = "git-mirror-agentrun-sync-manual"; const gitMirrorFlushJobPrefix = "git-mirror-agentrun-flush-manual"; -const githubSshUrl = "ssh://git@ssh.github.com:443/pikasTech/agentrun.git"; const mirrorToolsImage = "127.0.0.1:5000/hwlab/hwlab-ci-node-tools:node22-alpine-bun-v1"; +const gitMirrorRepositories = [ + { key: "agentrun", repository: "pikasTech/agentrun", sourceBranch, gitopsBranch }, + { key: "unidesk", repository: "pikasTech/unidesk", sourceBranch: "master" }, + { key: "agent_skills", repository: "pikasTech/agent_skills", sourceBranch: "master" }, +] as const; export function agentRunHelp(): unknown { return { @@ -2614,7 +2619,7 @@ function gitMirrorJobManifest(action: "sync" | "flush", name: string): Record /tmp/agentrun-github-proxy-connect.mjs <<'NODE_PROXY'", + "#!/usr/bin/env node", + "import net from \"node:net\";", + "const [proxyHost, proxyPortRaw, targetHost, targetPortRaw] = process.argv.slice(2);", + "const proxyPort = Number.parseInt(proxyPortRaw || \"\", 10);", + "const targetPort = Number.parseInt(targetPortRaw || \"\", 10);", + "if (!proxyHost || !Number.isInteger(proxyPort) || !targetHost || !Number.isInteger(targetPort)) process.exit(64);", + "const socket = net.createConnection({ host: proxyHost, port: proxyPort });", + "let buffer = Buffer.alloc(0);", + "socket.setTimeout(10000, () => { socket.destroy(); process.exit(65); });", + "socket.on(\"connect\", () => socket.write(\"CONNECT \" + targetHost + \":\" + targetPort + \" HTTP/1.1\\r\\nHost: \" + targetHost + \":\" + targetPort + \"\\r\\nProxy-Connection: Keep-Alive\\r\\n\\r\\n\"));", + "socket.on(\"error\", () => process.exit(66));", + "function onData(chunk) {", + " buffer = Buffer.concat([buffer, chunk]);", + " const headerEnd = buffer.indexOf(\"\\r\\n\\r\\n\");", + " if (headerEnd === -1 && buffer.length < 8192) return;", + " const head = buffer.slice(0, headerEnd + 4).toString(\"latin1\");", + " const statusLine = head.split(\"\\r\\n\", 1)[0] || \"\";", + " const statusCode = Number.parseInt(statusLine.split(\" \")[1] || \"\", 10);", + " if (!statusLine.startsWith(\"HTTP/1.\") || !Number.isInteger(statusCode) || statusCode < 200 || statusCode > 299) { socket.destroy(); process.exit(67); }", + " socket.off(\"data\", onData);", + " socket.setTimeout(0);", + " const rest = buffer.slice(headerEnd + 4);", + " if (rest.length) process.stdout.write(rest);", + " process.stdin.pipe(socket);", + " socket.pipe(process.stdout);", + "}", + "socket.on(\"data\", onData);", + "socket.on(\"close\", () => process.exit(0));", + "NODE_PROXY", + "chmod 0700 /tmp/agentrun-github-proxy-connect.mjs", + "export GIT_SSH_COMMAND=\"ssh -i /root/.ssh/id_rsa -o IdentitiesOnly=yes -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o UserKnownHostsFile=/root/.ssh/known_hosts -o ConnectTimeout=10 -o ServerAliveInterval=5 -o ServerAliveCountMax=1 -o ProxyCommand='node /tmp/agentrun-github-proxy-connect.mjs 127.0.0.1 10808 %h %p'\"", + ]; +} + function gitMirrorSyncShellScript(): string { + const repoSpecs = gitMirrorRepositories.map((repo) => [repo.key, repo.repository, repo.sourceBranch, repo.gitopsBranch ?? ""].join("|")); return [ "set -eu", "started_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)", "mkdir -p /cache/pikasTech", - "export GIT_SSH_COMMAND='ssh -i /git-ssh/ssh-privatekey -o StrictHostKeyChecking=no -o UserKnownHostsFile=/tmp/agentrun-known-hosts'", - `repo=${shQuote(gitMirrorRepoPath)}`, - `remote=${shQuote(githubSshUrl)}`, - "if [ ! -d \"$repo\" ]; then git clone --mirror \"$remote\" \"$repo\"; else git --git-dir=\"$repo\" remote set-url origin \"$remote\"; fi", - "git --git-dir=\"$repo\" config uploadpack.allowReachableSHA1InWant true", - "git --git-dir=\"$repo\" config http.receivepack true", - "git --git-dir=\"$repo\" fetch origin +refs/heads/v0.1:refs/heads/v0.1 +refs/heads/v0.1:refs/mirror-stage/heads/v0.1", - "if git --git-dir=\"$repo\" fetch origin +refs/heads/v0.1-gitops:refs/mirror-stage/heads/v0.1-gitops; then", - " if ! git --git-dir=\"$repo\" rev-parse --verify 'refs/heads/v0.1-gitops^{commit}' >/dev/null 2>&1; then", - " git --git-dir=\"$repo\" update-ref refs/heads/v0.1-gitops \"$(git --git-dir=\"$repo\" rev-parse --verify 'refs/mirror-stage/heads/v0.1-gitops^{commit}')\"", + ...gitMirrorSshSetupShellLines(), + "sync_repo() {", + " key=\"$1\"", + " repository=\"$2\"", + " source_branch=\"$3\"", + " gitops_branch=\"$4\"", + " repo=\"/cache/${repository}.git\"", + " remote=\"ssh://git@ssh.github.com:443/${repository}.git\"", + " mkdir -p \"$(dirname \"$repo\")\"", + " if [ -d \"$repo/objects\" ] && [ -f \"$repo/HEAD\" ]; then", + " git --git-dir=\"$repo\" remote set-url origin \"$remote\" || git --git-dir=\"$repo\" remote add origin \"$remote\"", + " else", + " rm -rf \"$repo\"", + " git init --bare \"$repo\"", + " git --git-dir=\"$repo\" remote add origin \"$remote\"", " fi", - "fi", - "local_v01=$(git --git-dir=\"$repo\" rev-parse --verify 'refs/heads/v0.1^{commit}')", - "github_v01=$(git --git-dir=\"$repo\" rev-parse --verify 'refs/mirror-stage/heads/v0.1^{commit}')", - "local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify 'refs/heads/v0.1-gitops^{commit}' 2>/dev/null || true)", - "github_gitops=$(git --git-dir=\"$repo\" rev-parse --verify 'refs/mirror-stage/heads/v0.1-gitops^{commit}' 2>/dev/null || true)", - "pending=false; if [ -n \"$local_gitops\" ] && { [ -z \"$github_gitops\" ] || [ \"$local_gitops\" != \"$github_gitops\" ]; }; then pending=true; fi", - "json_ref() { if [ -n \"$1\" ]; then printf '\"%s\"' \"$1\"; else printf null; fi; }", - "cat > /cache/agentrun.last-sync.json </dev/null || true)", + " local_gitops=$(git --git-dir=\"$repo\" rev-parse --verify \"refs/heads/${gitops_branch}^{commit}\" 2>/dev/null || true)", + " if [ -z \"$local_gitops\" ] && [ -n \"$github_gitops\" ]; then", + " git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$github_gitops\"", + " elif [ -n \"$local_gitops\" ] && [ -n \"$github_gitops\" ] && [ \"$local_gitops\" != \"$github_gitops\" ] && git --git-dir=\"$repo\" merge-base --is-ancestor \"$local_gitops\" \"$github_gitops\"; then", + " git --git-dir=\"$repo\" update-ref \"refs/heads/${gitops_branch}\" \"$github_gitops\"", + " fi", + " fi", + " fi", + " git --git-dir=\"$repo\" update-server-info", + "}", + `for spec in ${repoSpecs.map(shQuote).join(" ")}; do`, + " IFS='|' read -r key repository source_branch gitops_branch </dev/null || true)", @@ -2707,23 +2795,47 @@ function gitMirrorCacheProbeScript(): string { return [ "printf 'lastSync='; cat /cache/agentrun.last-sync.json 2>/dev/null || true; printf '\\n'", "printf 'lastFlush='; cat /cache/agentrun.last-flush.json 2>/dev/null || true; printf '\\n'", - `repo=${shQuote(gitMirrorRepoPath)}`, - "rev() { git --git-dir=\"$repo\" rev-parse --verify \"$1^{commit}\" 2>/dev/null || true; }", - "local_v01=$(rev refs/heads/v0.1)", - "github_v01=$(rev refs/mirror-stage/heads/v0.1)", - "local_gitops=$(rev refs/heads/v0.1-gitops)", - "github_gitops=$(rev refs/mirror-stage/heads/v0.1-gitops)", - "exact=false", - "exact_commit=\"\"", - "if [ -n \"$local_v01\" ]; then", - " tmp=$(mktemp -d)", - " if git init -q \"$tmp\" && git -C \"$tmp\" remote add origin http://127.0.0.1:8080/pikasTech/agentrun.git && git -C \"$tmp\" fetch --depth=1 origin \"$local_v01\" >/tmp/agentrun-exact-fetch.log 2>&1; then", - " exact_commit=$(git -C \"$tmp\" rev-parse FETCH_HEAD 2>/dev/null || true)", - " if [ \"$exact_commit\" = \"$local_v01\" ]; then exact=true; fi", - " fi", - " rm -rf \"$tmp\"", - "fi", - "node -e 'const [localV01,githubV01,localGitops,githubGitops,exact,exactCommit]=process.argv.slice(1); const n=v=>v&&v.length>0?v:null; const pending=Boolean(n(localGitops)&&(!n(githubGitops)||localGitops!==githubGitops)); console.log(\"refs=\"+JSON.stringify({refs:{localV01:n(localV01),githubV01:n(githubV01),localGitops:n(localGitops),githubGitops:n(githubGitops)},pendingFlush:pending,exactFetch:{localV01:exact===\"true\",commit:n(exactCommit)}}));' \"$local_v01\" \"$github_v01\" \"$local_gitops\" \"$github_gitops\" \"$exact\" \"$exact_commit\"", + `REPOSITORIES_JSON=${shQuote(JSON.stringify(gitMirrorRepositories))}`, + "export REPOSITORIES_JSON", + "node <<'NODE'", + "const { execFileSync } = require('node:child_process');", + "const { mkdtempSync, rmSync } = require('node:fs');", + "const { tmpdir } = require('node:os');", + "const { join } = require('node:path');", + "const repositories = JSON.parse(process.env.REPOSITORIES_JSON || '[]');", + "function rev(repo, ref) {", + " try { execFileSync('test', ['-d', repo + '/objects']); } catch { return null; }", + " try { return execFileSync('git', ['--git-dir=' + repo, 'rev-parse', '--verify', ref + '^{commit}'], { encoding: 'utf8' }).trim(); }", + " catch { return null; }", + "}", + "function exactFetch(repository, commit) {", + " if (!commit) return { ok: false, commit: null };", + " const dir = mkdtempSync(join(tmpdir(), 'agentrun-mirror-probe-'));", + " try {", + " execFileSync('git', ['init', '-q', dir]);", + " execFileSync('git', ['-C', dir, 'remote', 'add', 'origin', `http://127.0.0.1:8080/${repository}.git`]);", + " execFileSync('git', ['-C', dir, 'fetch', '--depth=1', 'origin', commit], { stdio: 'ignore' });", + " const fetched = execFileSync('git', ['-C', dir, 'rev-parse', 'FETCH_HEAD'], { encoding: 'utf8' }).trim();", + " return { ok: fetched === commit, commit: fetched || null };", + " } catch {", + " return { ok: false, commit: null };", + " } finally {", + " rmSync(dir, { recursive: true, force: true });", + " }", + "}", + "const items = {};", + "for (const spec of repositories) {", + " const repoPath = `/cache/${spec.repository}.git`;", + " const localSource = rev(repoPath, `refs/heads/${spec.sourceBranch}`);", + " const githubSource = rev(repoPath, `refs/mirror-stage/heads/${spec.sourceBranch}`);", + " const localGitops = spec.gitopsBranch ? rev(repoPath, `refs/heads/${spec.gitopsBranch}`) : null;", + " const githubGitops = spec.gitopsBranch ? rev(repoPath, `refs/mirror-stage/heads/${spec.gitopsBranch}`) : null;", + " const exact = exactFetch(spec.repository, localSource);", + " items[spec.key] = { repository: spec.repository, sourceBranch: spec.sourceBranch, localSource, githubSource, gitopsBranch: spec.gitopsBranch || null, localGitops, githubGitops, sourceInSync: Boolean(localSource && githubSource && localSource === githubSource), gitopsInSync: spec.gitopsBranch ? Boolean(localGitops && githubGitops && localGitops === githubGitops) : null, pendingFlush: Boolean(localGitops && (!githubGitops || localGitops !== githubGitops)), exactFetch: exact };", + "}", + "const agentrun = items.agentrun || {};", + "console.log('refs=' + JSON.stringify({ refs: { localV01: agentrun.localSource || null, githubV01: agentrun.githubSource || null, localGitops: agentrun.localGitops || null, githubGitops: agentrun.githubGitops || null }, pendingFlush: Boolean(agentrun.pendingFlush), exactFetch: { localV01: Boolean(agentrun.exactFetch && agentrun.exactFetch.ok), commit: agentrun.exactFetch ? agentrun.exactFetch.commit : null }, repositories: items }));", + "NODE", ].join("\n"); } @@ -2743,9 +2855,10 @@ function gitMirrorStatusSummary(raw: string): Record { localGitops, githubGitops, pendingFlush, + repositories: record(refs).repositories ?? record(lastSync).repositories ?? null, sourceInSync: Boolean(localV01 && githubV01 && localV01 === githubV01), gitopsInSync: Boolean(localGitops && githubGitops && localGitops === githubGitops), - githubInSync: Boolean(localV01 && githubV01 && localV01 === githubV01 && (!localGitops || localGitops === githubGitops)), + githubInSync: Boolean(localV01 && githubV01 && localV01 === githubV01 && (!localGitops || localGitops === githubGitops)) && gitMirrorBundleRepositoriesReady(refs), flushNeeded: pendingFlush === true, flushCommand: pendingFlush === true ? "bun scripts/cli.ts agentrun git-mirror flush --confirm" : null, exactFetch: record(refs).exactFetch ?? null, @@ -2754,6 +2867,15 @@ function gitMirrorStatusSummary(raw: string): Record { }; } +function gitMirrorBundleRepositoriesReady(refs: unknown): boolean { + const repositories = record(record(refs).repositories); + const expected = gitMirrorRepositories.filter((repo) => repo.key !== "agentrun").map((repo) => repo.key); + return expected.every((key) => { + const item = record(repositories[key]); + return item.sourceInSync === true && record(item.exactFetch).ok === true; + }); +} + function gitMirrorSyncRequirement(sourceCommit: string, rawStatus: string): { required: boolean; reason: string; localV01: string | null } { const summary = gitMirrorStatusSummary(rawStatus); const localV01 = stringOrNull(summary.localV01);