bda26730f5
模板 workflow 第一步改成最小 bootstrap(空工作区里仓内脚本还不存在);ci_checkout.sh 拆成 --post-checkout(检出后校验与补取:默认分支引用、事件 before)+ --print-remote;ci_gate.py 新分支范围以远端默认分支为基准;ci-gate.py 的 Python 依赖必须按 uv.lock 安装(uv sync --frozen)、所有检查走 uv run --frozen(项目内 .venv),并断言 sys.prefix != sys.base_prefix,缺锁/只有 requirements.txt 明确失败(不再 uv pip install --system)。文档补第三轮修正记录、trap 与自检命令。
123 lines
6.4 KiB
Bash
Executable File
123 lines
6.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# 检出后的**仓内校验与补取**(`--post-checkout`);以及只打印 remote 的 `--print-remote`。
|
||
#
|
||
# ⚠️ 为什么本脚本**不能**是 workflow 的第一步:
|
||
# Runner 的工作区初始是空的(trap-6),此时 `.gitea/scripts/ci_checkout.sh` **根本不存在** ——
|
||
# 直接 `bash .gitea/scripts/ci_checkout.sh` 会返回 127(实测,第三轮审查)。所以顺序必须是:
|
||
# ① workflow 里一段**最小 bootstrap**(只依赖 runner 给的环境变量,不依赖仓内任何文件):
|
||
# 校验协议/凭据 → git init + remote add → 取**事件完整 SHA** → checkout → 断言 HEAD == 事件 SHA;
|
||
# ② 检出完成、脚本就位之后,才调用本脚本 `--post-checkout` 做剩下的校验与补取。
|
||
# 本脚本仍然保留协议/凭据/身份的断言 —— 它们要能被本地负例打到(`--print-remote` 等)。
|
||
#
|
||
# 用法:
|
||
# .gitea/scripts/ci_checkout.sh --post-checkout # workflow 检出后调用(读下面的环境变量)
|
||
# .gitea/scripts/ci_checkout.sh --print-remote # 只打印将要使用的 remote(本地测协议,不联网)
|
||
#
|
||
# 环境变量(CI 提供):
|
||
# GITHUB_SERVER_URL / GITHUB_REPOSITORY / GITHUB_REF_NAME / GITHUB_SHA
|
||
# GITHUB_TOKEN(步骤级注入)/ GITHUB_WORKSPACE / GITHUB_EVENT_PATH(取 before 与默认分支)
|
||
set -uo pipefail
|
||
|
||
SRV="${GITHUB_SERVER_URL:-http://192.168.50.223:3000}"
|
||
WS="${GITHUB_WORKSPACE:-$(pwd)}"
|
||
BR="${GITHUB_REF_NAME:-${GITHUB_REF:-}}"
|
||
BR="${BR#refs/heads/}"
|
||
EVENT_SHA="${GITHUB_SHA:-}"
|
||
ZERO="0000000000000000000000000000000000000000"
|
||
MODE="${1:-}"
|
||
|
||
# ── 协议:保留 runner 给的服务端协议,并显式禁止 https → http 的降级 ──────────
|
||
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
|
||
case "$SRV" in https://*) WANT_SCHEME="https" ;; *) WANT_SCHEME="http" ;; esac
|
||
REMOTE="${SRV%/}/${GITHUB_REPOSITORY}.git"
|
||
case "$REMOTE" in https://*) GOT_SCHEME="https" ;; http://*) GOT_SCHEME="http" ;; *) GOT_SCHEME="?" ;; esac
|
||
if [ "$GOT_SCHEME" != "$WANT_SCHEME" ]; then
|
||
echo "❌ remote 的协议被降级了:拼出来是 $GOT_SCHEME,服务端给的是 $WANT_SCHEME"
|
||
exit 1
|
||
fi
|
||
# 凭据一律走 credential helper(URL 内嵌凭据会进 ps / 配置 / 日志)
|
||
case "$REMOTE" in *@*) echo "❌ remote URL 里带上了凭据"; exit 1 ;; esac
|
||
|
||
if [ "$MODE" = "--print-remote" ]; then
|
||
echo "$REMOTE"
|
||
exit 0
|
||
fi
|
||
if [ "$MODE" != "--post-checkout" ]; then
|
||
echo "用法:ci_checkout.sh --post-checkout(检出后校验+补取)|--print-remote(只打印 remote)"
|
||
echo "⚠️ 本脚本**不是**检出本身:空工作区里它还不存在,第一步必须是最小 bootstrap。"
|
||
exit 2
|
||
fi
|
||
|
||
cd "$WS" || exit 1
|
||
[ -n "$EVENT_SHA" ] || { echo "❌ 没有 GITHUB_SHA:检出必须固定到事件的完整 SHA"; exit 1; }
|
||
[ -n "${GITHUB_TOKEN:-}" ] || { echo "❌ 没有拿到读仓库用的令牌(GITHUB_TOKEN 为空)"; exit 1; }
|
||
gitauth() { git -c credential.helper='!f() { printf "username=x-access-token\npassword=%s\n" "$GITHUB_TOKEN"; }; f' "$@"; }
|
||
|
||
# ① 检出必须就是事件那个完整 SHA(bootstrap 已经断言过,这里再断言一次 —— 这一层能被本地负例打到)
|
||
GOT="$(git rev-parse HEAD 2>/dev/null || echo "")"
|
||
if [ "$GOT" != "$EVENT_SHA" ]; then
|
||
echo "❌ 检出 SHA 与事件 SHA 不一致:实际 ${GOT:-空} / 事件 $EVENT_SHA"
|
||
exit 1
|
||
fi
|
||
# remote 里不许有凭据(防有人改回内嵌凭据的写法)
|
||
if git remote get-url origin 2>/dev/null | grep -q '@'; then
|
||
echo "❌ remote URL 里带上了凭据:$(git remote get-url origin | sed 's#//.*@#//***@#')"
|
||
exit 1
|
||
fi
|
||
|
||
# ② 补取:远端追踪引用(分支 + 默认分支)与事件 before
|
||
# —— 没有这些引用,范围校验要么算不出、要么退化成"整部历史",两种都不是"检查过本次改动"。
|
||
if [ -n "$BR" ]; then
|
||
gitauth fetch -q --depth 50 origin "refs/heads/${BR}:refs/remotes/origin/${BR}" 2>/dev/null \
|
||
&& echo "✅ 已取远端分支引用 refs/remotes/origin/${BR}"
|
||
fi
|
||
|
||
DEFAULT=""
|
||
if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -f "${GITHUB_EVENT_PATH}" ]; then
|
||
DEFAULT="$(python3 -c 'import json,os
|
||
try:
|
||
d = json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))
|
||
print((d.get("repository") or {}).get("default_branch") or "")
|
||
except Exception:
|
||
print("")' 2>/dev/null || echo "")"
|
||
fi
|
||
[ -n "$DEFAULT" ] || DEFAULT="$(git symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's#^origin/##' || true)"
|
||
[ -n "$DEFAULT" ] || DEFAULT="$BR" # 推 main 时,被推的分支就是默认分支
|
||
if [ -n "$DEFAULT" ]; then
|
||
gitauth fetch -q --depth 50 origin "refs/heads/${DEFAULT}:refs/remotes/origin/${DEFAULT}" 2>/dev/null \
|
||
&& echo "✅ 已取默认分支引用 refs/remotes/origin/${DEFAULT}(新分支推送的范围基准)" \
|
||
|| echo "⚠️ 取不到默认分支 ${DEFAULT}:新分支推送会因此明确失败,不会静默算成空范围"
|
||
else
|
||
echo "⚠️ 事件里没有默认分支名,也推断不出:新分支推送会因此明确失败"
|
||
fi
|
||
|
||
BEFORE=""
|
||
if [ -n "${GITHUB_EVENT_PATH:-}" ] && [ -f "${GITHUB_EVENT_PATH}" ]; then
|
||
BEFORE="$(python3 -c 'import json,os
|
||
try:
|
||
d = json.load(open(os.environ["GITHUB_EVENT_PATH"], encoding="utf-8"))
|
||
print(d.get("before") or "")
|
||
except Exception:
|
||
print("")' 2>/dev/null || echo "")"
|
||
fi
|
||
if [ -n "$BEFORE" ] && [ "$BEFORE" != "$ZERO" ]; then
|
||
if git cat-file -e "${BEFORE}^{commit}" 2>/dev/null; then
|
||
echo "✅ 事件 before ${BEFORE:0:12} 已在克隆里(本次推送范围可直接算)"
|
||
else
|
||
gitauth fetch -q --depth 50 origin "$BEFORE" 2>/dev/null \
|
||
&& echo "✅ 已补取事件 before ${BEFORE:0:12}" \
|
||
|| echo "⚠️ 取不到事件 before ${BEFORE:0:12}:范围校验会明确失败,不会静默收缩"
|
||
fi
|
||
else
|
||
echo "(事件里没有 before / before 为全 0:新分支推送,范围按默认分支为基准判)"
|
||
fi
|
||
|
||
git rev-list --count HEAD > "${WS}/.ci-commits-count" 2>/dev/null || echo 1 > "${WS}/.ci-commits-count"
|
||
: > "${WS}/.ci-preview-url"
|
||
echo "✅ 检出就绪:${GITHUB_REPOSITORY} 分支 ${BR:-(游离)} @ ${GOT:0:12}(== 事件完整 SHA,协议 $GOT_SCHEME)"
|
||
git log -1 --format=' %h %an %s'
|
||
echo "CI_COMMITS_COUNT_FILE=${WS}/.ci-commits-count" >> "${GITHUB_ENV:-/dev/null}" |