Files
knowledge-governance/.gitea/workflows/build.yml
T
hermes c99332d770
build / build (push) Successful in 49s
feat(ci): 通知链接改用非秘密配置 CI_PUBLIC_BASE_URL(run/提交/变更统一),缺失或非法明确回退
为什么要改:通知里的链接一直用 GITHUB_SERVER_URL 拼(内网地址),群里/换网就点不开。

做法(不覆盖 GITHUB_SERVER_URL、不碰检出与内部调用):
- ci_report.py 新增 _link_base():CI_PUBLIC_BASE_URL 优先,缺/非法(非 http(s)、带 userinfo、带空白)⇒ 明确回退 GITHUB_SERVER_URL,并把来源写进 payload 的 link_base_source(可复盘)。
- 三类链接统一用它:run_url、commit_url(新增,提交行用短 SHA 展示、完整 SHA 在链接里)、compare_url;仓库必须是真 owner/repo、SHA 必须是完整 40 位,否则宁可不给链接。
- workflow 上报步骤加非秘密 CI_PUBLIC_BASE_URL: https://gitea.zheromiku.top(写在 YAML 里便于审查)。
- **不做消息文本的全局 URL 替换**:提交标题/摘要里的 URL 仍是文本(bridge 侧负例钉住)。
- 自检补 _link_base/_commit_url 分支(缺失回退、非法回退、尾斜杠、短 SHA 不拼链接)。
2026-10-13 10:27:43 +08:00

199 lines
11 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: build
# 提交 → 本机 Runner 跑**唯一权威门禁**(make verify)→ 全部必需检查通过才发预览 → 通知
#
# ⚠️ 本 workflow **只做 CI,不做 CD**:这里没有 deploy:test / deploy-production,
# 也不启动 compose。部署是各项目自己的事(正本 §8:通用 CI 默认不生成部署作业)。
#
# ⚠️ 凭据处理(三条硬规矩):
# 1. **不把令牌写进 remote URL**:URL 里的凭据会落进 .git/config,还会出现在任何打印 remote
# 的地方;这里用 credential helper(只在那一次 git 调用里生效)。
# 2. **不把密钥放在 workflow 级 env**:那等于把 bridge 令牌发给每一个步骤(包括跑待测代码的
# 步骤)。谁需要谁在自己的步骤上声明 —— 见「静态预览」「上报 ci-bridge」。
# 3. **不用写权限的令牌**:检出只用 GITHUB_TOKEN(只读),不请求任何写操作。
# `permissions:` 在 Gitea 1.26.2 上按实例支持情况而定(不支持时会被忽略)。
#
# 实测结论(勿随意改动):
# 1. 不用任何第三方 action(本 runner 的 github_mirror 为空,取 action 会随机红)
# 2. 必须自己 clone —— Runner 不会自动检出代码,工作区初始为空(trap-6)
# 3. act 以 `bash -e -o pipefail` 执行每步 → 禁止 `cmd | head` 这种会 SIGPIPE 提前退出的管道
# 4. 工具链来自主机只读挂载 /opt/citools(go/uv),缓存写 /opt/citools-cache
# 5. bridge 从容器内走 docker 网关 172.17.0.1:8899(实测可达)
# 6. 校验逻辑放仓内脚本(scripts/ci_checks.py + .gitea/scripts/*.py),YAML 只负责调用
# 7. 检出固定到**事件完整 SHA**,不是"当时那个分支 HEAD"(分支随时会前进)
on:
push:
branches:
- "**"
workflow_dispatch:
env:
LANG: C.UTF-8
LC_ALL: C.UTF-8
permissions:
contents: read
# 同一 ref 的新推送取消旧的运行。本 workflow 不含部署,取消它不会动任何线上东西;
# 生产部署是另一个 workflow,那种必须 cancel-in-progress: false —— 取消不是回滚。
concurrency:
group: build-${{ github.ref }}
cancel-in-progress: true
jobs:
build:
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
- name: 检出代码(最小 bootstrap:不依赖仓内文件,固定事件完整 SHA)
env:
GIT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -uo pipefail
# ⚠️ 工作区初始是空的(本 Runner 不自动检出,trap-6)⇒ 这一步**只能用 runner 给的环境变量**,
# 绝不能调用仓内脚本:此时 .gitea/scripts/ 还不存在,实测 rc=127(第三轮审查的启动阻断)。
# 检出完成、仓内脚本就位之后,才交给 ci_checkout.sh --post-checkout 做再断言与补取。
SRV="${GITHUB_SERVER_URL:-http://192.168.50.223:3000}"
case "$SRV" in
https://*|http://*) ;;
*) echo "❌ GITHUB_SERVER_URL 的协议不认识(只接受 http/https):${SRV:0:40}"; exit 1 ;;
esac
case "$SRV" in *[[:space:]]*) echo "❌ GITHUB_SERVER_URL 里有空白字符"; exit 1 ;; esac
REMOTE="${SRV%/}/${GITHUB_REPOSITORY}.git"
case "$REMOTE" in
https://*) SCHEME="https" ;;
http://*) SCHEME="http" ;;
*) echo "❌ remote 的协议不对:${REMOTE:0:30}"; exit 1 ;;
esac
WANT="${SRV%%://*}"
[ "$SCHEME" = "$WANT" ] || { echo "❌ 协议被降级:$SCHEME(服务端给的是 $WANT)"; exit 1; }
case "$REMOTE" in *@*) echo "❌ remote URL 里带上了凭据"; exit 1 ;; esac
[ -n "${GITHUB_SHA:-}" ] || { echo "❌ 没有 GITHUB_SHA:检出必须固定到事件的完整 SHA"; exit 1; }
[ -n "${GIT_TOKEN:-}" ] || { echo "❌ 没拿到读仓库用的令牌(secrets.GITHUB_TOKEN 为空)"; exit 1; }
BR="${GITHUB_REF_NAME:-${GITHUB_REF:-}}"; BR="${BR#refs/heads/}"
cd "${GITHUB_WORKSPACE:-$(pwd)}"
rm -rf ./* .git 2>/dev/null || true
git init -q .
git remote add origin "$REMOTE"
ga() { git -c credential.helper='!f() { printf "username=x-access-token\npassword=%s\n" "$GIT_TOKEN"; }; f' "$@"; }
if [ -n "$BR" ]; then
ga fetch -q --depth 50 origin "refs/heads/$BR" || { echo "❌ 取不到分支 $BR"; exit 1; }
else
ga fetch -q --depth 50 origin "$GITHUB_SHA" || { echo "❌ 取不到事件 SHA $GITHUB_SHA"; exit 1; }
fi
git checkout -q --detach FETCH_HEAD
if [ "$(git rev-parse HEAD)" != "$GITHUB_SHA" ]; then
echo "⚠️ 分支 $BR 的 HEAD != 事件 SHA(有人在之后又推了一次)—— 在已取到的历史里找事件 SHA"
# ⚠️ 不要 `fetch origin $SHA`:默认 Gitea/upload-pack **不允许请求未公开的对象**
# (实测 "服务器不允许请求未公开的对象")。做法是取深一点再核。
if ! git cat-file -e "${GITHUB_SHA}^{commit}" 2>/dev/null; then
ga fetch -q --deepen 200 origin "refs/heads/$BR" 2>/dev/null \
|| ga fetch -q --depth 200 origin "refs/heads/$BR" 2>/dev/null || true
fi
if git cat-file -e "${GITHUB_SHA}^{commit}" 2>/dev/null; then
git checkout -q --detach "$GITHUB_SHA"
else
echo "❌ 在已取到的历史里找不到事件 SHA $GITHUB_SHA"
echo " (浅克隆窗口不够:提高 fetch 深度;或等这次推送的 run 重跑)"
exit 1
fi
fi
GOT_SHA="$(git rev-parse HEAD)"
[ "$GOT_SHA" = "$GITHUB_SHA" ] || { echo "❌ 检出 $GOT_SHA != 事件 SHA $GITHUB_SHA"; exit 1; }
echo "✅ bootstrap 检出 ${GOT_SHA:0:12}(== 事件完整 SHA,协议 $SCHEME,remote 不含凭据)"
# 检出完成 ⇒ 仓内脚本就位:交给它做协议/身份再断言,并补取 before 与默认分支引用
GITHUB_TOKEN="$GIT_TOKEN" GITHUB_ENV="${GITHUB_ENV:-/dev/null}" \
bash .gitea/scripts/ci_checkout.sh --post-checkout
- name: 环境与工具链(缺什么一次性报全)
run: |
set -uo pipefail
{
echo "PATH=/opt/citools/go/bin:/opt/citools/bin:$PATH"
echo "GOCACHE=/opt/citools-cache/gocache"
echo "GOMODCACHE=/opt/citools-cache/gomod"
echo "GOPATH=/opt/citools-cache/gopath"
echo "UV_CACHE_DIR=/opt/citools-cache/uv"
echo "GOPROXY=https://goproxy.cn,direct"
echo "GOTOOLCHAIN=local"
echo "npm_config_cache=/opt/citools-cache/npm"
echo "CI_GATE_LOG=${GITHUB_WORKSPACE}/ci-gate.log"
echo "CI_REPORT=${GITHUB_WORKSPACE}/ci-report.json"
} >> "$GITHUB_ENV"
mkdir -p /opt/citools-cache/gocache /opt/citools-cache/gomod /opt/citools-cache/gopath \
/opt/citools-cache/uv /opt/citools-cache/npm 2>/dev/null || true
export PATH="/opt/citools/go/bin:/opt/citools/bin:$PATH"
miss=""
for t in make python3 node npm go gofmt git curl tar sha256sum; do
command -v "$t" >/dev/null 2>&1 || miss="$miss $t"
done
if [ -n "$miss" ]; then
echo "❌ 作业镜像里缺工具:$miss"
echo " 处理:补镜像/挂载,或把该检查从清单里删掉并写明理由 —— 不允许运行时静默跳过。"
exit 1
fi
echo "工具链: $(go version) | node $(node -v) | npm $(npm -v) | $(python3 -V)"
- name: 断言门禁入口存在(缺了就是红,不许静默跳过)
run: |
set -uo pipefail
if [ ! -f scripts/ci_checks.py ]; then
echo "❌ 找不到权威门禁入口 scripts/ci_checks.py"
echo " 门禁入口是本仓唯一的检查实现;缺了就不能跑 —— 不接受'没有入口所以跳过'。"
exit 1
fi
make -n verify | head -3
echo "✅ 门禁入口就位:make verify"
- name: 权威门禁(make verify = scripts/ci_checks.py)
run: |
set +e; set +o pipefail
make verify 2>&1 | tee "${GITHUB_WORKSPACE}/ci-gate.log"
rc=${PIPESTATUS[0]} # pipefail 已关,用 PIPESTATUS 取真退出码
echo "门禁退出码=$rc"
exit "$rc"
- name: 静态预览(**先核对完整报告**;核对不过一个 publish 请求都不发)
if: always()
env:
BRIDGE_URL: ${{ vars.CI_BRIDGE_URL }}
BRIDGE_TOKEN: ${{ secrets.CI_BRIDGE_TOKEN }}
CI_REPORT: ${{ github.workspace }}/ci-report.json
run: |
set +e; set +o pipefail
: > "${GITHUB_WORKSPACE}/.ci-preview-url"
# 顺序钉死在这里:ci_preview.py 内部**先**调 ci_report.verify()(与上报步骤
# 同一份核对逻辑:清单齐不齐 / 重复 / 未知项 / 完整 SHA / 档案 / 运行模式),
# 有任何问题就一个 publish 请求都不发。
# 旧写法只读 overall_status + SHA,缺项报告会**先发出去**、等到通知步骤才发现 ——
# 预览发出去收不回来(URL 会被打开、会被转群),那时再红也撤不回来。
python3 .gitea/scripts/ci_preview.py
rc=$?
[ "$rc" = "0" ] || echo "❌ 预览步骤非 0($rc):报告核对不过或读不到报告"
exit "$rc"
- name: 上报 ci-bridge(核对报告 + 通知失败不改变质量结果)
if: always()
env:
BRIDGE_URL: ${{ vars.CI_BRIDGE_URL }}
BRIDGE_TOKEN: ${{ secrets.CI_BRIDGE_TOKEN }}
# 通知里的链接基址(**非秘密**,直接写在 workflow 里便于审查与复盘):
# CI 运行 / 提交 / 变更 三类链接统一用它拼;提交与变更里的仓库、SHA、run_id 仍是真实值。
# **不覆盖 GITHUB_SERVER_URL**(检出与所有内部调用继续用它,检出地址一个字节没变)。
# 删掉这一行 ⇒ ci_report.py 明确回退到 GITHUB_SERVER_URL,并在上报里写 link_base_source。
CI_PUBLIC_BASE_URL: https://gitea.zheromiku.top
run: |
set +e; set +o pipefail
B="${BRIDGE_URL:-}"
for cand in "$B" "http://172.17.0.1:8899" "http://192.168.50.134:8899" "http://100.92.49.78:8899"; do
if [ -n "$cand" ]; then curl -s -m 5 -o /dev/null "$cand/healthz" && { B="$cand"; break; }; fi
done
echo "使用 ci-bridge: ${B:-(不可达)}"
# 这一步**先独立核对报告**(清单齐不齐 / 完整 SHA / 档案 / 自称是否与重算一致),
# 再上报;核对不过 ⇒ 非 0(质量无从判定,绝不能报绿)。
CI_BRIDGE_RESOLVED="$B" python3 .gitea/scripts/ci_report.py
rc=$?
if [ "$rc" -ne 0 ]; then
echo "❌ 上报脚本非 0 退出:报告核对不过或读不到 —— 这次运行的质量无从判定"
fi
exit "$rc"