Files
HomeAgent/.github/scripts/check-gitcode-repo.sh
JianFeeeee 777f2f810d fix(ci): gitcode 目标仓前置检查加有界重试(网络抖动不再误判)
2026-10-21 的 main 流水线(dfc4e16)里,「前置检查 gitcode 目标仓是否存在」
首次执行时 gitcode 没有给出预期响应,一次 ls-remote 判失败 ⇒ sync job 红;
重跑立刻全绿。把一次网络抖动翻译成「镜像同步坏了」,是纯噪声。

改法:只对**网络层**失败重试,其余立即失败。
- 网络层(DNS/连接重置/TLS/超时/EOF)→ 最多 3 次,退避 5s、10s
- 仓不存在(gitcode 实测:could not be found + **403**,不是 404)
  → 立即失败,不重试。重试只会把「去建仓」拖成十几秒空等
- 凭据问题(401/403 但不带 not-found 特征)→ 立即失败,不重试
  (token 错了等 15 秒也不会变对)
- 分类顺序上 notfound 必须先于 403:gitcode 的「仓不存在」响应里
  **同时**含 could not be found 与 error: 403,先判 403 会把真问题贴上
  「凭据问题」的错误标签
- 每次尝试打一行「第 N/M 次探测失败(类别):摘要」;重试提示打到 stderr
  (⏳ … 秒后重试),Actions 日志里一眼看得出发生过重试
- 耗尽重试时整段回放每次尝试的摘要,并明确写「判定:网络问题」
  —— 与「仓不存在」/「凭据问题」的判定措辞互不相同,可直接 grep 区分

同口径:**抽成 .github/scripts/check-gitcode-repo.sh,两个 workflow 共用同一个
文件**,而不是两边各写一份「逐字一致」的脚本 —— 后者迟早会漂。ci.yml 的
sync-gitcode-main 与 release.yml 的 sync-gitcode 各留 1 行 bash 调用。
(抽得出公共片段,故没有采用「两边逐字改」的方案。)

token 安全:GC_URL 含 token,永不 echo;所有输出先过 redact()。脱敏两道:
先按字面量遮 token(BRE 元字符先转义),再按 URL 结构遮 userinfo@。
结构式的 password 段**不能排除 `/`** —— 真实 token 可以含 `/`,第一版写成
[^@/[:space:]]* 时 `https://user:tok/en@host` 匹配不上,只剩字面量一道兜住
(判据 C6 首次就是红的)。

判据与变异(9 组正向 / 8 条变异,全部咬住):
判据脚本与变异脚本见 commondev 台账 criteria/b7-gitcode-precheck-retry{,.mutations}.sh。
正向覆盖:重试次数上限(恰好 3 次探测)、仓不存在时 1 次即失败无重试痕迹、
首次成功无重试噪声、抖动后第 2 次成功、凭据问题不重试、token 不进日志、
默认退避真的是 5s/10s(用假 sleep 记录入参,不真等 15 秒)、
两处 workflow 都调同一脚本事 yaml 可解析 + bash -n。
变异 8/8 咬住:默认次数 3→1、退避→0、仓不存在也重试、去掉上限、
去掉重试提示、清空 notfound 特征串、去掉结构式脱敏、ci.yml 退回内联 ls-remote。

过程中修掉三处自己判据的盲区(都不是改产物去迁就判据,而是补判据):
1. M2(退避→0/0)首次未被咬住 ⇒ 补 C7「默认退避 = 5 10」(假 sleep 记录入参)
2. M7(删结构式脱敏)首次未被咬住 ⇒ 补 C7b:userinfo 密码用**别的** secret
   且**含 `/`**。C6 用当前 token 的字面量做输入,字面量替换本身就能抹掉,
   于是结构式规则删了也照样绿;且密码不含 `/` 时兜底的裸 user@ 规则会顶上,
   同样验不出来
3. C7b 第一版断言 grep 'JianFeeeee' 会误报 —— 脚本正常输出里就有
   gitcode.com/JianFeeeee/HologramAgent(那是路径不是凭据),改为断言
   userinfo 形态(://JianFeeeee[:@])

真实网络实测(不是只跑假 git):
- 仓存在:目标仓可访问,rc=0
- 仓不存在:1 次探测即失败,日志含 could not be found + 403,rc=1
- 网络失败:3 次探测 + 2 次退避,总耗时 5.7s(退避调成 1s/1s),rc=1

yaml.safe_load 三个 workflow 全部可解析;bash -n 通过。

未改动(留待裁决):deploy/scripts/upload_assets.py docstring 里
「SDK 仓传 JianFeeeee/homeagent-sdk」指 gitcode 侧、与实测一致
(gitcode: homeagent-sdk=200 / homeagentsdk=404;GitHub 相反),保持原样。
2026-10-21 23:46:01 +08:00

190 lines
8.7 KiB
Bash
Executable File
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.

#!/usr/bin/env bash
# 前置检查 gitcode 镜像目标仓是否存在(**有界重试**)
#
# ── 为什么要重试 ──────────────────────────────────────────────
# 2026-10-21 的 main 流水线(dfc4e16)第一次跑这一步时,gitcode 没有给出
# 预期响应(网络抖动),一次 `ls-remote` 判失败 ⇒ sync job 红;重跑立刻全绿。
# 把「一次网络抖动」翻译成「镜像同步坏了」,是纯噪声。
#
# ── 为什么不能无脑重试 ────────────────────────────────────────
# 目标仓**确实不存在**时,重试只会把「去建仓」这个真问题拖成 3 次等待 +
# 更长的红,问题反而被掩盖。所以只对**网络层**失败重试:
#
# 网络层失败(DNS / 连接重置 / TLS / 超时 / 5xx / EOF)→ 重试,最多 3 次,退避 5s、10s
# 仓不存在(gitcode 实测:`could not be found` + 403)→ 立即失败,**不重试**
# 凭据问题(401/403 但不带「不存在」特征)→ 立即失败,**不重试**
# (token 错了等 15 秒也不会变对,只会让日志更难读)
#
# ── 口径 ──────────────────────────────────────────────────────
# ci.yml 的 sync-gitcode-main 与 release.yml 的 sync-gitcode **共用本文件**,
# 不各写一份 —— 两份逐字一致的脚本迟早会漂。
#
# ── token 安全 ───────────────────────────────────────────────
# GC_URL 里含 token,**任何情况下都不 echo 它**;git 的报错可能把 URL 的
# userinfo 带出来,故所有输出都先过 redact()。
#
# 用法:
# GC_TOKEN=<token> [GC_REPO=owner/name] bash .github/scripts/check-gitcode-repo.sh
# 退出码:0 = 目标仓可达;1 = 不可达(原因见日志末行「判定」)
#
# 环境变量(除 GC_TOKEN 外均为可选项,CI 只设 GC_TOKEN):
# GC_REPO 默认 JianFeeeee/HologramAgent
# GC_HOST 默认 gitcode.com
# GC_CHECK_ATTEMPTS 默认 3(探测总次数上限,含首次)
# GC_CHECK_BACKOFF 默认 "5 10"(每次失败后的等待秒数,按序取用;
# 仅供本地判据/调试加速,CI 不设置)
set -uo pipefail
GC_REPO="${GC_REPO:-JianFeeeee/HologramAgent}"
GC_HOST="${GC_HOST:-gitcode.com}"
MAX_ATTEMPTS="${GC_CHECK_ATTEMPTS:-3}"
read -r -a BACKOFF <<<"${GC_CHECK_BACKOFF:-5 10}"
# 凭据缺失时必须响亮失败:静默匿名探测会得到 403,被误读成「仓不存在/权限不足」,
# 而不是「根本没配 token」。
: "${GC_TOKEN:?GC_TOKEN 未设置 —— 前置检查不能匿名探测}"
# ★ 这一行含 token。永远不要 echo / printf 它,也不要把它拼进任何报错。
GC_URL="https://JianFeeeee:${GC_TOKEN}@${GC_HOST}"
TRAIL=() # 每次尝试的一行摘要,耗尽时整段回放
# redact:把日志里的凭据抹掉。两道,**先字面量、后结构**:
#
# 1) 先按字面量遮 token —— 这是最可靠的一道(token 是我们自己的串,
# 不该出现的地方一个都不该漏)。必须先把 token 里的 sed BRE 元字符
# 转义,否则 token 含 `[`/`*`/`|` 时这条规则会静默失效。
# 2) 再按 URL 结构遮 https://<userinfo>@ —— 兜底 git 回显整个 URL 的情形。
#
# ⚠️ 为什么结构式的 password 段**不能排除 `/`**:真实 token 可以含 `/`
# (`sekret-tok+ABC/123`)。第一版写成 `[^@/[:space:]]*`,于是
# `https://JianFeeeee:sekret-tok+ABC/123@...` 匹配不上,只剩字面量那一道兜住。
# 实测发现(判据 C6 首次就是红的)—— 这类"看起来在脱敏"的规则必须
# 用含特殊字符的 token 验。
redact() {
local s esc
esc=$(printf '%s' "$GC_TOKEN" | sed -e 's/[][\.^$*|]/\\&/g')
if [ -n "$esc" ]; then
s=$(printf '%s\n' "$1" | sed "s|${esc}|***|g" 2>/dev/null || printf '%s\n' "$1")
else
s="$1"
fi
# 结构:先遮 user:pass@(pass 段允许含 /),再遮裸 user@。
s=$(printf '%s\n' "$s" | sed -E 's#(https?://)[^/@[:space:]]+:[^@[:space:]]+@#\1***@#g')
printf '%s\n' "$s" | sed -E 's#(https?://)[^/@[:space:]]+@#\1***@#g'
}
# summarize:把多行 git 输出压成一行摘要(日志里一次尝试一行,便于对照)。
summarize() {
printf '%s\n' "$1" \
| tr '\n' ' ' \
| sed -E 's/[[:space:]]+/ /g; s/^ //; s/ $//' \
| cut -c1-200
}
# classify:把脱敏后的报错分成三类。**顺序重要**。
# gitcode 的「仓不存在」响应里**同时**有 could not be found 和 403:
# remote: <CH.00905403> The project you were looking for could not be found.
# fatal: ... The requested URL returned error: 403
# 若先判 403,就会被错误地归成「凭据问题」—— 真问题被换了个标签,
# 于是要么白等一轮重试、要么提示人去查 token,都指向错误的方向。
classify() {
case "$1" in
*"could not be found"*|*"does not exist"*|*"does not appear to be a git repository"*|*"not found"*|*"Not Found"*|*"404"*)
printf 'notfound\n'
;;
*"403"*|*"401"*|*"Forbidden"*|*"Unauthorized"*|*"Authentication failed"*|*"Permission denied"*)
printf 'auth\n'
;;
*)
printf 'network\n'
;;
esac
}
# report_trail:按尝试序号回放本轮所有尝试的摘要。
report_trail() {
if [ "${#TRAIL[@]}" -gt 0 ]; then
echo " 每次尝试的错误摘要:"
local s
for s in "${TRAIL[@]}"; do
echo " ${s}"
done
fi
}
# fail:统一失败出口。**每次失败都要打印「判定」,让人一眼分清
# 「仓不存在」/「凭据问题」/「网络问题」** —— 三者要采取的动作完全不同。
fail() {
local verdict="$1" attempts="$2" err="$3"
case "$verdict" in
notfound)
echo " ✗ gitcode 目标仓不存在:${GC_HOST}/${GC_REPO}(第 ${attempts} 次探测,未重试)"
echo " 请先在该账号下创建镜像仓(只读镜像),再重跑本 job:"
echo " https://${GC_HOST}/${GC_REPO}"
echo " 判定:仓不存在 —— 不重试(重试只会把「去建仓」拖成十几秒空等)"
;;
auth)
echo " ✗ 无法访问 gitcode 目标仓 ${GC_HOST}/${GC_REPO}:凭据问题(第 ${attempts} 次探测,未重试)"
echo " 若仓已建,请确认 GITCODE_TOKEN 属于该仓的推送账号:"
echo " https://${GC_HOST}/${GC_REPO}"
echo " 判定:凭据问题(401/403,不带 not-found 特征)—— 不重试(等十几秒它也不会变对)"
report_trail
;;
*)
echo " ✗ 连续 ${attempts} 次探测 gitcode 目标仓失败:${GC_HOST}/${GC_REPO}"
echo " 判定:网络问题(非「仓不存在」—— 那种情况会立即失败、不重试)"
echo " 可能的原因:gitcode 侧抖动 / DNS / TLS 握手被掐 / CI 出口被限流"
report_trail
echo " 最后一次的完整(已脱敏)输出:"
printf '%s\n' "$err" | sed 's/^/ /'
;;
esac
# 显式失败 —— 不静默跳过,也不 echo 一句就当成功。
exit 1
}
attempt=1
while :; do
# 用 ls-remote 探测:与后面那次 push 走同一个 URL、同一套凭据,
# 比另调一次 API 更贴近真实失败面。
if out=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads \
"${GC_URL}/${GC_REPO}.git" 2>&1); then
if [ "$attempt" -eq 1 ]; then
echo " 目标仓可访问:${GC_HOST}/${GC_REPO}"
else
echo " 目标仓可访问:${GC_HOST}/${GC_REPO}(重试后第 ${attempt} 次探测成功)"
fi
exit 0
fi
err=$(redact "$out")
digest=$(summarize "$err")
verdict=$(classify "$err")
TRAIL+=("#${attempt} [${verdict}] ${digest}")
echo " 第 ${attempt}/${MAX_ATTEMPTS} 次探测失败(${verdict}):${digest}"
if [ "$verdict" != "network" ]; then
fail "$verdict" "$attempt" "$err"
fi
if [ "$attempt" -ge "$MAX_ATTEMPTS" ]; then
fail network "$attempt" "$err"
fi
# 退避:按 BACKOFF 顺序取,越界则沿用最后一个值。
idx=$((attempt - 1))
if [ "$idx" -ge "${#BACKOFF[@]}" ]; then
idx=$((${#BACKOFF[@]} - 1))
fi
sleep_secs="${BACKOFF[$idx]}"
# 打到 stderr:让人在 Actions 日志里一眼看出「发生过重试」,
# 而不是以为只试了一次就红了。
echo " ⏳ 判为网络层失败,${sleep_secs} 秒后重试:${attempt}/${MAX_ATTEMPTS} → $((attempt + 1))/${MAX_ATTEMPTS}" >&2
sleep "$sleep_secs"
attempt=$((attempt + 1))
done