728 lines
40 KiB
YAML
728 lines
40 KiB
YAML
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
|