Yue.to 服务端组件的统一构建与外部控制仓。本仓库公开(公开仓 GitHub Actions 免费、无分钟上限),不含业务代码;私有代码仓由 PAT checkout,产物只推 ghcr.io/onesyue/*,不留 artifacts。它也承载必须脱离生产主机、且不能被私有 Actions 计费冻结拖死的告警链 dead-man 观察者。
客户端(yuelink)构建在 yuelink-ci,与本仓并列,即"两个构建仓"架构。
可构建镜像见 services.json:yue-node、yueops-web、checkin-api、yue-bot、
yueboard。只做跨仓源码契约校验、不应构建镜像的产品见
validation-targets.json;目前包含 YueLink。两份清单刻意分离,避免客户端被误送入
Docker 构建矩阵。
本仓只有一个发布工作流:.github/workflows/build.yml。Promotion 不是独立
workflow,而是该工作流在完成校验、构建、签名和证明后的最后一个受控步骤。
.github/workflows/alert-chain-deadman.yml 是只读运维探针,不构建、不发布:每
30 分钟读取 bastion 上的 heartbeat,并调用 YueOps 仓库中的规范判定器;异常只在
私有 YueOps 仓开事故 issue,公开仓不记录生产凭据内容。
.github/workflows/image-rescan.yml 每天把 services.json 中每个服务的 latest
解析成不可变 digest,并按声明的每个生产平台用 Trivy 0.74.0 重扫。它不 checkout
私有源码、不写 registry、不上传公开 artifact,只负责发现镜像发布后新增的可修复
HIGH/CRITICAL 漏洞。
# YueBoard 未 pin HEAD 只做验证,零 registry 写;精确 pin 才构建 candidate
gh workflow run build.yml -R onesyue/yueto-ci \
-f service=yueboard -f ref=<40-hex-reviewed-contract-pin>
gh workflow run build.yml -R onesyue/yueto-ci -f service=all
# 对远端 YueLink 精确源码提交只跑中央契约校验,不进入镜像构建
gh workflow run build.yml -R onesyue/yueto-ci \
-f service=yuelink -f ref=<40-hex-yuelink-sha>
# 只有显式 promote=true、ref 在默认分支上且本镜像构建输入与 HEAD 相同、不比 :latest 旧
# (P2 #11,scripts/promote-source-gate.py,改标签前再判一次),且 YueBoard ref 精确等于
# native-node-contract.json 的已评审 yueboard_contract_pin,才能提升
gh workflow run build.yml -R onesyue/yueto-ci \
-f service=yueboard -f ref=<40-hex-reviewed-contract-pin> -f promote=true
# 私有源码仓默认分支由 poll-sources.yml 每 20 分钟拉取检查;缺少精确
# built-<40-hex> 产物时只触发 candidate 构建,不自动提升 latest。ref 可以是完整分支或 tag;如果传 commit,必须传完整 40 位 SHA。GitHub
checkout 不把 7‑39 位短 SHA 当作可复现的 commit ref,中央 plan 会提前拒绝。
YueBoard 的未 pin 默认分支 HEAD 仍可用 promote=false 做完整验证,日志会明确
标为 non-promotable,并从 build matrix 剔除,因此不会登录 registry、构建镜像或
写 candidate / built-* / sha-* / latest。只有先通过签名的跨仓 pin 收敛把
yueboard_contract_pin 精确推进到该 40 位 SHA,candidate 构建和 promotion 才可运行。
service=all 同时运行 services.json 的服务校验与
validation-targets.json 的源码校验;后者不会产生 build matrix。仅校验目标不能
使用 promote=true。
poll-sources.yml 从 services.json 派生仓库/镜像组,逐个验证组内所有镜像。
新产物用完整 40 位源码 SHA 作 marker;迁移期仅在旧 7 位 marker 的 OCI
org.opencontainers.image.revision 精确等于当前 HEAD 时才承认已构建。registry
权限或网络错误会 fail closed,不会伪装成“镜像不存在”触发冗余重建。
P3(2026-09-24):缺 marker 的镜像若与已 promote 的 :latest 构建输入完全相同——
先用 gh attestation verify 验过它的 build provenance、recipe(services.json 条目 + build job
文本)未变、promoted revision 是 HEAD 的祖先、Dockerfile 没有浮动 # syntax= frontend——
就不派发它的构建(scripts/poll-skip-decision.py)。任何一项测不到都照常构建;跳过不打
built-* 标签(旧产物保留原来的源码身份),下一轮 poll 会再问一次。
YUETO_CI_PAT— classic PAT,勾repo+write:packages:checkout 私有代码仓 + 推 GHCR。 (已有包如 ghcr.io/onesyue/yueboard 归属各代码仓,本仓 GITHUB_TOKEN 推不动,必须用 PAT。)DEADMAN_SSH_KEY_B64— 专用只读 SSH 私钥的 base64。堡垒机公钥必须以command="/bin/cat /var/lib/yue-alert-heartbeat/heartbeat.json",restrict强制命令; 禁止复用任何 root 部署/轮换私钥。
YUETO_CI_PAT 是 classic、repo 全权限:本公开仓任一 workflow 被改坏,它对全部私仓可写。
工作流已改成「开关优先、PAT 兜底」,业主在控制台做完下面的事、设好变量即切换,不需要改代码:
| 用途 | 位置 | 迁移后凭据 | 最小权限 |
|---|---|---|---|
| 读 yueboard 提交 SHA(plan) | build.yml plan |
App token | yueboard contents:read |
| checkout 源码 + YueBoard 契约 | build.yml validate / build |
App token | yueboard / yue-node / yueops / yuelink contents:read |
| yue-node 私有 fork tag 校验 | build.yml Validate yue-node |
App token | quic-go contents:read |
| promote 前复核默认分支 HEAD | build.yml promote(现签一枚,因为 build job 可跑两小时而 App token 一小时过期) | App token | 三个服务源码仓 contents:read |
| GHCR 推送 / 签名 / provenance | build.yml Login to GHCR |
本仓 GITHUB_TOKEN(packages: write) |
每个 package 授予 yueto-ci Write |
| GHCR 读 | poll-sources / image-rescan | 本仓 GITHUB_TOKEN(packages: read) |
每个 package 授予 yueto-ci Read 以上 |
| 读三个源码仓 HEAD | poll-sources scan |
App token | contents:read |
| 派发本仓 build.yml | poll-sources Trigger builds |
本仓 GITHUB_TOKEN(actions: write;workflow_dispatch 是 GITHUB_TOKEN 允许触发新 run 的例外) |
— |
| deadman 读 YueOps 判定器 | alert-chain-deadman | App token | yueops contents:read |
| deadman 开事故 issue | alert-chain-deadman | 单独签发的 App token | yueops issues:write |
GitHub App 不能认证 GHCR,所以 registry 那一半走本仓 GITHUB_TOKEN,前提是每个 package 在
Package settings → Manage Actions access 里把 onesyue/yueto-ci 加进来。
两个开关(Settings → Secrets and variables → Actions → Variables):
YUETO_CI_APP_CLIENT_ID(+ secretYUETO_CI_APP_PRIVATE_KEY):非空即所有源码读取改用 App token; 签发失败让 job 失败,不会静默回退 PAT。YUETO_CI_GHCR_VIA_GITHUB_TOKEN=true:GHCR 登录改用本仓GITHUB_TOKEN。
两个都开并跑绿一轮(poll / build 候选 / 一次 promote / rescan / deadman 演练)后,删除 secret
YUETO_CI_PAT 并在 GitHub 撤销该 classic PAT。任何一处裸用 PAT 都会被
tests/test_credential_policy.py 拦下。App 的创建步骤见根仓
docs/2026-09-23-ci-credential-migration.md。
私有源码仓不再需要 YUETO_CI_DISPATCH_PAT;拉取式 poll 使用中央仓已有的
YUETO_CI_PAT。repository_dispatch 入口仅保留给受控兼容调用,仍由可信 actor、
完整 SHA 和默认分支 HEAD 三重门禁约束。
2026-09-14 起,「哪个 revision / digest 已 promote」的唯一真源是工作区根仓
onesyue/yueto 的 release.yaml。它由工作站 scripts/ship.sh 在拿到 promote 步骤的
digest、并通过签名/SBOM/provenance 三门验签之后写入、签名提交、推送
(scripts/release-yaml.py verify 再把记录与 GHCR sha-<revision> / latest 比对)。
本仓的 promote 步骤刻意不写任何仓,理由:根仓是私有免费档、无 ruleset 可强制签名,
CI 机器人写不出业主签名的提交;YUETO_CI_PAT 的 repo scope 对全部私仓可写,本仓
迄今一次都没用它写过——第一条「用它推私仓」的步骤会把爆炸半径扩到全部私仓;
yueops group 派发的三个矩阵 job 并行 promote,三处同时提交根仓必然互撞。
tests/test_build_policy.py::test_promotion_records_nothing_in_the_workspace_root_repo
钉住这一条:workflow 里不得出现 git commit/push、根仓 contents 写 API 或 release.yaml,
而 promote 步骤的 DIGEST / SOURCE_SHA / IMAGE env 锚点必须保留(那是 ship.sh 的输入)。
build.yml 的 input-shadow job 与验证并行,没有任何 job needs 它,
continue-on-error: true,权限全是 read。它做两件事,都只写 annotation 和 step summary:
scripts/input-fingerprint.py shadow-ci:对每张镜像,按 Dockerfile 实际的COPY/ADD/bind 源 + 生效的 ignore 文件(<Dockerfile>.dockerignore,否则 上下文根.dockerignore;services/*/.dockerignore这种同目录文件 BuildKit 不读) 算输入指纹,与 GHCR:latest的 revision 比较;再从该 digest 的 GitHub build provenance 取出构建它的本仓 commit,比较 build job 文本(recipe)。输出would-skip/would-build/would-build (unknown)。任何构建都照常执行。 本地同一实现:python3 scripts/input-fingerprint.py compare|history --repo ../yueops ...。scripts/verification-evidence.py probe(仅 promote run):检查是否存在同一本仓 commit、同一 service、同一 40 位源码 SHA、同一 hosted runner 镜像版本、24 小时内 的成功验证。复用是关闭的,且本轮结论是不开:validate 里的 pip-audit / npm audit / pnpm audit 结论随时间变化,身份再一致也不能代表 promote 时刻的结果。
回滚:删掉 input-shadow job(没有消费者,删除零影响)。
构建、source poll、镜像重扫三个真实 Buildx 消费者共用
scripts/install-verified-buildx.sh:Linux x86_64 固定 v0.37.1,下载前限制
HTTPS、执行前核对硬编码 SHA-256,再通过实际 docker buildx version 验证解析路径。
摘要来自 官方发布资产
与同版 checksums.txt 的独立对照。已有正确字节直接复用;旧版、下载失败或摘要不符
不能继续构建。版本/摘要变更必须一起评审,环境变量不能覆盖它们。
setup-buildx-action 默认复用 runner 已装版本是正常行为,不代表自动选择最新版。 安装器先完成验证,原固定 action 才创建固定 OCI BuildKit builder;只做 imagetools 的 poll/重扫仅安装 CLI,不创建多余 builder。这是构建/推送可靠性更新,不要求重发 已验收应用镜像,也不改变生产节点 APT 包清单。
仓库 Settings → Actions 的 selected-actions 必须覆盖工作流直接调用的动作,也必须覆盖
复合动作内部的第三方调用。aquasecurity/trivy-action v0.36.0 会继续调用精确固定的
aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514;只放行顶层
trivy-action 会让镜像任务在 Set up job 阶段失败,扫描根本不会开始。当前闭包为:
anchore/sbom-action@*aquasecurity/setup-trivy@3fb12ec12f41e471780db15c232d5dd185dcb514aquasecurity/trivy-action@*astral-sh/setup-uv@*bufbuild/buf-setup-action@*docker/build-push-action@*docker/login-action@*docker/setup-buildx-action@*docker/setup-qemu-action@*sigstore/cosign-installer@*
同时保持 github_owned_allowed=true、verified_allowed=false;工作流本身仍必须把每个
第三方动作固定到完整 40 位提交,白名单里的 @* 不等于允许可变 tag 进入源码。
构建搬到本仓后,Sigstore keyless 签名的 identity 从 https://github.com/onesyue/<代码仓>/...
变为 https://github.com/onesyue/yueto-ci/...。节点侧部署验签(yueops
scripts/verify-image-signature.sh 的 --certificate-identity-regexp)必须同步更新为:
^https://github.com/onesyue/yueto-ci/
迁移顺序(每个服务):本仓构建成功 → 验签脚本 regexp 更新并部署 → 切换部署 pin 到本仓产出的 tag → 删除代码仓里的旧 docker-publish workflow。
- 日志保持简洁,绝不回显配置/路径细节;敏感值一律走 secrets(Actions 自动打码)。
- 不产出 artifacts(公开仓 artifacts 任何人可下载),产物只进 GHCR。
- Self-hosted runner 仅能通过有写入权限的人员手动
workflow_dispatch并显式选择yue-local-release;代码仓repository_dispatch与默认手动运行 仍使用 GitHub-hosted runner。工作流会自举 GNU make,并在校验、构建和 promotion 前对实际工具链 fail closed。YueNode 的 race 门禁会自举build-essential并显式启用 CGO;镜像签名阶段还要求 Debian 的gettext-base(提供envsubst)。GitHub-hosted runner 使用setup-python; Debian 13 的yue-local-release使用系统 Python 3.13,且在执行任何 Python policy 前验证精确主/次版本。不接受依赖 runner 手工状态的隐式通过。 注册时必须同时保留默认self-hosted、Linux、X64标签并添加唯一自定义 标签yue-local-release;四个标签必须全部匹配,不能只靠可误贴的自定义标签 把非 Linux 或非 x86_64 主机送入发布任务。 - 安全前置:本仓保持 public 时不得注册常驻 self-hosted runner,更不得把堡垒机、
面板、数据库或承载用户流量的业务节点接成 runner。只有先把控制仓改为 private、
完成受保护分支与 Actions 白名单门禁后,才能在无生产凭据和生产网络访问权的
专用 Debian 13 x86_64 一次性虚机上启用
--ephemeral --disableupdaterunner; 每个 runner 只领取一个 job,并在外送诊断日志后销毁整台虚机和 Docker 状态。