diff --git a/.gitignore b/.gitignore index 9148ecd..5bb85b7 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,8 @@ logs/ node_modules/ package-lock.json +deploy/artifact-catalog.v01.json +deploy/gitops/g14/ dist/ build/ coverage/ diff --git a/AGENTS.md b/AGENTS.md index f8d2bd6..f7293dc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,11 +8,11 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Agent 执行基础设施。本仓 - P0: 代码标识符、API 路径、命令、配置键、日志字段、协议字段和必要英文专有名词可以保留英文,但解释性文字必须使用中文。 - P0: 外部英文资料只能作为引用或短摘录出现;落入本仓库的设计结论、验收标准和操作说明必须转写为中文。 -## Critical Source Workspace Rule +## Critical Source Worktree Rule -- P0: `v0.1` 长期 source workspace 是 `G14:/root/agentrun-v01`,固定使用 `v0.1` 分支,`origin` 固定为 `git@github.com:pikasTech/agentrun.git`。 +- P0: `v0.1` 长期 source worktree 是 `G14:/root/agentrun-v01`,固定使用 `v0.1` 分支,`origin` 固定为 `git@github.com:pikasTech/agentrun.git`。 - P0: 每次开发、部署、恢复中断或上下文压缩后,都必须先从 UniDesk 执行 `tran G14:/root/agentrun-v01 script -- 'pwd; git status --short --branch; git remote -v'`,确认路径、分支、remote 和 clean 状态。 -- P0: 固定 workspace 只用于预检、fetch、worktree 管理和最终同步。常规修改必须在 `/root/agentrun-v01/.worktree/{pr_branch}` 中完成,并从最新 `origin/v0.1` 创建。 +- P0: 固定 source worktree 只用于预检、fetch、worktree 管理和最终同步。常规修改必须在 `/root/agentrun-v01/.worktree/{pr_branch}` 中完成,并从最新 `origin/v0.1` 创建。 - P0: 不得把 UniDesk、HWLAB、D601 workspace、临时 clone、pod 内副本或 runner checkout 当作 AgentRun source truth。 ## Critical Versioned Lane Rule @@ -49,7 +49,7 @@ AgentRun 是面向 UniDesk 与 HWLAB 的共享 Agent 执行基础设施。本仓 - `docs/reference/spec-v01-documentation-governance.md`:v0.1 文档治理、唯一入口、spec 权威和过程材料承载规则。 - `docs/reference/spec-v01-services.md`:v0.1 服务总览、保留对象、deferred 对象和单服务规格索引。 -- `docs/reference/spec-v01-cicd.md`:v0.1 分支、workspace、namespace、GitOps、registry、CI/CD 和发布验收规格。 +- `docs/reference/spec-v01-cicd.md`:v0.1 分支、source worktree、namespace、GitOps、registry、CI/CD 和发布验收规格。 - `docs/reference/spec-v01-postgres.md`:v0.1 Postgres durable store、schema migration 和 SecretRef 规格。 - `docs/reference/spec-v01-secret-distribution.md`:v0.1 Code Agent provider credential 和运行时 Secret 分发规格。 - `docs/reference/spec-v01-validation.md`:v0.1 两层验证模型,自测试允许 mock,综合联调必须 100% 真实。 diff --git a/deploy/container/Containerfile b/deploy/container/Containerfile new file mode 100644 index 0000000..3f9a612 --- /dev/null +++ b/deploy/container/Containerfile @@ -0,0 +1,13 @@ +FROM oven/bun:1.2.15-alpine + +WORKDIR /app +ENV NODE_ENV=production +ENV PORT=8080 + +COPY package.json tsconfig.json ./ +COPY scripts ./scripts +COPY src ./src +COPY deploy/deploy.json ./deploy/deploy.json + +EXPOSE 8080 +CMD ["bun", "src/mgr/main.ts"] diff --git a/deploy/templates/argocd/application-v01.yaml b/deploy/templates/argocd/application-v01.yaml new file mode 100644 index 0000000..18e5cfe --- /dev/null +++ b/deploy/templates/argocd/application-v01.yaml @@ -0,0 +1,21 @@ +apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: agentrun-g14-v01 + namespace: argocd +spec: + project: agentrun-v01 + source: + repoURL: git@github.com:pikasTech/agentrun.git + targetRevision: v0.1-gitops + path: deploy/gitops/g14/runtime-v01 + destination: + server: https://kubernetes.default.svc + namespace: agentrun-v01 + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true diff --git a/deploy/templates/argocd/project.yaml b/deploy/templates/argocd/project.yaml new file mode 100644 index 0000000..202d4bd --- /dev/null +++ b/deploy/templates/argocd/project.yaml @@ -0,0 +1,18 @@ +apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: agentrun-v01 + namespace: argocd +spec: + description: AgentRun v0.1 GitOps lane + sourceRepos: + - git@github.com:pikasTech/agentrun.git + destinations: + - server: https://kubernetes.default.svc + namespace: agentrun-v01 + clusterResourceWhitelist: + - group: "" + kind: Namespace + namespaceResourceWhitelist: + - group: "*" + kind: "*" diff --git a/deploy/templates/tekton/pipeline.yaml b/deploy/templates/tekton/pipeline.yaml new file mode 100644 index 0000000..f18ffd9 --- /dev/null +++ b/deploy/templates/tekton/pipeline.yaml @@ -0,0 +1,264 @@ +apiVersion: tekton.dev/v1 +kind: Pipeline +metadata: + name: agentrun-v01-ci-image-publish + namespace: agentrun-ci + labels: + app.kubernetes.io/part-of: agentrun + agentrun.pikastech.local/lane: v0.1 +spec: + params: + - name: git-url + type: string + default: git@github.com:pikasTech/agentrun.git + - name: source-branch + type: string + default: v0.1 + - name: gitops-branch + type: string + default: v0.1-gitops + - name: revision + type: string + - name: registry-prefix + type: string + default: 127.0.0.1:5000/agentrun + - name: tools-image + type: string + default: oven/bun:1.2.15-alpine + workspaces: + - name: source + - name: git-ssh + tasks: + - name: prepare-source + workspaces: + - name: source + workspace: source + - name: git-ssh + workspace: git-ssh + taskSpec: + params: + - name: git-url + - name: source-branch + - name: revision + - name: tools-image + workspaces: + - name: source + - name: git-ssh + steps: + - name: clone-and-check + image: $(params.tools-image) + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + script: | + #!/bin/sh + set -eu + apk add --no-cache git openssh-client curl + mkdir -p /root/.ssh + cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa + chmod 600 /root/.ssh/id_rsa + ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null + rm -rf /workspace/source/repo + git clone --branch "$(params.source-branch)" "$(params.git-url)" /workspace/source/repo + cd /workspace/source/repo + git config --global --add safe.directory /workspace/source/repo + git checkout "$(params.revision)" + test "$(git rev-parse HEAD)" = "$(params.revision)" + bun install + bun run check + bun scripts/agentrun-gitops-render.ts --out /tmp/agentrun-gitops-render-check --source-commit "$(params.revision)" --check + bun run self-test + params: + - name: git-url + value: $(params.git-url) + - name: source-branch + value: $(params.source-branch) + - name: revision + value: $(params.revision) + - name: tools-image + value: $(params.tools-image) + - name: image-publish + runAfter: [prepare-source] + workspaces: + - name: source + workspace: source + taskSpec: + params: + - name: revision + - name: registry-prefix + results: + - name: image + - name: digest + - name: repository-digest + sidecars: + - name: buildkitd + image: moby/buildkit:rootless + args: + - --addr + - unix:///workspace/buildkit-run/buildkitd.sock + - --oci-worker-no-process-sandbox + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + - name: http_proxy + value: http://127.0.0.1:10808 + - name: https_proxy + value: http://127.0.0.1:10808 + - name: no_proxy + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + securityContext: + seccompProfile: + type: Unconfined + runAsUser: 1000 + runAsGroup: 1000 + volumeMounts: + - name: buildkit-run + mountPath: /workspace/buildkit-run + steps: + - name: prepare-buildctl + image: moby/buildkit:rootless + script: | + #!/bin/sh + set -eu + mkdir -p /workspace/buildkit-bin + cp /usr/bin/buildctl /workspace/buildkit-bin/buildctl + chmod +x /workspace/buildkit-bin/buildctl + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: build-and-push + image: oven/bun:1.2.15-alpine + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + script: | + #!/bin/sh + set -eu + apk add --no-cache curl + cd /workspace/source/repo + image="$(params.registry-prefix)/agentrun-mgr:$(params.revision)" + buildctl=/workspace/buildkit-bin/buildctl + for attempt in $(seq 1 60); do + if "$buildctl" --addr unix:///workspace/buildkit-run/buildkitd.sock debug workers >/dev/null 2>&1; then break; fi + sleep 1 + done + "$buildctl" --addr unix:///workspace/buildkit-run/buildkitd.sock build \ + --frontend dockerfile.v0 \ + --local context=. \ + --local dockerfile=deploy/container \ + --opt filename=Containerfile \ + --output type=image,name="$image",push=true + digest="$(curl -fsSI "http://127.0.0.1:5000/v2/agentrun/agentrun-mgr/manifests/$(params.revision)" | awk -F': ' 'tolower($1)=="docker-content-digest" {gsub(/\r/,"",$2); print $2; exit}')" + test -n "$digest" + printf '%s' "$image" > /tekton/results/image + printf '%s' "$digest" > /tekton/results/digest + printf '%s' "$(params.registry-prefix)/agentrun-mgr@$digest" > /tekton/results/repository-digest + volumeMounts: + - name: buildkit-bin + mountPath: /workspace/buildkit-bin + - name: buildkit-run + mountPath: /workspace/buildkit-run + volumes: + - name: buildkit-bin + emptyDir: {} + - name: buildkit-run + emptyDir: {} + params: + - name: revision + value: $(params.revision) + - name: registry-prefix + value: $(params.registry-prefix) + - name: gitops-promote + runAfter: [image-publish] + workspaces: + - name: source + workspace: source + - name: git-ssh + workspace: git-ssh + taskSpec: + params: + - name: git-url + - name: gitops-branch + - name: revision + - name: registry-prefix + - name: image + - name: digest + - name: repository-digest + workspaces: + - name: source + - name: git-ssh + steps: + - name: promote + image: oven/bun:1.2.15-alpine + env: + - name: HTTP_PROXY + value: http://127.0.0.1:10808 + - name: HTTPS_PROXY + value: http://127.0.0.1:10808 + - name: NO_PROXY + value: hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local + script: | + #!/bin/sh + set -eu + apk add --no-cache git openssh-client + mkdir -p /root/.ssh + cp /workspace/git-ssh/ssh-privatekey /root/.ssh/id_rsa + chmod 600 /root/.ssh/id_rsa + ssh-keyscan github.com >> /root/.ssh/known_hosts 2>/dev/null + cd /workspace/source/repo + cat > /workspace/source/artifact-catalog.v01.json </dev/null 2>&1 || true + } + cd /workspace/source/gitops + git config user.email agentrun-ci@g14.local + git config user.name agentrun-ci + mkdir -p deploy/gitops/g14 deploy + rm -rf deploy/gitops/g14/runtime-v01 deploy/gitops/g14/argocd + cp -a /workspace/source/rendered/runtime-v01 deploy/gitops/g14/runtime-v01 + cp -a /workspace/source/rendered/argocd deploy/gitops/g14/argocd + cp /workspace/source/rendered/artifact-catalog.v01.json deploy/artifact-catalog.v01.json + git add deploy + git commit -m "gitops: promote agentrun v0.1 $(params.revision)" || true + git push origin "$(params.gitops-branch)" + params: + - name: git-url + value: $(params.git-url) + - name: gitops-branch + value: $(params.gitops-branch) + - name: revision + value: $(params.revision) + - name: registry-prefix + value: $(params.registry-prefix) + - name: image + value: $(tasks.image-publish.results.image) + - name: digest + value: $(tasks.image-publish.results.digest) + - name: repository-digest + value: $(tasks.image-publish.results.repository-digest) diff --git a/deploy/templates/tekton/pipelinerun.sample.yaml b/deploy/templates/tekton/pipelinerun.sample.yaml new file mode 100644 index 0000000..6c7eef5 --- /dev/null +++ b/deploy/templates/tekton/pipelinerun.sample.yaml @@ -0,0 +1,29 @@ +apiVersion: tekton.dev/v1 +kind: PipelineRun +metadata: + generateName: agentrun-v01-ci- + namespace: agentrun-ci +spec: + pipelineRef: + name: agentrun-v01-ci-image-publish + taskRunTemplate: + serviceAccountName: agentrun-v01-tekton-runner + podTemplate: + hostNetwork: true + dnsPolicy: ClusterFirstWithHostNet + securityContext: + fsGroup: 1000 + params: + - name: revision + value: REPLACE_WITH_FULL_SOURCE_COMMIT + workspaces: + - name: source + volumeClaimTemplate: + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi + - name: git-ssh + secret: + secretName: agentrun-git-ssh diff --git a/deploy/templates/tekton/rbac.yaml b/deploy/templates/tekton/rbac.yaml new file mode 100644 index 0000000..37fd613 --- /dev/null +++ b/deploy/templates/tekton/rbac.yaml @@ -0,0 +1,36 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: agentrun-ci +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: agentrun-v01-tekton-runner + namespace: agentrun-ci +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: agentrun-v01-tekton-runner + namespace: agentrun-ci +rules: + - apiGroups: ["tekton.dev"] + resources: ["pipelineruns", "taskruns"] + verbs: ["get", "list", "watch", "create", "patch", "update"] + - apiGroups: [""] + resources: ["pods", "pods/log", "secrets", "configmaps", "persistentvolumeclaims"] + verbs: ["get", "list", "watch", "create", "patch", "update", "delete"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: agentrun-v01-tekton-runner + namespace: agentrun-ci +subjects: + - kind: ServiceAccount + name: agentrun-v01-tekton-runner +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: agentrun-v01-tekton-runner diff --git a/docs/reference/architecture.md b/docs/reference/architecture.md index aca9cfd..473ef29 100644 --- a/docs/reference/architecture.md +++ b/docs/reference/architecture.md @@ -212,7 +212,7 @@ Postgres DSN、provider credential 和未来 tenant credential 的分发边界 ## 部署方向 -AgentRun 从 `v0.1` 开始按版本 lane 滚动,废弃 `dev/prod` 管理口径。`v0.1` 的固定 source workspace 是 `G14:/root/agentrun-v01`,固定 source branch 是 `v0.1`,固定运行目标是 G14 原生 k3s namespace `agentrun-v01`。后续 `v0.2`、`v0.3` 必须拥有自己的 branch、workspace、namespace、GitOps branch、runtime path 和发布验收。 +AgentRun 从 `v0.1` 开始按版本 lane 滚动,废弃 `dev/prod` 管理口径。`v0.1` 的固定 source worktree 是 `G14:/root/agentrun-v01`,固定 source branch 是 `v0.1`,固定运行目标是 G14 原生 k3s namespace `agentrun-v01`。后续 `v0.2`、`v0.3` 必须拥有自己的 branch、source worktree、namespace、GitOps branch、runtime path 和发布验收。 Control-plane service 应是长驻服务;runner 应是短生命周期 Job 或受控 host-native process。Backend adapter 可以作为 pod 或 host-native service 运行,但必须通过 AgentRun 注册 capability 和 health,不能通过临时地址被 ad hoc 调用。 diff --git a/docs/reference/spec-v01-cicd.md b/docs/reference/spec-v01-cicd.md index 5b6632d..38a6ac8 100644 --- a/docs/reference/spec-v01-cicd.md +++ b/docs/reference/spec-v01-cicd.md @@ -5,11 +5,11 @@ ## 设计目标 - `v0.1` 是独立 lane,不复用 `agentrun_dev` 或 `agentrun_prod` 作为当前运行面。 -- 每条版本 lane 拥有独立 source branch、source workspace、GitOps branch、runtime namespace、artifact catalog、runtime path、CI pipeline 和发布验收。 +- 每条版本 lane 拥有独立 source branch、source worktree、GitOps branch、runtime namespace、artifact catalog、runtime path、CI pipeline 和发布验收。 - `v0.1` 只能部署到 `agentrun-v01` namespace;`v0.2`、`v0.3` 后续使用自己的 namespace 和 GitOps 路径。 - CI/CD 必须使用 G14 原生 k3s、纯 Tekton Pipeline/Task/PipelineRun 和 Argo CD;不得引入自定义 runner、CI.json runner、长期自研 poller/reconciler、D601 legacy、临时 clone、手工 Pod patch 或本地镜像作为发布真相。 - `v0.1` 的 CD 唯一手写真相源是 source branch 内的 `deploy/deploy.json`;Tekton 生成的 artifact catalog 和 Argo desired state 必须与 source branch 分离,只写入 `v0.1-gitops`。 -- 发布证据以 live runtime、Argo desired state、GitOps branch、Tekton 证据和干净 source workspace 顺序判断。 +- 发布证据以 live runtime、Argo desired state、GitOps branch、Tekton 证据和干净 source worktree 顺序判断。 ## 固定命名 @@ -17,7 +17,7 @@ | --- | --- | | Source repo | `git@github.com:pikasTech/agentrun.git` | | Source branch | `v0.1` | -| Source workspace | `G14:/root/agentrun-v01` | +| Source worktree | `G14:/root/agentrun-v01` | | Worktree root | `G14:/root/agentrun-v01/.worktree/{task}` | | Runtime namespace | `agentrun-v01` | | GitOps branch | `v0.1-gitops` | @@ -34,7 +34,9 @@ ## Bun + TypeScript CI 边界 -`v0.1` 自研代码的 CI/CD 工具链以 Bun + TypeScript 为准。Tekton task 必须在受控容器内安装或使用固定 Bun 运行时,执行依赖安装、类型检查、自测试和镜像构建;不得把 master server、G14 固定 source workspace 或手工 host shell 作为构建机。 +`v0.1` 自研代码的 CI/CD 工具链以 Bun + TypeScript 为准。Tekton task 必须在受控容器内安装或使用固定 Bun 运行时,执行依赖安装、类型检查、自测试和镜像构建;不得把 master server、G14 固定 source worktree 或手工 host shell 作为构建机。 + +G14 依赖拉取和镜像发布沿用 HWLAB 成熟 lane 的运行面假设:`PipelineRun.spec.taskRunTemplate.podTemplate.hostNetwork=true` 且 `dnsPolicy=ClusterFirstWithHostNet`。因此 Tekton task 内的 `127.0.0.1:10808` 指向 G14 host bootstrap proxy,`127.0.0.1:5000` 指向 G14 host-managed registry;`NO_PROXY` 必须保留 `hyueapi.com`、`.hyueapi.com`、cluster service CIDR、`.svc` 和 `.cluster.local`。如果未来改成 cluster Service proxy 或 registry DNS,必须同步更新 Tekton template、render 脚本和本文规格,不得只改某个 PipelineRun 实例。 CI 的最小检查应覆盖: @@ -53,9 +55,9 @@ CI 的最小检查应覆盖: 2. Argo desired state:`argocd/agentrun-g14-v01` 的 revision、sync、health、source branch 和 runtime path。 3. GitOps branch:`v0.1-gitops` 中的 `deploy/artifact-catalog.v01.json` 与 `deploy/gitops/g14/runtime-v01/**`。 4. Tekton 执行证据:PipelineRun、TaskRun result、image digest 和 promotion 终态。 -5. 干净 source workspace:`G14:/root/agentrun-v01`、`origin/v0.1`、render 脚本、deploy intent 和 `--no-write` 输出。 +5. 干净 source worktree:`G14:/root/agentrun-v01`、`origin/v0.1`、render 脚本、deploy intent 和 `--no-write` 输出。 -旧 `master` 记忆、`/root/agentrun` 固定工作区、`agentrun_dev`、`agentrun_prod`、D601 legacy 路径、临时 worktree 或本地容器只能作为线索,不能作为 `v0.1` 发布通过证据。 +旧 `master` 记忆、`/root/agentrun` 历史固定目录、`agentrun_dev`、`agentrun_prod`、D601 legacy 路径、临时 worktree 或本地容器只能作为线索,不能作为 `v0.1` 发布通过证据。 ## Source 与 GitOps 分层 @@ -140,7 +142,7 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st ## 验收标准 -- `G14:/root/agentrun-v01` 为 `v0.1...origin/v0.1` 且 clean。 +- `G14:/root/agentrun-v01` source worktree 为 `v0.1...origin/v0.1` 且 clean。 - `AGENTS.md` 和 `docs/reference/` 不得把 `agentrun_dev` 或 `agentrun_prod` 写成 `v0.1` 当前 namespace、Argo destination、Pipeline target 或验收目标;只允许在废弃说明和历史背景中提及。 - `agentrun-v01` namespace 存在,且 `agentrun_dev`/`agentrun_prod` 不参与 `v0.1` 发布判定。 - `v0.1-gitops` branch 和 `deploy/gitops/g14/runtime-v01` 成为 Argo desired state 来源。 @@ -154,7 +156,7 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st | 规格项 | 状态 | 说明 | | --- | --- | --- | | `v0.1` source branch | 已建立 | `origin/v0.1` 存在。 | -| `G14:/root/agentrun-v01` workspace | 已建立 | 固定工作区使用 `v0.1` 分支。 | +| `G14:/root/agentrun-v01` source worktree | 已建立 | 固定 source worktree 使用 `v0.1` 分支。 | | `agentrun-v01` namespace | 未实现 | 需要后续初始化。 | | `v0.1-gitops` branch | 未实现 | 需要后续初始化。 | | 纯 Tekton/Argo lane | 未实现 | 需要后续按本文补齐;不得引入自定义 runner。 | diff --git a/docs/reference/spec-v01-documentation-governance.md b/docs/reference/spec-v01-documentation-governance.md index b139b87..7745fde 100644 --- a/docs/reference/spec-v01-documentation-governance.md +++ b/docs/reference/spec-v01-documentation-governance.md @@ -45,14 +45,14 @@ 1. 发现 `README.md`、`docs/*.md`、`docs/*.json` 或计划文档时,先判断是否有长期价值。 2. 有长期价值的,只把稳定结论吸收到对应 `docs/reference/spec-v01-*.md`;不要整篇搬入 reference。 3. 属于计划、里程碑、阶段迁移、报告、一次性排障或历史验收的,全文迁入 GitHub issue 或 PR 评论,再删除源文件。 -4. 与 `v0.1` 规格冲突的旧 `dev/prod`、`/root/agentrun` 默认工作区、`agentrun_dev`、`agentrun_prod` 说法必须删除或改为历史背景。 +4. 与 `v0.1` 规格冲突的旧 `dev/prod`、`/root/agentrun` 默认 source 目录、`agentrun_dev`、`agentrun_prod` 说法必须删除或改为历史背景。 5. 发现独立 `TEST.md` 或测试计划文档时,先把仍有效的测试场景蒸馏到对应 `spec-v01-*.md` 的“测试规格”小节,再删除源文件。 6. 未实现规格必须在对应 spec 的“规格的实现情况”中明确写成 `未实现` 或 `未完全实现`,不要用散落 TODO 代替。 ## v0.1 当前权威入口 - Source branch:`v0.1`。 -- Source workspace:`G14:/root/agentrun-v01`。 +- Source worktree:`G14:/root/agentrun-v01`。 - Worktree 根:`G14:/root/agentrun-v01/.worktree/{task}`。 - Runtime namespace:`agentrun-v01`。 - 发布和验收规格:[spec-v01-cicd.md](spec-v01-cicd.md)。 @@ -71,7 +71,7 @@ - `AGENTS.md` 索引本文和其他 `spec-v01-*` 规格。 - `docs/reference/` 中存在 `spec-v01-documentation-governance.md`、`spec-v01-services.md`、`spec-v01-cicd.md`、`spec-v01-postgres.md`、`spec-v01-secret-distribution.md`、`spec-v01-validation.md`、`spec-v01-agentrun-mgr.md`、`spec-v01-agentrun-runner.md`、`spec-v01-backend-adapter.md`、`spec-v01-backend-codex.md`、`spec-v01-cli.md` 和 `spec-v01-scheduler.md`。 -- `AGENTS.md` 和 `docs/reference/` 不得把旧 `agentrun_dev`、`agentrun_prod`、`G14:/root/agentrun` 或 `/root/agentrun` 写成当前工作区、namespace、发布目标或验收目标;只允许在废弃说明和历史背景中提及。 +- `AGENTS.md` 和 `docs/reference/` 不得把旧 `agentrun_dev`、`agentrun_prod`、`G14:/root/agentrun` 或 `/root/agentrun` 写成当前 source worktree、namespace、发布目标或验收目标;只允许在废弃说明和历史背景中提及。 - `docs/` 根目录不新增临时 Markdown 报告或 JSON dump。 - 仓库根目录不存在 `TEST.md`;测试场景维护在对应 `spec-v01-*.md` 的“测试规格”小节。 - 后续实现任务先更新或引用对应 `spec-v01-*`,再修改代码和测试。 diff --git a/scripts/agentrun-gitops-render.ts b/scripts/agentrun-gitops-render.ts new file mode 100644 index 0000000..1e8916a --- /dev/null +++ b/scripts/agentrun-gitops-render.ts @@ -0,0 +1,4 @@ +#!/usr/bin/env bun +import { runGitopsRenderCli } from "./src/gitops-render.js"; + +await runGitopsRenderCli(process.argv.slice(2)); diff --git a/scripts/src/gitops-render.ts b/scripts/src/gitops-render.ts new file mode 100644 index 0000000..00ecf4d --- /dev/null +++ b/scripts/src/gitops-render.ts @@ -0,0 +1,355 @@ +import { mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import path from "node:path"; +import type { JsonRecord } from "../../src/common/types.js"; +import { AgentRunError, errorToJson } from "../../src/common/errors.js"; + +interface RenderOptions { + outDir: string; + deployFile: string; + catalogFile?: string; + sourceCommit: string; + registryPrefix: string; + requireCatalog: boolean; + check: boolean; +} + +interface ArtifactCatalog { + lane: string; + sourceBranch: string; + gitopsBranch: string; + sourceCommitId: string; + services: Array<{ serviceId: string; image: string; digest: string; repositoryDigest: string; imageTag: string }>; +} + +export async function runGitopsRenderCli(argv: string[]): Promise { + try { + const options = parseArgs(argv); + const summary = await renderGitops(options); + console.log(JSON.stringify({ ok: true, data: summary })); + } catch (error) { + console.log(JSON.stringify({ ok: false, failureKind: error instanceof AgentRunError ? error.failureKind : "infra-failed", message: error instanceof Error ? error.message : String(error), error: errorToJson(error) })); + process.exitCode = 1; + } +} + +export async function renderGitops(options: RenderOptions): Promise { + const deploy = JSON.parse(await readFile(options.deployFile, "utf8")) as JsonRecord; + const runtimeNamespace = stringField(deploy, "runtimeNamespace", "agentrun-v01"); + const gitopsBranch = stringField(deploy, "gitopsBranch", "v0.1-gitops"); + const runtimePath = stringField(deploy, "runtimePath", "deploy/gitops/g14/runtime-v01"); + const catalog = await loadCatalog(options, gitopsBranch); + const image = imageForService(catalog, "agentrun-mgr", options); + + if (options.check) await rm(options.outDir, { recursive: true, force: true }); + await mkdir(path.join(options.outDir, "argocd"), { recursive: true }); + await mkdir(path.join(options.outDir, "runtime-v01"), { recursive: true }); + await writeFile(path.join(options.outDir, "source.json"), `${JSON.stringify({ lane: "v0.1", sourceCommit: options.sourceCommit, generatedBy: "scripts/agentrun-gitops-render.ts" }, null, 2)}\n`); + await writeFile(path.join(options.outDir, "artifact-catalog.v01.json"), `${JSON.stringify(catalog, null, 2)}\n`); + await writeFile(path.join(options.outDir, "argocd", "project.yaml"), projectYaml(runtimeNamespace)); + await writeFile(path.join(options.outDir, "argocd", "application-v01.yaml"), applicationYaml(gitopsBranch, runtimePath, runtimeNamespace)); + await writeFile(path.join(options.outDir, "runtime-v01", "kustomization.yaml"), kustomizationYaml()); + await writeFile(path.join(options.outDir, "runtime-v01", "namespace.yaml"), namespaceYaml(runtimeNamespace)); + await writeFile(path.join(options.outDir, "runtime-v01", "postgres.yaml"), postgresYaml(runtimeNamespace)); + await writeFile(path.join(options.outDir, "runtime-v01", "mgr.yaml"), managerYaml(runtimeNamespace, image)); + await writeFile(path.join(options.outDir, "runtime-v01", "runner-rbac.yaml"), runnerRbacYaml(runtimeNamespace)); + return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: image.repositoryDigest, sourceCommit: options.sourceCommit }; +} + +async function loadCatalog(options: RenderOptions, gitopsBranch: string): Promise { + if (options.catalogFile) return JSON.parse(await readFile(options.catalogFile, "utf8")) as ArtifactCatalog; + if (options.requireCatalog) throw new AgentRunError("schema-invalid", "artifact catalog is required for promotion render", { httpStatus: 2 }); + const digest = `sha256:${"0".repeat(64)}`; + const image = `${options.registryPrefix}/agentrun-mgr:${options.sourceCommit}`; + return { + lane: "v0.1", + sourceBranch: "v0.1", + gitopsBranch, + sourceCommitId: options.sourceCommit, + services: [{ serviceId: "agentrun-mgr", image, digest, repositoryDigest: `${options.registryPrefix}/agentrun-mgr@${digest}`, imageTag: options.sourceCommit }], + }; +} + +function imageForService(catalog: ArtifactCatalog, serviceId: string, options: RenderOptions): { repositoryDigest: string } { + const service = catalog.services.find((item) => item.serviceId === serviceId); + if (!service) throw new AgentRunError("schema-invalid", `catalog missing service ${serviceId}`, { httpStatus: 2 }); + if (!/^sha256:[a-f0-9]{64}$/u.test(service.digest)) throw new AgentRunError("schema-invalid", `catalog service ${serviceId} has invalid digest`, { httpStatus: 2 }); + if (options.requireCatalog && service.digest === `sha256:${"0".repeat(64)}`) throw new AgentRunError("schema-invalid", "placeholder digest is not allowed in promotion render", { httpStatus: 2 }); + return { repositoryDigest: service.repositoryDigest || `${service.image.slice(0, service.image.lastIndexOf(":"))}@${service.digest}` }; +} + +function projectYaml(namespace: string): string { + return `apiVersion: argoproj.io/v1alpha1 +kind: AppProject +metadata: + name: agentrun-v01 + namespace: argocd +spec: + description: AgentRun v0.1 GitOps lane + sourceRepos: + - git@github.com:pikasTech/agentrun.git + destinations: + - server: https://kubernetes.default.svc + namespace: ${namespace} + clusterResourceWhitelist: + - group: "" + kind: Namespace + namespaceResourceWhitelist: + - group: "*" + kind: "*" +`; +} + +function applicationYaml(gitopsBranch: string, runtimePath: string, namespace: string): string { + return `apiVersion: argoproj.io/v1alpha1 +kind: Application +metadata: + name: agentrun-g14-v01 + namespace: argocd +spec: + project: agentrun-v01 + source: + repoURL: git@github.com:pikasTech/agentrun.git + targetRevision: ${gitopsBranch} + path: ${runtimePath} + destination: + server: https://kubernetes.default.svc + namespace: ${namespace} + syncPolicy: + automated: + prune: false + selfHeal: true + syncOptions: + - CreateNamespace=true + - ApplyOutOfSyncOnly=true +`; +} + +function kustomizationYaml(): string { + return `apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization +resources: + - namespace.yaml + - postgres.yaml + - mgr.yaml + - runner-rbac.yaml +`; +} + +function namespaceYaml(namespace: string): string { + return `apiVersion: v1 +kind: Namespace +metadata: + name: ${namespace} + labels: + app.kubernetes.io/part-of: agentrun + agentrun.pikastech.local/lane: v0.1 +`; +} + +function postgresYaml(namespace: string): string { + return `apiVersion: v1 +kind: Service +metadata: + name: agentrun-v01-postgres + namespace: ${namespace} +spec: + selector: + app.kubernetes.io/name: agentrun-v01-postgres + ports: + - name: postgres + port: 5432 + targetPort: postgres +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: agentrun-v01-postgres + namespace: ${namespace} +spec: + serviceName: agentrun-v01-postgres + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: agentrun-v01-postgres + template: + metadata: + labels: + app.kubernetes.io/name: agentrun-v01-postgres + spec: + containers: + - name: postgres + image: postgres:16-alpine + ports: + - name: postgres + containerPort: 5432 + env: + - name: POSTGRES_DB + value: agentrun_v01 + - name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: agentrun-v01-postgres + key: username + - name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: agentrun-v01-postgres + key: password + volumeMounts: + - name: data + mountPath: /var/lib/postgresql/data + volumeClaimTemplates: + - metadata: + name: data + spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: 5Gi +`; +} + +function managerYaml(namespace: string, image: { repositoryDigest: string }): string { + return `apiVersion: v1 +kind: ServiceAccount +metadata: + name: agentrun-v01-mgr + namespace: ${namespace} +--- +apiVersion: v1 +kind: Service +metadata: + name: agentrun-mgr + namespace: ${namespace} +spec: + selector: + app.kubernetes.io/name: agentrun-mgr + ports: + - name: http + port: 8080 + targetPort: http +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: agentrun-mgr + namespace: ${namespace} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: agentrun-mgr + template: + metadata: + labels: + app.kubernetes.io/name: agentrun-mgr + annotations: + agentrun.pikastech.local/lane: v0.1 + spec: + serviceAccountName: agentrun-v01-mgr + containers: + - name: mgr + image: ${image.repositoryDigest} + imagePullPolicy: IfNotPresent + ports: + - name: http + containerPort: 8080 + env: + - name: AGENTRUN_LANE + value: v0.1 + - name: DATABASE_URL + valueFrom: + secretKeyRef: + name: agentrun-v01-mgr-db + key: DATABASE_URL + readinessProbe: + httpGet: + path: /health/readiness + port: http + livenessProbe: + httpGet: + path: /health/live + port: http + resources: + requests: + cpu: 100m + memory: 256Mi + limits: + cpu: 800m + memory: 1Gi +`; +} + +function runnerRbacYaml(namespace: string): string { + return `apiVersion: v1 +kind: ServiceAccount +metadata: + name: agentrun-v01-runner + namespace: ${namespace} +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: Role +metadata: + name: agentrun-v01-runner-secret-reader + namespace: ${namespace} +rules: + - apiGroups: [""] + resources: ["secrets"] + resourceNames: ["agentrun-v01-provider-codex"] + verbs: ["get"] +--- +apiVersion: rbac.authorization.k8s.io/v1 +kind: RoleBinding +metadata: + name: agentrun-v01-runner-secret-reader + namespace: ${namespace} +subjects: + - kind: ServiceAccount + name: agentrun-v01-runner +roleRef: + apiGroup: rbac.authorization.k8s.io + kind: Role + name: agentrun-v01-runner-secret-reader +`; +} + +function parseArgs(argv: string[]): RenderOptions { + const flags = new Map(); + for (let index = 0; index < argv.length; index += 1) { + const item = argv[index] ?? ""; + if (!item.startsWith("--")) continue; + const key = item.slice(2); + const next = argv[index + 1]; + if (next === undefined || next.startsWith("--")) flags.set(key, true); + else { + flags.set(key, next); + index += 1; + } + } + const options: RenderOptions = { + outDir: stringFlag(flags, "out", "deploy/gitops/g14"), + deployFile: stringFlag(flags, "deploy-file", "deploy/deploy.json"), + sourceCommit: stringFlag(flags, "source-commit", "source-check"), + registryPrefix: stringFlag(flags, "registry-prefix", "127.0.0.1:5000/agentrun"), + requireCatalog: flags.get("require-catalog") === true, + check: flags.get("check") === true, + }; + const catalogFile = optionalStringFlag(flags, "catalog"); + if (catalogFile) options.catalogFile = catalogFile; + return options; +} + +function stringFlag(flags: Map, key: string, fallback: string): string { + const value = flags.get(key); + return typeof value === "string" && value.length > 0 ? value : fallback; +} + +function optionalStringFlag(flags: Map, key: string): string | undefined { + const value = flags.get(key); + return typeof value === "string" && value.length > 0 ? value : undefined; +} + +function stringField(record: JsonRecord, key: string, fallback: string): string { + const value = record[key]; + return typeof value === "string" && value.length > 0 ? value : fallback; +}