Files
Agent-System-Dev/.gitea/workflows/ci.yaml
T
agent-exec-transport 0aae8ed99f
ci / svc-realcombo (pull_request) Successful in 3m46s
ci / test (pull_request) Successful in 8m45s
ci / test-pg (pull_request) Failing after 13m7s
agent-exec transport 62f937e2b36ba8a042082602fc93d1cb/7c9a5c05b51c
2026-10-21 14:51:51 +00:00

728 lines
40 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: ci
on:
pull_request:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
env:
GOPROXY: https://goproxy.cn,direct
steps:
- name: Checkout
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# Credential handling stays inline: no file from the candidate is executed.
# Workflow provenance and runner isolation still require platform controls.
python3 - <<'PY_CHECKOUT'
import os
from pathlib import Path
import re
import subprocess
import sys
from urllib.parse import urlsplit
# 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。
# 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态
# 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为
# http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空
# netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed_server = urlsplit(SERVER)
if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc:
sys.exit("Checkout server must be the platform-injected http(s) URL")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
# Reviewed allowlist, independent of candidate .gitmodules. Add repositories
# here only with an explicit review of the checkout credential's scope.
REPOSITORIES = {
"modules/core": "Agent-System-Core",
"modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend",
"modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents",
"deps/agent-system-core": "Agent-System-Core",
"deps/agent-document-renderer": "Agent-Document-Renderer",
"deps/development-guidelines": "Development-Guidelines",
"deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad",
"deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command",
"deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System",
"deps/development-manager": "Development-Manager",
"development/guidelines": "Development-Guidelines",
}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
if not root_token or not component_token:
sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN")
if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev":
sys.exit("Checkout repository is outside the reviewed allowlist")
sha = os.environ.get("GITHUB_SHA", "")
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
sys.exit("Checkout requires a fixed commit SHA")
# Discard inherited Git tracing/config/SSH hooks. Auth lives only in one
# Git process tree, in an exact URL-scoped header, never in a URL or file.
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null",
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0")
config = {
"core.hooksPath": "/dev/null", "core.fsmonitor": "false",
"init.templateDir": "", "credential.helper": "",
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
}
def git(repo, *args, extra=None):
values = config | (extra or {})
env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return subprocess.run(["git", "-C", str(repo), *args], env=env,
check=True, stdout=subprocess.PIPE).stdout
def materialize(repo, depth=0):
if depth > 16:
raise ValueError("Submodule depth exceeds the reviewed checkout limit")
links = {}
for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"):
if not entry:
continue
metadata, path = entry.split(b"\t", 1)
mode, kind, commit = metadata.decode().split()
if mode == "160000":
links[path.decode()] = commit
if not links:
return
raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
entries = {}
for item in raw.split(b"\0"):
if not item:
continue
key, value = item.decode().split("\n", 1)
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
if not match:
raise ValueError("Unsupported candidate .gitmodules setting")
name, field = match.groups()
if field in entries.setdefault(name, {}):
raise ValueError("Duplicate candidate .gitmodules setting")
entries[name][field] = value
if set(entries) != set(links):
raise ValueError("Submodule names must match the fixed gitlink paths")
# Validate the entire level before any authenticated component fetch.
for name, fields in entries.items():
repository = REPOSITORIES.get(name)
if not repository or fields.get("path") != name:
raise ValueError("Submodule path is outside the reviewed allowlist")
trusted_url = f"{SERVER}/weichen/{repository}.git"
if fields.get("url") not in {SSH + repository + ".git", trusted_url}:
raise ValueError("Submodule URL is outside the reviewed allowlist")
for name in entries:
url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git"
# Persist only the reviewed public URL so Git recognizes the
# initialized submodule after process-scoped config is gone.
git(repo, "config", f"submodule.{name}.url", url)
git(repo, "submodule", "update", "--init", "--checkout", "--", name,
extra={f"submodule.{name}.url": url,
f"submodule.{name}.update": "checkout",
f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token})
child = repo / name
if git(child, "rev-parse", "HEAD").decode().strip() != links[name]:
raise ValueError("Materialized submodule does not match the fixed SHA")
materialize(child, depth + 1)
try:
root = Path.cwd()
# An ephemeral clean checkout is required; do not reuse prior Git state.
if (root / ".git").exists():
raise ValueError("Checkout requires a fresh workspace")
git(root, "init", "-q")
url = SERVER + "/weichen/Agent-System-Dev.git"
git(root, "remote", "add", "origin", url)
git(root, "fetch", "--depth=1", "origin", sha,
extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token})
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
materialize(root)
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit(f"Checkout refused: {error}")
print("Checkout complete; credentials were not persisted in Git configuration")
PY_CHECKOUT
unset TOKEN SUBMODULE_TOKEN
- name: PR base gate
run: python3 scripts/task_pr_check.py ci
- name: Install Go 1.27.1
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Validate AGENT-EXECUTION.md with the pinned shared executor
run: scripts/agent-exec validate --repo .
- name: Governance check with the pinned kit
run: scripts/dm governance check --repo .
- name: Install Node 22
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Install house-style Python packages
run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl
- name: make test (boundaries, pins, libs, core, backend, frontend, ui, agent-command)
run: make test
- name: Build the single binary
run: make build
# 协作存储方案 U2:真实 PostgreSQL + pgvector + Redis 门禁。
# 缺服务时失败而非跳过(make test-backend-pg 硬校验环境变量);
# 组件版本随 job 输出固定,供验收记录对齐(方案 KTD12/U2)。
test-pg:
# 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业);
# 进入 main 的 PR 与 main 推送跑全量门禁。
if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }}
runs-on: ubuntu-latest
env:
GOPROXY: https://goproxy.cn,direct
steps:
- name: Checkout
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# Credential handling stays inline: no file from the candidate is executed.
# Workflow provenance and runner isolation still require platform controls.
python3 - <<'PY_CHECKOUT'
import os
from pathlib import Path
import re
import subprocess
import sys
from urllib.parse import urlsplit
# 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。
# 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态
# 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为
# http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空
# netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed_server = urlsplit(SERVER)
if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc:
sys.exit("Checkout server must be the platform-injected http(s) URL")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
# Reviewed allowlist, independent of candidate .gitmodules. Add repositories
# here only with an explicit review of the checkout credential's scope.
REPOSITORIES = {
"modules/core": "Agent-System-Core",
"modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend",
"modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents",
"deps/agent-system-core": "Agent-System-Core",
"deps/agent-document-renderer": "Agent-Document-Renderer",
"deps/development-guidelines": "Development-Guidelines",
"deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad",
"deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command",
"deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System",
"deps/development-manager": "Development-Manager",
"development/guidelines": "Development-Guidelines",
}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
if not root_token or not component_token:
sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN")
if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev":
sys.exit("Checkout repository is outside the reviewed allowlist")
sha = os.environ.get("GITHUB_SHA", "")
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
sys.exit("Checkout requires a fixed commit SHA")
# Discard inherited Git tracing/config/SSH hooks. Auth lives only in one
# Git process tree, in an exact URL-scoped header, never in a URL or file.
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null",
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0")
config = {
"core.hooksPath": "/dev/null", "core.fsmonitor": "false",
"init.templateDir": "", "credential.helper": "",
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
}
def git(repo, *args, extra=None):
values = config | (extra or {})
env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return subprocess.run(["git", "-C", str(repo), *args], env=env,
check=True, stdout=subprocess.PIPE).stdout
def materialize(repo, depth=0):
if depth > 16:
raise ValueError("Submodule depth exceeds the reviewed checkout limit")
links = {}
for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"):
if not entry:
continue
metadata, path = entry.split(b"\t", 1)
mode, kind, commit = metadata.decode().split()
if mode == "160000":
links[path.decode()] = commit
if not links:
return
raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
entries = {}
for item in raw.split(b"\0"):
if not item:
continue
key, value = item.decode().split("\n", 1)
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
if not match:
raise ValueError("Unsupported candidate .gitmodules setting")
name, field = match.groups()
if field in entries.setdefault(name, {}):
raise ValueError("Duplicate candidate .gitmodules setting")
entries[name][field] = value
if set(entries) != set(links):
raise ValueError("Submodule names must match the fixed gitlink paths")
# Validate the entire level before any authenticated component fetch.
for name, fields in entries.items():
repository = REPOSITORIES.get(name)
if not repository or fields.get("path") != name:
raise ValueError("Submodule path is outside the reviewed allowlist")
trusted_url = f"{SERVER}/weichen/{repository}.git"
if fields.get("url") not in {SSH + repository + ".git", trusted_url}:
raise ValueError("Submodule URL is outside the reviewed allowlist")
for name in entries:
url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git"
# Persist only the reviewed public URL so Git recognizes the
# initialized submodule after process-scoped config is gone.
git(repo, "config", f"submodule.{name}.url", url)
git(repo, "submodule", "update", "--init", "--checkout", "--", name,
extra={f"submodule.{name}.url": url,
f"submodule.{name}.update": "checkout",
f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token})
child = repo / name
if git(child, "rev-parse", "HEAD").decode().strip() != links[name]:
raise ValueError("Materialized submodule does not match the fixed SHA")
materialize(child, depth + 1)
try:
root = Path.cwd()
# An ephemeral clean checkout is required; do not reuse prior Git state.
if (root / ".git").exists():
raise ValueError("Checkout requires a fresh workspace")
git(root, "init", "-q")
url = SERVER + "/weichen/Agent-System-Dev.git"
git(root, "remote", "add", "origin", url)
git(root, "fetch", "--depth=1", "origin", sha,
extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token})
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
materialize(root)
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit(f"Checkout refused: {error}")
print("Checkout complete; credentials were not persisted in Git configuration")
PY_CHECKOUT
unset TOKEN SUBMODULE_TOKEN
- name: Install Go 1.27.1
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Install house-style Python packages
run: python3 -m pip install --break-system-packages -i https://pypi.tuna.tsinghua.edu.cn/simple python-docx python-pptx openpyxl
- name: Install PostgreSQL, pgvector and Redis
run: |
set -euo pipefail
SUDO=""
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then SUDO=sudo; fi
$SUDO apt-get update -y
$SUDO apt-get install -y postgresql postgresql-contrib postgresql-16-pgvector redis-server
# 固定组件/扩展版本输出(随日志归档,作为验收对齐依据)。
psql --version
redis-server --version
- name: Start PostgreSQL and Redis
run: |
set -euo pipefail
SUDO=""
if command -v sudo >/dev/null 2>&1 && [ "$(id -u)" != 0 ]; then SUDO=sudo; fi
# 运行器容器以 root 运行且无 sudo:以 postgres 身份执行用 runuser/su
# (实测 $SUDO -u 在 root 容器展开成裸 "-u" → exit 127)。
pg_as_postgres() {
if command -v runuser >/dev/null 2>&1; then runuser -u postgres -- "$@";
else su postgres -s /bin/bash -c "$*"; fi
}
$SUDO service postgresql start || $SUDO pg_ctlcluster 16 main start
# 预装 pg_trgm 到 public:与测试辅助的幂等预装一致,锁定的组件契约;
# pgvector 版本一并输出(随日志归档,作为验收对齐依据)。
pg_as_postgres psql -tAc "CREATE EXTENSION IF NOT EXISTS vector"
pg_as_postgres psql -tAc "SELECT extname || '=' || extversion FROM pg_extension WHERE extname IN ('vector','pg_trgm')"
pg_as_postgres psql -c "CREATE USER ci_agent PASSWORD 'ci_job_local_only' CREATEDB"
pg_as_postgres psql -c "CREATE DATABASE agent_test OWNER ci_agent"
pg_as_postgres psql -d agent_test -c "CREATE EXTENSION IF NOT EXISTS pg_trgm"
redis-server --daemonize yes --port 6399 --save '' --appendonly no
redis-cli -p 6399 ping
- name: Backend tests against real PostgreSQL and Redis (required, no silent skip)
env:
AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable
AGENT_REDIS_TEST_ADDR: 127.0.0.1:6399
run: make test-backend-pg
- name: Two-process HTTP acceptance over shared PostgreSQL (U7, required)
env:
AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable
run: make test-multi-instance
- name: RDM lifecycle pin is three fixed SHAs
run: python3 -m unittest tests.test_lifecycle_pin -q && bash scripts/fetch-rdm-pin.sh tests/integration/lifecycle/rdm.pin >/dev/null
- name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition)
env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头,
# 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一
# 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信
# https 基址后再取。只追加 pin 的三个 40 位 hex,不 source pin 文件。
SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
scheme="${GITHUB_SERVER_URL%%://*}"
export GIT_CONFIG_COUNT=4
export GIT_CONFIG_KEY_0="protocol.$scheme.allow"
export GIT_CONFIG_VALUE_0="always"
export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader"
export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN"
export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf"
export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
export GIT_CONFIG_KEY_3="core.hooksPath"
export GIT_CONFIG_VALUE_3="/dev/null"
git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm
mapfile -t pins < <(python3 - <<'PY'
import pathlib
got = {}
for line in pathlib.Path("tests/integration/lifecycle/rdm.pin").read_text().splitlines():
line = line.strip()
if not line or line.startswith("#") or "=" not in line:
continue
key, value = line.split("=", 1)
if key not in ("sha", "prev_sha", "legacy_sha") or len(value) != 40:
raise SystemExit("rdm.pin rejected")
if any(c not in "0123456789abcdef" for c in value):
raise SystemExit("rdm.pin rejected")
got[key] = value
if set(got) != {"sha", "prev_sha", "legacy_sha"}:
raise SystemExit("rdm.pin rejected")
print(got["prev_sha"])
print(got["legacy_sha"])
print(got["sha"])
PY
)
[ "${#pins[@]}" -eq 3 ]
# depth=1 会把三个 pin 变成互不相关的浅根,update 会把 prev→sha 误判成分叉。
# prev→sha 的 first-parent 距离为 23,depth=200 仍有充足余量,且不拉全史。
# 三个 pin 的 pins 布局不一定相同,物化与断言以各 pin 自身的 gitlink 为准。
git -C rdm fetch --depth=200 origin "${pins[@]}"
for rev in "${pins[@]}"; do
git -C rdm checkout --detach "$rev"
git -C rdm submodule update --init --recursive
done
echo "RDM commit: $(git -C rdm rev-parse HEAD)"
# 构建循环每个 pin 都要重新物化自己的子模块,凭据环境保留到循环结束;
# make 经 env -u 剥离凭据变量运行,凭据只给 git 子模块更新使用。
without_creds() {
env -u GIT_CONFIG_COUNT -u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \
-u GIT_CONFIG_KEY_1 -u GIT_CONFIG_VALUE_1 -u GIT_CONFIG_KEY_2 \
-u GIT_CONFIG_VALUE_2 -u GIT_CONFIG_KEY_3 -u GIT_CONFIG_VALUE_3 \
-u SUBMODULE_TOKEN "$@"
}
: > rdm/pin-build-digests.txt
for rev in "${pins[@]}"; do
git -C rdm checkout --detach "$rev"
git -C rdm submodule update --init --recursive
# fail-loud:pins/* 本地 replace 目标缺失时 Go 会静默退回远端解析并报
# missing go.sum entry,因此构建前逐个核对子模块是否落地。pins 集合混有
# Go 仓与非 Go 仓(如前端仓根无 go.mod),不能以有无 go.mod 判定;
# 真正要保证的是:HEAD 等于该 pin 的 gitlink,且工作树有已跟踪文件。
links="$(git -C rdm ls-tree "$rev" pins/)"
while read -r mode _type commit path; do
[ "$mode" = "160000" ] || continue
head="$(git -C rdm -C "$path" rev-parse HEAD 2>/dev/null || true)"
if [ "$head" != "$commit" ]; then
echo "rdm pin $rev 的 $path 未落地:HEAD ${head:-<none>} 与 gitlink $commit 不符" >&2
exit 1
fi
tracked="$(git -C rdm -C "$path" ls-files 2>/dev/null || true)"
if [ -z "$tracked" ]; then
echo "rdm pin $rev 的 $path 未落地:工作树无已跟踪文件" >&2
exit 1
fi
done <<< "$links"
without_creds make -C rdm build
sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt
done
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \
GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
unset SUBMODULE_TOKEN
- name: Install Node 22
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Build the real agent-system binary
run: make build
- name: Real-combination lifecycle gate (make test-lifecycle, required)
env:
AGENT_PG_TEST_DSN: postgres://ci_agent:ci_job_local_only@127.0.0.1:5432/agent_test?sslmode=disable
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 路径变量在脚本内解析(YAML env: 不做 shell 展开)。
# 不设 SVC_RDM_ORIGIN:全轨 git 部署由 svc-realcombo 作业覆盖。
# pins 物化没有 SSH known_hosts。token 只经 SVC_CI_GIT_TOKEN 进
# ci_git_env 的 URL 级头,不写文件。
export SVC_TEST_REAL=1 SVC_TEST_PG=1
export SVC_REAL_BIN="$PWD/bin/agent-system"
export SVC_REAL_RDM_BIN="$PWD/rdm/bin/agent-system-rdm"
export SVC_RDM_SRC_DIR="$PWD/rdm"
export SVC_CI_GIT_SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
export SVC_CI_GIT_TOKEN="$SUBMODULE_TOKEN"
unset SUBMODULE_TOKEN
make test-lifecycle
# U3(统一部署方案):真实 Dev/RDM 制品的 svc.sh 生命周期门禁——桩套件
# (test_svc*.py)只证明脚本逻辑;本作业证明「声明≠能力」的最后一道:真实
# identity 领养/verify、release 切换、伴生工作台、RDM 全轨部署。缺件即失败,
# 不静默跳过(SVC_TEST_REAL=1 硬开关)。
svc-realcombo:
# 分级门禁:base 非 main 的 PR(L2 child → L1 initiative)只跑轻量 CI(test 作业);
# 进入 main 的 PR 与 main 推送跑全量门禁。
if: ${{ github.event_name != 'pull_request' || github.base_ref == 'main' }}
runs-on: ubuntu-latest
env:
GOPROXY: https://goproxy.cn,direct
steps:
- name: Checkout
env:
TOKEN: ${{ secrets.GITHUB_TOKEN }}
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# Credential handling stays inline: no file from the candidate is executed.
# Workflow provenance and runner isolation still require platform controls.
python3 - <<'PY_CHECKOUT'
import os
from pathlib import Path
import re
import subprocess
import sys
from urllib.parse import urlsplit
# 服务端基址取运行器注入的 GITHUB_SERVER_URL(平台控制、候选 PR 不可影响)。
# 2026-10-05 修复(run 948/949 实测):注入值为内网直连形态
# 'http://172.17.0.1:3000'(docker 网关 → 宿主 Gitea),与公网拼写不同且为
# http——此前硬编码常量严格比对会拒绝合法检出。校验收宽为「http(s) + 非空
# netloc」;凭据仍只附着于「该基址 + 下方仓库白名单」构造的 URL。
SERVER = os.environ.get("GITHUB_SERVER_URL", "").rstrip("/")
parsed_server = urlsplit(SERVER)
if parsed_server.scheme not in {"http", "https"} or not parsed_server.netloc:
sys.exit("Checkout server must be the platform-injected http(s) URL")
SSH = "ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
# Reviewed allowlist, independent of candidate .gitmodules. Add repositories
# here only with an explicit review of the checkout credential's scope.
REPOSITORIES = {
"modules/core": "Agent-System-Core",
"modules/backend": "Agent-System-Backend",
"modules/frontend": "Agent-System-Frontend",
"modules/ui": "Agent-System-UI",
"modules/documents": "Agent-System-Documents",
"deps/agent-system-core": "Agent-System-Core",
"deps/agent-document-renderer": "Agent-Document-Renderer",
"deps/development-guidelines": "Development-Guidelines",
"deps/agent-native": "Agent-Native",
"deps/agent-squad": "Agent-Squad",
"deps/agent-intent": "Agent-Intent",
"deps/agent-command": "Agent-Command",
"deps/error-code-system": "Error-Code-System",
"deps/logging-system": "Logging-System",
"deps/development-manager": "Development-Manager",
"development/guidelines": "Development-Guidelines",
}
root_token = os.environ.pop("TOKEN", "")
component_token = os.environ.pop("SUBMODULE_TOKEN", "")
if not root_token or not component_token:
sys.exit("Checkout requires TOKEN and SUBMODULE_TOKEN")
if os.environ.get("GITHUB_REPOSITORY") != "weichen/Agent-System-Dev":
sys.exit("Checkout repository is outside the reviewed allowlist")
sha = os.environ.get("GITHUB_SHA", "")
if not re.fullmatch(r"[0-9a-fA-F]{40}", sha):
sys.exit("Checkout requires a fixed commit SHA")
# Discard inherited Git tracing/config/SSH hooks. Auth lives only in one
# Git process tree, in an exact URL-scoped header, never in a URL or file.
clean_env = {k: v for k, v in os.environ.items() if not k.startswith("GIT_")}
clean_env.update(GIT_CONFIG_NOSYSTEM="1", GIT_CONFIG_SYSTEM="/dev/null",
GIT_CONFIG_GLOBAL="/dev/null", GIT_TERMINAL_PROMPT="0")
config = {
"core.hooksPath": "/dev/null", "core.fsmonitor": "false",
"init.templateDir": "", "credential.helper": "",
"fetch.recurseSubmodules": "false", "submodule.recurse": "false",
"http.followRedirects": "false", "protocol.allow": "never",
"protocol." + urlsplit(SERVER).scheme + ".allow": "always",
}
def git(repo, *args, extra=None):
values = config | (extra or {})
env = clean_env | {"GIT_CONFIG_COUNT": str(len(values))}
for index, (key, value) in enumerate(values.items()):
env[f"GIT_CONFIG_KEY_{index}"] = key
env[f"GIT_CONFIG_VALUE_{index}"] = value
return subprocess.run(["git", "-C", str(repo), *args], env=env,
check=True, stdout=subprocess.PIPE).stdout
def materialize(repo, depth=0):
if depth > 16:
raise ValueError("Submodule depth exceeds the reviewed checkout limit")
links = {}
for entry in git(repo, "ls-tree", "-rz", "HEAD").split(b"\0"):
if not entry:
continue
metadata, path = entry.split(b"\t", 1)
mode, kind, commit = metadata.decode().split()
if mode == "160000":
links[path.decode()] = commit
if not links:
return
raw = git(repo, "config", "--no-includes", "--blob", "HEAD:.gitmodules", "--null", "--list")
entries = {}
for item in raw.split(b"\0"):
if not item:
continue
key, value = item.decode().split("\n", 1)
match = re.fullmatch(r"submodule\.(.+)\.(path|url)", key)
if not match:
raise ValueError("Unsupported candidate .gitmodules setting")
name, field = match.groups()
if field in entries.setdefault(name, {}):
raise ValueError("Duplicate candidate .gitmodules setting")
entries[name][field] = value
if set(entries) != set(links):
raise ValueError("Submodule names must match the fixed gitlink paths")
# Validate the entire level before any authenticated component fetch.
for name, fields in entries.items():
repository = REPOSITORIES.get(name)
if not repository or fields.get("path") != name:
raise ValueError("Submodule path is outside the reviewed allowlist")
trusted_url = f"{SERVER}/weichen/{repository}.git"
if fields.get("url") not in {SSH + repository + ".git", trusted_url}:
raise ValueError("Submodule URL is outside the reviewed allowlist")
for name in entries:
url = f"{SERVER}/weichen/{REPOSITORIES[name]}.git"
# Persist only the reviewed public URL so Git recognizes the
# initialized submodule after process-scoped config is gone.
git(repo, "config", f"submodule.{name}.url", url)
git(repo, "submodule", "update", "--init", "--checkout", "--", name,
extra={f"submodule.{name}.url": url,
f"submodule.{name}.update": "checkout",
f"http.{url}.extraheader": "AUTHORIZATION: token " + component_token})
child = repo / name
if git(child, "rev-parse", "HEAD").decode().strip() != links[name]:
raise ValueError("Materialized submodule does not match the fixed SHA")
materialize(child, depth + 1)
try:
root = Path.cwd()
# An ephemeral clean checkout is required; do not reuse prior Git state.
if (root / ".git").exists():
raise ValueError("Checkout requires a fresh workspace")
git(root, "init", "-q")
url = SERVER + "/weichen/Agent-System-Dev.git"
git(root, "remote", "add", "origin", url)
git(root, "fetch", "--depth=1", "origin", sha,
extra={f"http.{url}.extraheader": "AUTHORIZATION: token " + root_token})
git(root, "checkout", "-q", "--detach", "FETCH_HEAD")
materialize(root)
except (ValueError, OSError, subprocess.CalledProcessError) as error:
sys.exit(f"Checkout refused: {error}")
print("Checkout complete; credentials were not persisted in Git configuration")
PY_CHECKOUT
unset TOKEN SUBMODULE_TOKEN
- name: Install Go 1.27.1
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/go.tgz" https://mirrors.aliyun.com/golang/go1.27.1.linux-amd64.tar.gz
tar -C /usr/local -xzf "$RUNNER_TEMP/go.tgz"
ln -sfn /usr/local/go/bin/go /usr/local/bin/go
ln -sfn /usr/local/go/bin/gofmt /usr/local/bin/gofmt
- name: Install Node 22
run: |
set -euo pipefail
curl -fsSL -o "$RUNNER_TEMP/node.tar.xz" https://npmmirror.com/mirrors/node/v22.23.3/node-v22.23.3-linux-x64.tar.xz
tar -C /usr/local --strip-components=1 -xJf "$RUNNER_TEMP/node.tar.xz"
- name: Build the real agent-system binary
run: make build
- name: Clone Agent-System-RDM with nested pins (reviewed allowlist addition)
env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 凭据纪律与 Checkout 块同源:经 GIT_CONFIG_* 环境注入 URL 级 token 头,
# 不落文件/命令行;allowlist 扩项 = Agent-System-RDM 及其 pins(同一
# 组件集合),由本 PR 评审背书。SSH pins URL 以 insteadOf 改写到受信
# https 基址后再取。
SERVER="$(printf '%s' "$GITHUB_SERVER_URL" | sed 's:/*$::')"
scheme="${GITHUB_SERVER_URL%%://*}"
export GIT_CONFIG_COUNT=4
export GIT_CONFIG_KEY_0="protocol.$scheme.allow"
export GIT_CONFIG_VALUE_0="always"
export GIT_CONFIG_KEY_1="http.$SERVER/.extraheader"
export GIT_CONFIG_VALUE_1="AUTHORIZATION: token $SUBMODULE_TOKEN"
export GIT_CONFIG_KEY_2="url.$SERVER/weichen/.insteadOf"
export GIT_CONFIG_VALUE_2="ssh://git@git-ssh.u1680832.nyat.app:31411/weichen/"
export GIT_CONFIG_KEY_3="core.hooksPath"
export GIT_CONFIG_VALUE_3="/dev/null"
git clone --depth=1 "$SERVER/weichen/Agent-System-RDM.git" rdm
# 凭据环境须保持到 pins 子模块取完(insteadOf + token 头覆盖嵌套 SSH URL),
# 之后再 unset。
git -C rdm submodule update --init --recursive
echo "RDM commit: $(git -C rdm rev-parse HEAD)"
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \
GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
unset SUBMODULE_TOKEN
make -C rdm build
- name: Run real-combination lifecycle gates (SQLite, U3, required)
env:
SUBMODULE_TOKEN: ${{ secrets.SUBMODULE_TOKEN }}
run: |
set -euo pipefail
# 路径变量在脚本内解析(YAML env: 不做 shell 展开)。
export SVC_TEST_REAL=1
export SVC_REAL_BIN="$PWD/bin/agent-system"
export SVC_REAL_RDM_BIN="$PWD/rdm/bin/agent-system-rdm"
export SVC_RDM_ORIGIN="$PWD/rdm"
# CI 无 SSH key:把受信基址与 URL 级 token 头经环境交给套件(仅环境传递)。
export SVC_CI_GIT_SERVER="${GITHUB_SERVER_URL%/}"
export SVC_CI_GIT_TOKEN="${SUBMODULE_TOKEN}"
python3 -m unittest tests.test_svc_realcombo -v