修正隔离CI的代理与提权流程
Project CI / Frontend tests (push) Successful in 3m2s
Project CI / Repository checks (push) Successful in 3m21s
Project CI / Backend tests (push) Failing after 3m36s
Project CI / Native shell tests (push) Failing after 11m16s

保留root任务的代理环境并为非root sudo透传受控代理
记录Gitea 1.26.4与rootless runner隔离部署
沉淀Docker socket和Compose路径排障经验
This commit is contained in:
2026-07-21 21:28:14 +08:00
parent 1f29ea6d8c
commit 79b9404983
4 changed files with 51 additions and 21 deletions
+36 -20
View File
@@ -37,12 +37,16 @@ jobs:
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
sudo_command=()
if [[ "$(id -u)" -ne 0 ]]; then
command -v sudo >/dev/null 2>&1 || {
echo 'non-root runner user requires sudo for system dependencies.' >&2
exit 1
}
sudo_command=(sudo -E)
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
"${sudo_command[@]}" apt-get update
"${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates \
curl
@@ -154,12 +158,16 @@ jobs:
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
sudo_command=()
if [[ "$(id -u)" -ne 0 ]]; then
command -v sudo >/dev/null 2>&1 || {
echo 'non-root runner user requires sudo for system dependencies.' >&2
exit 1
}
sudo_command=(sudo -E)
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
"${sudo_command[@]}" apt-get update
"${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
@@ -251,12 +259,16 @@ jobs:
echo 'ubuntu-latest runner must provide an Ubuntu or Debian environment.' >&2
exit 1
}
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
sudo_command=()
if [[ "$(id -u)" -ne 0 ]]; then
command -v sudo >/dev/null 2>&1 || {
echo 'non-root runner user requires sudo for system dependencies.' >&2
exit 1
}
sudo_command=(sudo -E)
fi
${sudo_command} apt-get update
${sudo_command} env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
"${sudo_command[@]}" apt-get update
"${sudo_command[@]}" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
build-essential \
ca-certificates \
clang \
@@ -286,12 +298,16 @@ jobs:
npm_path="$(command -v npm)"
test -x "${node_path}"
test -x "${npm_path}"
sudo_command=''
if command -v sudo >/dev/null 2>&1; then
sudo_command='sudo'
sudo_command=()
if [[ "$(id -u)" -ne 0 ]]; then
command -v sudo >/dev/null 2>&1 || {
echo 'non-root runner user requires sudo to expose trusted Node.js paths.' >&2
exit 1
}
sudo_command=(sudo -E)
fi
${sudo_command} ln -sfn "${node_path}" /usr/local/bin/node
${sudo_command} ln -sfn "${npm_path}" /usr/local/bin/npm
"${sudo_command[@]}" ln -sfn "${node_path}" /usr/local/bin/node
"${sudo_command[@]}" ln -sfn "${npm_path}" /usr/local/bin/npm
/usr/local/bin/node --version
/usr/local/bin/npm --version
@@ -458,6 +458,7 @@ npm run check:server-rs-ddd
- checkout 必须使用完整历史。PR 将 base SHA 写入 `SPACETIME_SCHEMA_BASE_REF`,直接推送 `master` 使用 before SHA;事件基线不可解析时直接失败。Gitea 检查的是 PR head 而非预合并 commitworkflow 必须拒绝不包含最新 base commit 的过期 PR,分支保护同时保持“PR 过期禁止合并”。
- 普通 PR job 不读取业务 secret,不运行真实 API/SpacetimeDB/OSS/支付/生成/live smoke,也不执行会修改外部状态的维护、迁移、发布或备份命令。
- Gitea 至少升级到 `1.26.4` 后才能注册执行 PR job 的 runner`ubuntu-latest` 标签只映射到固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像,不使用浮动镜像 tag,不映射 host,不向 job 暴露 Docker socket、业务 secret 或不必要内网。runner 能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm、Rust 分发和 crates.ioworkflow 的官方 action 固定完整 commit,若内网禁用 GitHub,先在当前 Gitea 镜像对应 commit 并改用绝对 URL。受控镜像优先预装 rustup。Gitea 1.26 的任务超时由 runner 全局配置控制;首次运行成功后,`master` 分支保护必须要求上述四个 job 全部成功。
- `genarrative-station` 当前使用 Gitea `1.26.4` + Runner `2.0.0-dind-rootless`runner/job image 固定 digest,外层非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 只监听 Unix socket`docker_host: "-"` 阻止 socket进入 job。job 只在 `gitea-actions` internal network,通过 `/git` reverse gateway访问 Gitea,通过拒绝私网/保留地址/metadata 的 80/443 proxy访问公共依赖;直连公网和 Gitea 数据网必须失败。内层 bwrap 所需 namespace/proc 选项只能用于该 rootless DinD,不能放宽宿主 rootful runner。系统依赖步骤在 root job 中不调用 sudo,非 root 时用 `sudo -E` 保留受控 proxy。宿主 compose helper 必须把 `/opt/gitea-stack` 挂到同名绝对路径,避免相对 volume错误落到 `/stack` 空目录。
## 后端相关默认验证
@@ -3475,3 +3475,12 @@
- 处理:同一失败 revision 上,尚无协作事实的兼容 run 可以保留总控直接修复;已有协作事实且总控只编排时,只允许创建新的 `code-prototype` 后续修复委派,明确继承最新 `preview.validate` 诊断和 `game/index.html` 产物要求,不把它伪装成已有 repair delivery 的二次返工。专业 Agent 推进 revision 后,总控先验证新 revision,再重新试玩。
- 验证:回归测试同时覆盖“无协作时仍可直接修复”“有协作时工具目录只剩 `agent.delegate`”“专业 Agent 推进新 revision 后总控只能先复验”,并用 `supervisor-autonomous-playable-lane-defense` 真实 E2E 检查静态烟雾、桌面/移动浏览器、全部固定试玩断言、唯一 Supervisor 回复和零残留。
- 关联:`apps/ai-game-creator-shell/src-tauri/src/agent.rs``apps/ai-game-creator-shell/src-tauri/src/tests.rs``apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs`
## Gitea PR runner 不能把宿主 Docker socket 当普通 volume
- 现象:`act_runner``container.docker_host` 留空时,job inspect 会出现 `/var/run/docker.sock:/var/run/docker.sock`PR 脚本即使 `valid_volumes: []`,仍可直接调用 Docker API读取其它容器、挂宿主目录或取得 runner 配置。另一类迁移故障是 rootless daemon可以启动,但 bwrap 在 job 内报 `No permissions to create new namespace``Failed to make / slave``Mount too revealing`
- 原因:空 `docker_host` 会自动发现并把控制 socket传播到 jobrootful Docker 的 seccomp、AppArmor 与 system-path masks 又不支持完整 nested bwrap。直接使用 `--privileged`、外层 `CAP_SYS_ADMIN` 或 host executor 会把测试跑绿建立在破坏 PR 隔离的前提上。宿主启用 Clash fake-IP 时,简单按 DNS 的 `198.18.0.0/15` 判断公网还会误拒所有公共依赖。
- 处理:先把 Gitea 升到至少 `1.26.4`,再用固定 digest 的 Runner 2.0.0 rootless DinD;外层保持非 privileged、无 `CAP_SYS_ADMIN`,内部 Docker 仅 Unix socketrunner 固定 `docker_host: "-"`。job 使用独立 internal networkGitea 经 reverse gateway,公共 80/443 经使用公共 DoH 验证真实 IP、拒绝私网/保留地址/metadata 的 proxy;DoH 要缓存并合并同域并发,长下载 timeout 不能只有 60 秒。rootless job 内的 bwrap namespace/proc 选项不能复制到宿主 rootful runner。apt 步骤在 root 时直接执行,非 root 时用 `sudo -E`,否则 sudo `env_reset` 会让 apt 丢失 proxy。
- 运维陷阱:从容器内运行 Compose 时,宿主 `/opt/gitea-stack` 必须挂到容器同名绝对路径;挂成 `/stack` 会让相对 bind source 被 daemon解析为宿主 `/stack/...`,表现为 Gitea进入空安装页、gateway 脚本“缺失”。发现后不要迁移空库,立即用同路径 mount 重建并核对原数据大小、installed 日志、仓库数和 API。切换前保留冷数据 tar、pg_dumpall 和原 compose/env/runner 配置,备份与 token 不提交 Git。
- 验证:同时检查 Gitea 版本、runner declare、外层 `Privileged=false`/无 CapAdd/无宿主 socket、inner job `Binds=[]`、固定 image digest、`/var/run/docker.sock` 不存在、公共 proxy可用、直连公网/Postgres/metadata 失败,以及完整 bwrap canary。最后重跑四个 CI jobcheckout成功但 apt/rustup/npm 同时失败时,先排 proxy/env,而不是改测试。
- 关联:`.gitea/workflows/project-ci.yml``docs/【开发运维】本地开发验证与生产运维-2026-05-15.md``docs/project-memory/shared-memory/development-workflow.md`
@@ -211,7 +211,11 @@ npm run check
PR checkout 必须保留完整 Git 历史,并把 PR base SHA 传给 `SPACETIME_SCHEMA_BASE_REF``check:spacetime-schema` 依赖该基线识别已有表字段删除、改名、重排和改类型;事件给出的基线缺失或本地不可解析时必须直接失败,不能退化为空差异检查。Gitea 的 PR checkout 是 PR head,不是与目标分支的预合并 commit,因此 workflow 还会验证 PR head 包含事件中的最新 base commit;分支保护必须继续开启“PR 过期禁止合并”,过期分支先更新再重跑。向 `master` 直接推送时使用 push before SHA,手工触发时回退到 `origin/master`
启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustupfallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。Gitea 1.26 不执行 workflow 的 `timeout-minutes`任务最长运行时间在 runner 全局配置收口,不能只在 YAML 写一个不会生效的超时值
启用或注册执行 PR job 的 runner 前,Gitea 服务端必须至少升级到 `1.26.4`;不得在 `1.26.2` 上执行不受信任 PR 代码。runner 必须提供 `ubuntu-latest` 标签,并将其映射到经验证且固定 digest 的 Ubuntu 24.04 级 Docker/临时隔离镜像;禁止使用浮动镜像 tag,禁止将该标签映射到 host 执行器,禁止向 job 暴露 Docker socket、业务环境变量、业务密钥或不必要的内网。workflow 会安装 Node 22、仓库 `rust-toolchain.toml` 固定的 Rust 1.96.0,以及 clang/lld 和 Tauri Linux 依赖;受控 runner 镜像应预装 rustupfallback 下载只用于首次引导。runner 仍需能访问 Gitea、GitHub Actions 与 `actions/node-versions`、nodejs.org、npm registry、Rust 分发和 crates.io。workflow 中的 `actions/checkout` / `actions/setup-node` 固定到完整 commit;内网 runner 不允许访问 GitHub 时,先把对应 commit 镜像到当前 Gitea 并把 workflow 改为绝对 action URL。首版不使用 Actions cache,避免未配置 runner cache 网络时把缓存恢复错误变成 PR 失败。当前 Runner 2.0.0 已支持 job 级 `timeout-minutes` runner 全局 `3h` 仍是所有任务的硬上限;若 workflow 以后新增更短 timeout,不能删除全局兜底
当前 `genarrative-station` 已于 2026-07-21 升级到 Gitea `1.26.4`,并使用固定 digest 的 Gitea Runner `2.0.0-dind-rootless` 与固定 digest 的 Ubuntu 24.04 job image。外层 runner 以 `rootless` 用户运行,`privileged=false`、不增加 `CAP_SYS_ADMIN`,只映射 `/dev/net/tun`,内部 Docker 只监听私有 Unix socketrunner 配置必须保持 `docker_host: "-"``valid_volumes: []``bind_workdir: false`,防止内部 Docker socket或宿主 bind mount进入 job。job 只连接 `gitea-actions` internal network`genarrative-station` 由只转发 `/git` 到 Gitea 的内部 gateway解析,公网依赖只经拒绝私网、保留地址和 metadata 的 80/443 egress proxy;绕过 proxy 的公网和 Postgres/Redis 数据网都必须不可达。完整 bwrap canary 需要 rootless DinD 外层的 rootlesskit AppArmor/userns 边界,以及内层 job 的 namespace/proc 挂载支持;相关 `seccomp/systempaths` 放宽只允许存在于这个无宿主 socket的 rootless DinD 内层,禁止复制回控制宿主 rootful Docker 的 runner。workflow 的系统依赖步骤在 UID 0 时不再调用 sudo;非 root runner 必须使用 `sudo -E` 保留受控 proxy 环境,避免 `sudo``env_reset` 让 apt 绕过 gateway。
站点 stack 仍由宿主 `/opt/gitea-stack` 管理,`.env`、runner 注册文件和数据库凭据不进入仓库。Compose 必须在 helper/container 内把该目录挂到同一个绝对路径再执行;若挂成 `/stack`,相对 bind source 会被 Docker daemon 误解析为宿主 `/stack/...` 并启动空数据目录。升级或 runner 迁移前先停止 Gitea 写入并保留 `data/gitea` 冷快照、`pg_dumpall`、compose/.env 与 runner config/.runner;本次可回滚快照位于 `backups/gitea-ci-migration-20260721-211018`。备份文件、绝对宿主配置和注册 token 不得提交 Git。
workflow 首次成功运行后,在 Gitea `master` 分支保护中把 `Project CI / Repository checks (pull_request)``Project CI / Frontend tests (pull_request)``Project CI / Backend tests (pull_request)``Project CI / Native shell tests (pull_request)` 四个完整 context 都设为合并必需检查,并从最近一周已上报 context 表复核名称后再保存。不能只填裸 job 名,否则无法匹配 Gitea 实际上报的 `<workflow> / <job> (<event>)`。只提交 workflow 文件不会自动创建 runner,也不会自动修改分支保护;如果 Actions 长时间停留在等待状态,先到仓库或组织的 Actions runner 页面确认存在在线、带 `ubuntu-latest` 标签的 runner。