feat: support env reuse and git mirror in v0.1 cicd

This commit is contained in:
Codex
2026-06-02 00:52:19 +08:00
parent f34312c424
commit df38f605fa
11 changed files with 365 additions and 79 deletions
+1 -1
View File
@@ -52,7 +52,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 分支、source worktree、namespace、GitOps、registry、CI/CD 和发布验收规格。
- `docs/reference/spec-v01-cicd.md`v0.1 分支、source worktree、git mirror、env reuse、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-runtime-assembly.md`v0.1 runner/backend 启动前的装配 SPEC,覆盖 BackendImageRef、ProfileRef、SessionRef、Git-only ResourceBundleRef 和 tool credential SecretRef scope。
+9 -8
View File
@@ -1,23 +1,24 @@
FROM oven/bun:1.2.15-alpine
WORKDIR /app
WORKDIR /opt/agentrun
ARG HTTP_PROXY
ARG HTTPS_PROXY
ARG NO_PROXY
ENV NODE_ENV=production
ENV PORT=8080
ENV AGENTRUN_CODEX_COMMAND=/app/node_modules/.bin/codex
ENV AGENTRUN_CODEX_COMMAND=/opt/agentrun/node_modules/.bin/codex
ENV AGENTRUN_APP_ROOT=/workspace/agentrun
ENV AGENTRUN_BOOT_REPO_URL=http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
RUN HTTP_PROXY="$HTTP_PROXY" HTTPS_PROXY="$HTTPS_PROXY" NO_PROXY="$NO_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTPS_PROXY" no_proxy="$NO_PROXY" \
apk add --no-cache ca-certificates curl git github-cli kubectl nodejs openssh-client
COPY package.json tsconfig.json ./
COPY package.json bun.lock tsconfig.json ./
RUN HTTP_PROXY="$HTTP_PROXY" HTTPS_PROXY="$HTTPS_PROXY" NO_PROXY="$NO_PROXY" http_proxy="$HTTP_PROXY" https_proxy="$HTTPS_PROXY" no_proxy="$NO_PROXY" \
bun install --production
RUN /app/node_modules/.bin/codex --version && /app/node_modules/.bin/codex app-server --help >/dev/null
COPY scripts ./scripts
COPY src ./src
COPY deploy/deploy.json ./deploy/deploy.json
RUN /opt/agentrun/node_modules/.bin/codex --version && /opt/agentrun/node_modules/.bin/codex app-server --help >/dev/null
COPY deploy/runtime/boot ./deploy/runtime/boot
RUN chmod +x /opt/agentrun/deploy/runtime/boot/*.sh
EXPOSE 8080
CMD ["bun", "src/mgr/main.ts"]
CMD ["/opt/agentrun/deploy/runtime/boot/agentrun-mgr.sh"]
+44
View File
@@ -0,0 +1,44 @@
#!/bin/sh
set -eu
entrypoint="${1:-src/mgr/main.ts}"
repo_url="${AGENTRUN_BOOT_REPO_URL:-http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git}"
commit="${AGENTRUN_BOOT_COMMIT:-${AGENTRUN_SOURCE_COMMIT:-}}"
app_root="${AGENTRUN_APP_ROOT:-/workspace/agentrun}"
if [ -z "$commit" ] || ! printf '%s' "$commit" | grep -Eq '^[0-9a-f]{40}$'; then
printf '{"ok":false,"event":"agentrun-boot","failureKind":"source-commit-invalid","message":"AGENTRUN_BOOT_COMMIT must be a full git SHA"}\n' >&2
exit 64
fi
mkdir -p "$(dirname "$app_root")"
rm -rf "$app_root"
mkdir -p "$app_root"
cd "$app_root"
git init -q
git remote add origin "$repo_url"
fetch_log=/tmp/agentrun-boot-fetch.log
if ! git fetch --depth=1 origin "$commit" >"$fetch_log" 2>&1; then
message=$(tail -n 20 "$fetch_log" | tr '\n' ' ' | sed 's/[[:space:]]\+/ /g')
failure_kind=git-mirror-fetch-failed
if printf '%s' "$message" | grep -Eiq 'not our ref|unadvertised|Server does not allow request'; then
failure_kind=git-mirror-exact-commit-unavailable
elif printf '%s' "$message" | grep -Eiq 'Could not resolve host|Connection timed out|Failed to connect'; then
failure_kind=git-mirror-network-failed
elif printf '%s' "$message" | grep -Eiq 'Authentication failed|Permission denied|repository.*not found'; then
failure_kind=git-mirror-auth-failed
fi
printf '{"ok":false,"event":"agentrun-boot","failureKind":"%s","repoUrl":"%s","commit":"%s","message":%s}\n' "$failure_kind" "$repo_url" "$commit" "$(node -e 'console.log(JSON.stringify(process.argv[1] || ""))' "$message")" >&2
exit 65
fi
git checkout -q --detach "$commit"
actual=$(git rev-parse HEAD)
if [ "$actual" != "$commit" ]; then
printf '{"ok":false,"event":"agentrun-boot","failureKind":"source-commit-mismatch","expected":"%s","actual":"%s"}\n' "$commit" "$actual" >&2
exit 66
fi
ln -sfn /opt/agentrun/node_modules "$app_root/node_modules"
printf '{"ok":true,"event":"agentrun-boot","repoUrl":"%s","commit":"%s","entrypoint":"%s","nodeModules":"/opt/agentrun/node_modules"}\n' "$repo_url" "$commit" "$entrypoint"
exec bun "$entrypoint"
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
set -eu
exec /opt/agentrun/deploy/runtime/boot/agentrun-boot.sh src/mgr/main.ts
+3
View File
@@ -0,0 +1,3 @@
#!/bin/sh
set -eu
exec /opt/agentrun/deploy/runtime/boot/agentrun-boot.sh src/runner/main.ts
+211 -47
View File
@@ -11,6 +11,12 @@ spec:
- name: git-url
type: string
default: git@github.com:pikasTech/agentrun.git
- name: git-read-url
type: string
default: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
- name: git-write-url
type: string
default: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git
- name: source-branch
type: string
default: v0.1
@@ -33,17 +39,13 @@ spec:
workspaces:
- name: source
workspace: source
- name: git-ssh
workspace: git-ssh
taskSpec:
params:
- name: git-url
- name: source-branch
- name: git-read-url
- name: revision
- name: tools-image
workspaces:
- name: source
- name: git-ssh
steps:
- name: clone-and-check
image: $(params.tools-image)
@@ -60,37 +62,132 @@ spec:
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: GIT_TERMINAL_PROMPT
value: "0"
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
apk add --no-cache git curl
rm -rf /workspace/source/repo
git clone --branch "$(params.source-branch)" "$(params.git-url)" /workspace/source/repo
mkdir -p /workspace/source/repo
git init /workspace/source/repo
cd /workspace/source/repo
git config --global --add safe.directory /workspace/source/repo
git checkout "$(params.revision)"
git remote add origin "$(params.git-read-url)"
git fetch --depth=1 origin "$(params.revision)"
git checkout --detach FETCH_HEAD
test "$(git rev-parse HEAD)" = "$(params.revision)"
bun install
bun install --frozen-lockfile
bun run check
bun scripts/agentrun-gitops-render.ts --out /tmp/agentrun-gitops-render-check --source-commit "$(params.revision)" --check
AGENTRUN_SELFTEST_CODEX_COMMAND="$(command -v bun)" \
AGENTRUN_SELFTEST_CODEX_ARGS="[\"$PWD/src/selftest/fake-codex-app-server.ts\"]" \
bun run self-test
params:
- name: git-url
value: $(params.git-url)
- name: source-branch
value: $(params.source-branch)
- name: git-read-url
value: $(params.git-read-url)
- name: revision
value: $(params.revision)
- name: tools-image
value: $(params.tools-image)
- name: image-publish
- name: plan-artifacts
runAfter: [prepare-source]
workspaces:
- name: source
workspace: source
taskSpec:
params:
- name: git-read-url
- name: gitops-branch
- name: revision
- name: registry-prefix
results:
- name: env-identity
- name: build-count
- name: reuse-count
- name: summary
workspaces:
- name: source
steps:
- name: plan
image: oven/bun:1.2.15-alpine
env:
- name: GIT_TERMINAL_PROMPT
value: "0"
script: |
#!/bin/sh
set -eu
apk add --no-cache git nodejs
cd /workspace/source/repo
node <<'NODE' > /workspace/source/env-identity
const { createHash } = require("node:crypto");
const { readFileSync } = require("node:fs");
const inputs = [
["baseImage", "oven/bun:1.2.15-alpine"],
["systemPackages", "ca-certificates git kubectl nodejs openssh-client"],
["containerfile", readFileSync("deploy/container/Containerfile", "utf8")],
["bootScript", readFileSync("deploy/runtime/boot/agentrun-boot.sh", "utf8")],
["bootMgr", readFileSync("deploy/runtime/boot/agentrun-mgr.sh", "utf8")],
["bootRunner", readFileSync("deploy/runtime/boot/agentrun-runner.sh", "utf8")],
["packageJson", readFileSync("package.json", "utf8")],
["bunLock", readFileSync("bun.lock", "utf8")],
["tsconfig", readFileSync("tsconfig.json", "utf8")],
];
process.stdout.write(createHash("sha256").update(JSON.stringify(inputs)).digest("hex").slice(0, 20));
NODE
env_identity="$(cat /workspace/source/env-identity)"
rm -rf /workspace/source/gitops-prev
if git clone --depth=1 --branch "$(params.gitops-branch)" "$(params.git-read-url)" /workspace/source/gitops-prev >/tmp/agentrun-prev-gitops.log 2>&1; then
prev_catalog=/workspace/source/gitops-prev/deploy/artifact-catalog.v01.json
else
prev_catalog=/dev/null
fi
AGENTRUN_ENV_IDENTITY="$env_identity" \
AGENTRUN_PREV_CATALOG="$prev_catalog" \
AGENTRUN_REVISION="$(params.revision)" \
AGENTRUN_GITOPS_BRANCH="$(params.gitops-branch)" \
AGENTRUN_REGISTRY_PREFIX="$(params.registry-prefix)" \
node <<'NODE'
const { readFileSync, writeFileSync } = require("node:fs");
const envIdentity = process.env.AGENTRUN_ENV_IDENTITY;
const revision = process.env.AGENTRUN_REVISION;
const gitopsBranch = process.env.AGENTRUN_GITOPS_BRANCH;
let previousService = null;
try {
const catalog = JSON.parse(readFileSync(process.env.AGENTRUN_PREV_CATALOG, "utf8"));
previousService = (catalog.services || []).find((item) => item.serviceId === "agentrun-mgr" && item.envIdentity === envIdentity && /^sha256:[a-f0-9]{64}$/.test(item.envDigest || item.digest || "")) || null;
} catch {}
const reused = previousService !== null;
const plan = {
lane: "v0.1",
sourceBranch: "v0.1",
gitopsBranch,
sourceCommitId: revision,
envIdentity,
toolchainInputs: ["oven/bun:1.2.15-alpine", "deploy/container/Containerfile", "deploy/runtime/boot/*.sh", "package.json", "bun.lock", "tsconfig.json", "apk:ca-certificates git kubectl nodejs openssh-client"],
buildServices: reused ? [] : ["agentrun-mgr"],
reusedServices: reused ? ["agentrun-mgr"] : [],
unsafeReuseServices: [],
previousService,
summary: `build=${reused ? 0 : 1} reuse=${reused ? 1 : 0} unsafeReuse=0`,
};
writeFileSync("/workspace/source/ci-plan.json", `${JSON.stringify(plan, null, 2)}\n`);
writeFileSync("/tekton/results/env-identity", envIdentity);
writeFileSync("/tekton/results/build-count", String(plan.buildServices.length));
writeFileSync("/tekton/results/reuse-count", String(plan.reusedServices.length));
writeFileSync("/tekton/results/summary", plan.summary);
console.log(JSON.stringify({ event: "agentrun-ci-plan", status: "succeeded", sourceCommitId: revision, envIdentity, buildServices: plan.buildServices, reusedServices: plan.reusedServices, unsafeReuseServices: [], summary: plan.summary }));
NODE
params:
- name: git-read-url
value: $(params.git-read-url)
- name: gitops-branch
value: $(params.gitops-branch)
- name: revision
value: $(params.revision)
- name: registry-prefix
value: $(params.registry-prefix)
- name: image-publish
runAfter: [plan-artifacts]
workspaces:
- name: source
workspace: source
@@ -102,6 +199,8 @@ spec:
- name: image
- name: digest
- name: repository-digest
- name: env-identity
- name: status
sidecars:
- name: buildkitd
image: moby/buildkit:rootless
@@ -145,7 +244,7 @@ spec:
volumeMounts:
- name: buildkit-bin
mountPath: /workspace/buildkit-bin
- name: build-and-push
- name: build-or-reuse
image: oven/bun:1.2.15-alpine
env:
- name: HTTP_PROXY
@@ -157,9 +256,28 @@ spec:
script: |
#!/bin/sh
set -eu
apk add --no-cache curl
apk add --no-cache curl nodejs
cd /workspace/source/repo
image="$(params.registry-prefix)/agentrun-mgr:$(params.revision)"
env_identity="$(cat /workspace/source/env-identity)"
if node -e 'const p=require("/workspace/source/ci-plan.json"); process.exit((p.buildServices||[]).length===0 ? 0 : 1)'; then
node <<'NODE'
const { readFileSync, writeFileSync } = require("node:fs");
const plan = JSON.parse(readFileSync("/workspace/source/ci-plan.json", "utf8"));
const service = plan.previousService;
if (!service) throw new Error("reuse plan missing previousService");
const image = service.envImage || service.image;
const digest = service.envDigest || service.digest;
const repositoryDigest = service.envRepositoryDigest || service.repositoryDigest;
writeFileSync("/tekton/results/image", image);
writeFileSync("/tekton/results/digest", digest);
writeFileSync("/tekton/results/repository-digest", repositoryDigest);
writeFileSync("/tekton/results/env-identity", plan.envIdentity);
writeFileSync("/tekton/results/status", "reused");
console.log(JSON.stringify({ event: "agentrun-env-image", status: "reused", serviceId: "agentrun-mgr", envIdentity: plan.envIdentity, image, digest, summary: plan.summary }));
NODE
exit 0
fi
image="$(params.registry-prefix)/agentrun-mgr-env:${env_identity}"
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
@@ -174,12 +292,15 @@ spec:
--opt build-arg:HTTPS_PROXY=http://127.0.0.1:10808 \
--opt build-arg:NO_PROXY=hyueapi.com,.hyueapi.com,127.0.0.1,localhost,::1,10.42.0.0/16,10.43.0.0/16,.svc,.cluster.local \
--output type=image,name="$image",push=true,registry.insecure=true
digest="$(curl -fsSI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "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}')"
digest="$(curl -fsSI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "http://127.0.0.1:5000/v2/agentrun/agentrun-mgr-env/manifests/$env_identity" | awk -F': ' 'tolower($1)=="docker-content-digest" {gsub(/\r/,"",$2); print $2; exit}')"
test -n "$digest"
curl -fsSI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "http://127.0.0.1:5000/v2/agentrun/agentrun-mgr/manifests/$digest" >/dev/null
curl -fsSI -H "Accept: application/vnd.docker.distribution.manifest.v2+json" "http://127.0.0.1:5000/v2/agentrun/agentrun-mgr-env/manifests/$digest" >/dev/null
printf '%s' "$image" > /tekton/results/image
printf '%s' "$digest" > /tekton/results/digest
printf '%s' "$(params.registry-prefix)/agentrun-mgr@$digest" > /tekton/results/repository-digest
printf '%s' "$(params.registry-prefix)/agentrun-mgr-env@$digest" > /tekton/results/repository-digest
printf '%s' "$env_identity" > /tekton/results/env-identity
printf '%s' built > /tekton/results/status
printf '{"event":"agentrun-env-image","status":"built","serviceId":"agentrun-mgr","envIdentity":"%s","image":"%s","digest":"%s"}\n' "$env_identity" "$image" "$digest"
volumeMounts:
- name: buildkit-bin
mountPath: /workspace/buildkit-bin
@@ -200,47 +321,85 @@ spec:
workspaces:
- name: source
workspace: source
- name: git-ssh
workspace: git-ssh
taskSpec:
params:
- name: git-url
- name: git-write-url
- name: gitops-branch
- name: revision
- name: registry-prefix
- name: image
- name: digest
- name: repository-digest
- name: env-identity
- name: image-status
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
- name: GIT_TERMINAL_PROMPT
value: "0"
- name: AGENTRUN_IMAGE
value: $(params.image)
- name: AGENTRUN_DIGEST
value: $(params.digest)
- name: AGENTRUN_REPOSITORY_DIGEST
value: $(params.repository-digest)
- name: AGENTRUN_ENV_IDENTITY
value: $(params.env-identity)
- name: AGENTRUN_IMAGE_STATUS
value: $(params.image-status)
- name: AGENTRUN_REVISION
value: $(params.revision)
- name: AGENTRUN_GITOPS_BRANCH
value: $(params.gitops-branch)
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
apk add --no-cache git openssh-client nodejs
cd /workspace/source/repo
cat > /workspace/source/artifact-catalog.v01.json <<EOF
{"lane":"v0.1","sourceBranch":"v0.1","gitopsBranch":"$(params.gitops-branch)","sourceCommitId":"$(params.revision)","services":[{"serviceId":"agentrun-mgr","image":"$(params.image)","digest":"$(params.digest)","repositoryDigest":"$(params.repository-digest)","imageTag":"$(params.revision)"}]}
EOF
node <<'NODE' > /workspace/source/artifact-catalog.v01.json
const { readFileSync } = require("node:fs");
const plan = JSON.parse(readFileSync("/workspace/source/ci-plan.json", "utf8"));
const service = {
serviceId: "agentrun-mgr",
artifactKind: "env-reuse",
status: process.env.AGENTRUN_IMAGE_STATUS,
image: process.env.AGENTRUN_IMAGE,
digest: process.env.AGENTRUN_DIGEST,
repositoryDigest: process.env.AGENTRUN_REPOSITORY_DIGEST,
imageTag: process.env.AGENTRUN_ENV_IDENTITY,
envIdentity: process.env.AGENTRUN_ENV_IDENTITY,
envImage: process.env.AGENTRUN_IMAGE,
envDigest: process.env.AGENTRUN_DIGEST,
envRepositoryDigest: process.env.AGENTRUN_REPOSITORY_DIGEST,
bootCommit: process.env.AGENTRUN_REVISION,
bootScript: "deploy/runtime/boot/agentrun-boot.sh",
provenance: {
sourceCommitId: process.env.AGENTRUN_REVISION,
toolchainInputs: plan.toolchainInputs,
buildServices: plan.buildServices,
reusedServices: plan.reusedServices,
unsafeReuseServices: plan.unsafeReuseServices,
previousSourceCommitId: plan.previousService?.bootCommit || null,
},
};
const catalog = {
lane: "v0.1",
sourceBranch: "v0.1",
gitopsBranch: process.env.AGENTRUN_GITOPS_BRANCH,
sourceCommitId: process.env.AGENTRUN_REVISION,
summary: plan.summary,
services: [service],
};
console.log(JSON.stringify(catalog, null, 2));
NODE
rm -rf /workspace/source/rendered
bun scripts/agentrun-gitops-render.ts --out /workspace/source/rendered --source-commit "$(params.revision)" --registry-prefix "$(params.registry-prefix)" --catalog /workspace/source/artifact-catalog.v01.json --require-catalog
rm -rf /workspace/source/gitops
git clone --branch "$(params.gitops-branch)" "$(params.git-url)" /workspace/source/gitops || {
git clone "$(params.git-url)" /workspace/source/gitops
git clone --branch "$(params.gitops-branch)" "$(params.git-write-url)" /workspace/source/gitops || {
git clone "$(params.git-write-url)" /workspace/source/gitops
cd /workspace/source/gitops
git checkout --orphan "$(params.gitops-branch)"
git rm -rf . >/dev/null 2>&1 || true
@@ -256,9 +415,10 @@ spec:
git add deploy
git commit -m "gitops: promote agentrun v0.1 $(params.revision)" || true
git push origin "$(params.gitops-branch)"
printf '{"event":"agentrun-gitops-promote","status":"succeeded","sourceCommitId":"%s","envIdentity":"%s","imageStatus":"%s","summary":%s}\n' "$(params.revision)" "$(params.env-identity)" "$(params.image-status)" "$(node -e 'const p=require("/workspace/source/ci-plan.json"); console.log(JSON.stringify(p.summary))')"
params:
- name: git-url
value: $(params.git-url)
- name: git-write-url
value: $(params.git-write-url)
- name: gitops-branch
value: $(params.gitops-branch)
- name: revision
@@ -271,3 +431,7 @@ spec:
value: $(tasks.image-publish.results.digest)
- name: repository-digest
value: $(tasks.image-publish.results.repository-digest)
- name: env-identity
value: $(tasks.image-publish.results.env-identity)
- name: image-status
value: $(tasks.image-publish.results.status)
@@ -16,6 +16,10 @@ spec:
params:
- name: revision
value: REPLACE_WITH_FULL_SOURCE_COMMIT
- name: git-read-url
value: http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git
- name: git-write-url
value: http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git
workspaces:
- name: source
volumeClaimTemplate:
+26 -11
View File
@@ -9,6 +9,8 @@
- `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`
- Source checkout 与 GitOps promotion 必须优先走 `devops-infra` git mirror/relayPipeline 不直接依赖 GitHub SSH fetch/push。
- Runtime image 采用 env reuse:依赖、Bun/Node、系统包、lockfile、Containerfile 和 boot 脚本构成 env identity;普通 TS/文档/CLI 业务源码变更只改变 boot commit,不重建 env image。
- 发布证据以 live runtime、Argo desired state、GitOps branch、Tekton 证据和干净 source worktree 顺序判断。
## 固定命名
@@ -21,12 +23,17 @@
| Worktree root | `G14:/root/agentrun-v01/.worktree/{task}` |
| Runtime namespace | `agentrun-v01` |
| GitOps branch | `v0.1-gitops` |
| Git mirror read URL | `http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git` |
| Git mirror write URL | `http://git-mirror-write.devops-infra.svc.cluster.local/pikasTech/agentrun.git` |
| Git mirror cache | `devops-infra:/cache/pikasTech/agentrun.git` |
| Artifact catalog | `v0.1-gitops:deploy/artifact-catalog.v01.json` |
| Runtime path | `v0.1-gitops:deploy/gitops/g14/runtime-v01` |
| Tekton namespace | `agentrun-ci` |
| Tekton Pipeline | `agentrun-v01-ci-image-publish` |
| Tekton ServiceAccount | `agentrun-v01-tekton-runner` |
| PipelineRun prefix | `agentrun-v01-ci-<short12>` |
| Env image repository | `127.0.0.1:5000/agentrun/agentrun-mgr-env:<envIdentity>` |
| Boot script | `deploy/runtime/boot/agentrun-boot.sh` |
| Argo CD AppProject | `argocd/agentrun-v01` |
| Argo CD Application | `argocd/agentrun-g14-v01` |
@@ -45,7 +52,9 @@ CI 的最小检查应覆盖:
- Bun/TS 单元自测试,包括 manager schema、adapter mock、Codex fake app-server stdio client 和 CLI JSON 输出。
- `deploy/deploy.json` 与 GitOps render 只读校验。
容器镜像可以直接运行 TS 入口,也可以运行同一 source commit 构建出的 JS artifact;无论选择哪种形式,artifact catalog 必须记录完整 source commit 和 image digest。CI/CD 仍然只允许纯 Tekton + Argo CD,不因 Bun 工具链引入自定义 runner、长期 poller 或源分支生成物提交。
容器镜像必须区分 env identity 与 source commit。`agentrun-mgr-env:<envIdentity>` 只包含 Bun runtime、生产依赖、Codex CLI、git/kubectl/node 等系统依赖和 boot 脚本,不 bake `src/``scripts/` 或某个业务 source commit。运行时通过 `AGENTRUN_BOOT_COMMIT` 从 git mirror 按完整 SHA 做 `git fetch --depth=1 origin <sha>`,再用 env image 内的 `node_modules` 启动 manager 或 runner。CI/CD 仍然只允许纯 Tekton + Argo CD,不因 Bun 工具链引入自定义 runner、长期 poller 或源分支生成物提交。
Env identity 的输入至少包含:Bun base image、系统包列表、`deploy/container/Containerfile``deploy/runtime/boot/*.sh``package.json``bun.lock``tsconfig.json`。只改业务 TS、文档、模板中不影响 runtime env 的内容时,planner 必须输出 `build=0 reuse=1 unsafeReuse=0`,并复用上一版 catalog 中同一 `envIdentity` 的 digest。
## 真相源
@@ -54,9 +63,11 @@ CI 的最小检查应覆盖:
1. Live runtime`agentrun-v01` namespace 中 Deployment/Job/Pod ready、事件、日志和 service health。
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 终态。
4. Tekton 执行证据:PipelineRun、TaskRun result、env identity、build/reuse summary、image digest 和 promotion 终态。
5. 干净 source worktree`G14:/root/agentrun-v01``origin/v0.1`、render 脚本、deploy intent 和 `--no-write` 输出。
`devops-infra` git mirror 是 CI/CD source 与 GitOps relay,不是新的 source truth。source truth 仍是 GitHub `v0.1`GitOps truth 仍是 `v0.1-gitops`mirror 只负责降低 Pipeline 中的外网 fetch/push 抖动,并让 exact commit fetch 可在集群内稳定命中。受控入口是 UniDesk CLI`agentrun v01 git-mirror status|sync|flush``status` 必须展示 `localV01``githubV01``localGitops``githubGitops``pendingFlush` 和 exact full-SHA fetch 结果;`trigger-current` 在创建 PipelineRun 前必须先检查 `localV01`,必要时同步 mirror,再继续。
`master` 记忆、`/root/agentrun` 历史固定目录、`agentrun_dev``agentrun_prod`、D601 legacy 路径、临时 worktree 或本地容器只能作为线索,不能作为 `v0.1` 发布通过证据。
## Source 与 GitOps 分层
@@ -76,7 +87,7 @@ CI 的最小检查应覆盖:
`v0.1-gitops` branch 必须包含:
- `deploy/artifact-catalog.v01.json`,记录 image tag、digest、source commit、service identity、publish/reuse 状态。
- `deploy/artifact-catalog.v01.json`,记录 env image tag、digest、source commit、env identity、service identity、build/reuse 状态和 provenance
- `deploy/gitops/g14/runtime-v01/**`,作为 Argo CD 消费的 desired state。
- 必要 generated metadata,但不得包含 Secret 值。
@@ -107,11 +118,11 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st
标准链路如下:
1. 人工或受控 GitHub/UniDesk 入口为某个 `origin/v0.1` source commit 创建 Tekton `PipelineRun`;触发器可以是 Tekton Triggers 或手动 `PipelineRun`,但不能是长期自定义 runner。
2. `prepare-source` checkout `v0.1` source,并从 `v0.1-gitops` 读取上一版 `deploy/artifact-catalog.v01.json`
2. `prepare-source` 通过 git mirror read URL 按完整 SHA checkout `v0.1` source,不使用 branch tip 近似 checkout
3. 原语校验 task 只覆盖文档治理、spec 链接、`deploy/deploy.json` schema、轻量语法和必要单元测试;旧 `dev/prod` gate 不进入 lane。
4. Plan task 读取 `deploy/deploy.json` 与上一版 artifact catalog判断 affected/reused servicesplanner 只能输出 TaskRun result 或临时 workspace 文件,不回写 source。
5. Affected service 通过 Tekton Task 内的 BuildKit/kaniko/buildah 之一发布到 G14 本地 registryreused service 复用 catalog digest。不得使用 Docker daemon、DIND 或仓库外自定义 runner。
6. Promotion task 用 publish results 刷新 `deploy/artifact-catalog.v01.json`,并用 `deploy/deploy.json` render `deploy/gitops/g14/runtime-v01/**`,只推送到 `v0.1-gitops`
4. Plan task 通过 git mirror read URL 读取上一版 `v0.1-gitops:deploy/artifact-catalog.v01.json`,计算 `envIdentity`判断 affected/reused servicesplanner 只能输出 TaskRun result 或临时 workspace 文件,不回写 source。
5. Affected env image 通过 Tekton Task 内的 BuildKit 发布到 G14 本地 registryreused service 直接复用 catalog digest。不得使用 Docker daemon、DIND 或仓库外自定义 runner。
6. Promotion task 用 publish/reuse results 刷新 `deploy/artifact-catalog.v01.json`,并用 `deploy/deploy.json` render `deploy/gitops/g14/runtime-v01/**`,只通过 git mirror write URL 推送到本地 `v0.1-gitops` relay。发布收口再用 `agentrun v01 git-mirror flush --confirm` 把 GitOps branch 快进回 GitHub
7. Argo CD Application `agentrun-g14-v01``v0.1-gitops:deploy/gitops/g14/runtime-v01` 同步到 `agentrun-v01`
8. 验收只观察 `agentrun-v01` runtime、Argo revision/sync/health 和对应 service health。
@@ -119,10 +130,10 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st
镜像身份同时是 RuntimeAssembly 的 `BackendImageRef` 来源;四要素总模型见 [spec-v01-runtime-assembly.md](spec-v01-runtime-assembly.md)。本文只定义 image digest、artifact catalog 和 GitOps promotion 边界,不定义 profile、session 或 resource bundle 字段。
- `v0.1` 镜像 tag 使用完整 40 位 source commitId
- `v0.1` env image tag 使用 `envIdentity`,而不是 source commitId。source commitId 通过 `AGENTRUN_BOOT_COMMIT` 和 catalog `bootCommit` 进入 runtime
- Runtime manifest 必须使用 digest pin 作为部署身份;G14 本地 registry 对同一 tag 的默认 HEAD 可能返回 Docker schema1 compatibility digestTekton 必须用 `Accept: application/vnd.docker.distribution.manifest.v2+json` 采集 containerd 可直接拉取的 schema2 manifest digest,并在写入 catalog 前按 digest HEAD 验证。
- Catalog 必须记录 lane、source branch、GitOps branch、source commitId、serviceId、image tag、digest、component identity 和 publish/reuse 状态。
- 同一 source commit 对同一 service 应生成同一镜像;lane 差异放在 manifest、env、SecretRef、namespace、RBAC 和 runtime config 中,不 bake 进镜像。
- Catalog 必须记录 lane、source branch、GitOps branch、source commitId、serviceId、env image tag、digest、env identity、boot commit、toolchain inputs 和 build/reuse 状态。
- 同一 env identity 对同一 service 应生成同一镜像;lane 差异放在 manifest、env、SecretRef、namespace、RBAC 和 runtime config 中,不 bake 进镜像。普通 source commit 差异由 boot checkout 选择。
- `deploy/deploy.json` 只承载人写 runtime intent,不承载 digest、publish state 或 reuse evidence。
- Source branch 不得因为 promotion 出现自动提交;若发布后 source branch 变化,必须是人工修改源码、测试、文档、模板或 `deploy/deploy.json`
@@ -152,7 +163,9 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st
- `v0.1-gitops` branch 和 `deploy/gitops/g14/runtime-v01` 成为 Argo desired state 来源。
- `deploy/deploy.json` 是 CD 唯一手写真相源;source branch 不跟踪 artifact catalog、runtime generated manifests、digest 或 publish state。
- Tekton/Argo 路径不包含自定义 runner、CI.json runner、长期自研 poller 或 control-plane reconciler。
- Runtime manifest 使用 digest pincatalog 记录完整 source commit 与 digest。
- Runtime manifest 使用 digest pincatalog 记录完整 source commit、env identity、build/reuse summary 与 digest。
- `agentrun v01 git-mirror status``sourceInSync=true``exactFetch.localV01=true`,并能用 mirror 对当前 full SHA 执行 shallow fetch。
- 小的业务源码变更完成后,PipelineRun `planArtifacts.summary` 应为 `build=0 reuse=1 unsafeReuse=0``image-publish` task result `status=reused`,且不出现新的 env image build。
- 发布完成后可通过 `G14:k3s` 读取 `agentrun-v01` Pod ready、service health 和对应 image digest。
## 规格的实现情况
@@ -164,4 +177,6 @@ Tekton promotion 可以读取 `deploy/deploy.json` 来 render runtime desired st
| `agentrun-v01` namespace | 已实现/已通过主闭环 | GitOps lane 已同步 manager、Postgres、ServiceAccount、SecretRef 和 runner Job 所需对象;发布前仍按 [spec-v01-validation.md](spec-v01-validation.md) 手动复验。 |
| `v0.1-gitops` branch | 已实现 | Tekton promotion 生成 artifact catalog 与 runtime desired stateArgo 从 `deploy/gitops/g14/runtime-v01` 同步。 |
| 纯 Tekton/Argo lane | 已实现/已通过主闭环 | `agentrun-ci` Pipeline、BuildKit 镜像发布、GitOps promotion 和 Argo Application 已形成闭环;不得回退到自定义 runner 或 dev/prod。 |
| git mirror/relay | 已实现 | UniDesk `agentrun v01 git-mirror status|sync|flush` 管理 `devops-infra:/cache/pikasTech/agentrun.git`Pipeline read/write 默认使用 mirror URL`trigger-current` 会在创建 PipelineRun 前 pre-sync `v0.1`。 |
| env reuse | 已实现/待持续验收 | Pipeline 计算 `envIdentity`,命中上一版 catalog 时输出 `build=0 reuse=1 unsafeReuse=0` 并复用 env image digest;首次 env identity 或依赖/Containerfile/boot 变化才构建 `agentrun-mgr-env:<envIdentity>`。 |
| `dev/prod` 废弃口径 | 已定义 | 本文明确 `agentrun_dev``agentrun_prod` 不作为当前规格目标。 |
+53 -11
View File
@@ -18,9 +18,29 @@ interface ArtifactCatalog {
sourceBranch: string;
gitopsBranch: string;
sourceCommitId: string;
services: Array<{ serviceId: string; image: string; digest: string; repositoryDigest: string; imageTag: string }>;
summary?: string;
services: CatalogService[];
}
interface CatalogService {
serviceId: string;
image: string;
digest: string;
repositoryDigest: string;
imageTag: string;
artifactKind?: string;
status?: string;
envIdentity?: string;
envImage?: string;
envDigest?: string;
envRepositoryDigest?: string;
bootCommit?: string;
bootScript?: string;
provenance?: JsonRecord;
}
const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git";
export async function runGitopsRenderCli(argv: string[]): Promise<void> {
try {
const options = parseArgs(argv);
@@ -52,29 +72,39 @@ export async function renderGitops(options: RenderOptions): Promise<JsonRecord>
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, options.sourceCommit));
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 };
return { outDir: options.outDir, runtimeNamespace, gitopsBranch, runtimePath, image: repositoryDigestForService(image), sourceCommit: options.sourceCommit, envIdentity: image.envIdentity ?? null, artifactStatus: image.status ?? null };
}
async function loadCatalog(options: RenderOptions, gitopsBranch: string): Promise<ArtifactCatalog> {
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}`;
const image = `${options.registryPrefix}/agentrun-mgr-env:${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 }],
summary: "build=1 reuse=0 unsafeReuse=0",
services: [{ serviceId: "agentrun-mgr", artifactKind: "env-reuse", status: "placeholder", image, digest, repositoryDigest: `${options.registryPrefix}/agentrun-mgr-env@${digest}`, imageTag: options.sourceCommit, envIdentity: options.sourceCommit, envImage: image, envDigest: digest, envRepositoryDigest: `${options.registryPrefix}/agentrun-mgr-env@${digest}`, bootCommit: options.sourceCommit, bootScript: "deploy/runtime/boot/agentrun-boot.sh" }],
};
}
function imageForService(catalog: ArtifactCatalog, serviceId: string, options: RenderOptions): { repositoryDigest: string } {
function imageForService(catalog: ArtifactCatalog, serviceId: string, options: RenderOptions): CatalogService {
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}` };
const digest = service.envDigest ?? service.digest;
if (!/^sha256:[a-f0-9]{64}$/u.test(digest)) throw new AgentRunError("schema-invalid", `catalog service ${serviceId} has invalid digest`, { httpStatus: 2 });
if (options.requireCatalog && digest === `sha256:${"0".repeat(64)}`) throw new AgentRunError("schema-invalid", "placeholder digest is not allowed in promotion render", { httpStatus: 2 });
return service;
}
function repositoryDigestForService(service: CatalogService): string {
if (service.envRepositoryDigest) return service.envRepositoryDigest;
if (service.repositoryDigest) return service.repositoryDigest;
const image = service.envImage ?? service.image;
const digest = service.envDigest ?? service.digest;
return `${image.slice(0, image.lastIndexOf(":"))}@${digest}`;
}
function projectYaml(namespace: string): string {
@@ -209,7 +239,9 @@ spec:
`;
}
function managerYaml(namespace: string, image: { repositoryDigest: string }, sourceCommit: string): string {
function managerYaml(namespace: string, image: CatalogService, sourceCommit: string): string {
const imageRef = repositoryDigestForService(image);
const envIdentity = image.envIdentity ?? image.imageTag ?? "unknown";
return `apiVersion: v1
kind: ServiceAccount
metadata:
@@ -245,11 +277,13 @@ spec:
app.kubernetes.io/name: agentrun-mgr
annotations:
agentrun.pikastech.local/lane: v0.1
agentrun.pikastech.local/source-commit: ${JSON.stringify(sourceCommit)}
agentrun.pikastech.local/env-identity: ${JSON.stringify(envIdentity)}
spec:
serviceAccountName: agentrun-v01-mgr
containers:
- name: mgr
image: ${image.repositoryDigest}
image: ${imageRef}
imagePullPolicy: IfNotPresent
ports:
- name: http
@@ -264,12 +298,20 @@ spec:
key: DATABASE_URL
- name: AGENTRUN_SOURCE_COMMIT
value: ${JSON.stringify(sourceCommit)}
- name: AGENTRUN_BOOT_COMMIT
value: ${JSON.stringify(sourceCommit)}
- name: AGENTRUN_BOOT_MODE
value: mgr
- name: AGENTRUN_BOOT_REPO_URL
value: ${JSON.stringify(defaultBootRepoUrl)}
- name: AGENTRUN_ENV_IDENTITY
value: ${JSON.stringify(envIdentity)}
- name: AGENTRUN_RUNTIME_NAMESPACE
value: ${JSON.stringify(namespace)}
- name: AGENTRUN_INTERNAL_MGR_URL
value: ${JSON.stringify(`http://agentrun-mgr.${namespace}.svc.cluster.local:8080`)}
- name: AGENTRUN_RUNNER_IMAGE
value: ${JSON.stringify(image.repositoryDigest)}
value: ${JSON.stringify(imageRef)}
- name: AGENTRUN_RUNNER_SERVICE_ACCOUNT
value: "agentrun-v01-runner"
readinessProbe:
+7 -1
View File
@@ -2,6 +2,8 @@ import { stableHash } from "../common/validation.js";
import type { BackendProfile, ExecutionPolicy, JsonRecord, JsonValue, RunRecord, SecretRef } from "../common/types.js";
import { backendProfileSpec } from "../common/backend-profiles.js";
const defaultBootRepoUrl = "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/agentrun.git";
export interface RunnerJobRenderOptions {
run: RunRecord;
commandId: string;
@@ -124,7 +126,7 @@ export function renderRunnerJobManifest(options: RunnerJobRenderOptions): { mani
name: "runner",
image: options.image,
imagePullPolicy: options.imagePullPolicy ?? "IfNotPresent",
command: ["bun", "src/runner/main.ts"],
command: ["/opt/agentrun/deploy/runtime/boot/agentrun-runner.sh"],
env,
volumeMounts: [
{ name: "runner-home", mountPath: "/home/agentrun" },
@@ -164,6 +166,10 @@ function runnerEnv(options: RunnerJobRenderOptions, context: { namespace: string
{ name: "AGENTRUN_RESOURCE_BUNDLE_JSON", value: JSON.stringify(options.run.resourceBundleRef ?? null) },
{ name: "AGENTRUN_WORKSPACE_ROOT", value: "/home/agentrun/workspaces" },
{ name: "AGENTRUN_SOURCE_COMMIT", value: context.sourceCommit },
{ name: "AGENTRUN_BOOT_COMMIT", value: context.sourceCommit },
{ name: "AGENTRUN_BOOT_REPO_URL", value: defaultBootRepoUrl },
{ name: "AGENTRUN_BOOT_MODE", value: "runner" },
{ name: "AGENTRUN_APP_ROOT", value: "/home/agentrun/agentrun-source" },
{ name: "AGENTRUN_RUNTIME_NAMESPACE", value: context.namespace },
{ name: "AGENTRUN_K8S_JOB_NAME", value: context.jobName },
{ name: "AGENTRUN_LOG_PATH", value: "/tmp/agentrun-runner.jsonl" },
@@ -15,6 +15,8 @@ const execFile = promisify(execFileCallback);
const selfTest: SelfTestCase = async (context) => {
const containerfile = await readFile(path.join(context.root, "deploy/container/Containerfile"), "utf8");
assert.ok(containerfile.includes(" git ") && containerfile.includes(" openssh-client"), "runtime image must include git and openssh-client for ResourceBundleRef checkout");
assert.ok(containerfile.includes("deploy/runtime/boot") && containerfile.includes("agentrun-mgr.sh"), "runtime image must boot through the env-reuse source checkout script");
assert.ok(!containerfile.includes("COPY src ./src"), "runtime env image must not bake source files into every source commit image");
const fakeKubectl = path.join(context.tmp, "fake-kubectl-hwlab.js");
const createdManifest = path.join(context.tmp, "created-hwlab-runner-job.json");
await writeFile(fakeKubectl, `#!/usr/bin/env bun
@@ -54,6 +56,8 @@ console.log(JSON.stringify({ apiVersion: manifest.apiVersion, kind: manifest.kin
);
const manifest = JSON.parse(await readFile(createdManifest, "utf8")) as JsonRecord;
assert.ok(JSON.stringify(manifest).includes("AGENTRUN_RESOURCE_BUNDLE_JSON"));
assert.ok(JSON.stringify(manifest).includes("/opt/agentrun/deploy/runtime/boot/agentrun-runner.sh"));
assert.ok(JSON.stringify(manifest).includes("AGENTRUN_BOOT_COMMIT"));
assertNoSecretLeak(created);
const pendingCancel = await createHwlabRun(client, context, bundle, "hwlab-session-cancel-pending", "cancel pending", "hwlab-command-cancel-pending");