Merge main into refactor/rename-batch6-site-docs:吸收前置检查重试脚本,GC_REPO 收为 job 级单一定义点

PR #19 合入后 main 上 ci.yml / release.yml 的前置检查换成了公共脚本
.github/scripts/check-gitcode-repo.sh,与本批「GC_REPO 收为单一定义点」
改了同一段代码,故冲突。解决口径:

- 前置检查主体取 main 的重试脚本版本(不保留本批内联的那份长 case,
  重试/退避/notfound-优先于-403 的能力保留)
- GC_REPO 仍收为 job 级 env 单一定义处:main 上 ci.yml 两处、release.yml
  三处字面量(含 upload_assets 调用里那处)全部改为走 ${GC_REPO}
- 结果:每个文件内部只有 1 个定义点,注释里的实测清单同步为当前状态

零代码改动:.go/.lua/go.mod/go.sum/third_party 变更数 0。
YAML safe_load 两文件通过;check-gitcode-repo.sh bash -n 通过。
This commit is contained in:
xiao-zhai
2026-10-22 07:17:57 +08:00
3 changed files with 221 additions and 100 deletions

189
.github/scripts/check-gitcode-repo.sh vendored Executable file
View File

@ -0,0 +1,189 @@
#!/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

View File

@ -229,35 +229,23 @@ jobs:
env:
# ══════════════════════════════════════════════════════════════════
# ★★ gitcode 镜像目标仓名 —— **本 job 的唯一定义处**。
# 改 gitcode 仓名时只改这一行;下面推 main 的消费点走 ${GC_REPO}。
# 与 release.yml 的 sync-gitcode 保持同值(两处各自独立定义,
# 因为它们是不同的 workflow —— 改的时候两个文件都要改,但每个
# 文件内部只有一个定义处)。
#
# 取值依据(实测 2026-10-21 20:1x,直连不带代理,非推测):
# $ curl -s -o /dev/null -w '%{http_code}' \
# https://gitcode.com/JianFeeeee/<repo>
# 改 gitcode 仓名时只改这一行;下面所有消费点都走 ${GC_REPO}。
# 与 release.yml 的 sync-gitcode 保持同值(两个 workflow 各自独立
# 定义:改的时候两个文件都要改,但每个文件内部只有一个定义处)。
#
# 取值依据(实测 2026-10-21,直连不带代理,非推测):
# https://gitcode.com/JianFeeeee/HologramAgent → 200 ← 当前用这个
# https://gitcode.com/JianFeeeee/HomeAgent → 301 ← 旧名,跳转到上面
# https://gitcode.com/JianFeeeee/homeagent-sdk → 200(SDK 仓,另一对象)
# https://gitcode.com/JianFeeeee/hologramagentsdk → 200(SDK 仓,另一对象)
# https://gitcode.com/JianFeeeee/homeagentsdk → 404
# https://gitcode.com/JianFeeeee/HologramAgent-sdk → 404
#
# 背景:gitcode 侧镜像仓已由 HomeAgent 改名为 HologramAgent(与 GitHub
# 主仓同名)。旧地址 HomeAgent 仍 301 跳转,跟随后也能到达,但走重定向
# ⇒ 别再用它(git 侧会多一跳,且改名前的旧名随时可能被彻底释放)。
# 主仓同名)。旧地址仍 301 跳转,跟随后也能到达,但走重定向 ⇒ 别再用它。
# 判定两仓同一对象:两地址 git ls-remote 的 refs 列表 md5 相同。
#
# ⚠ 失败面(与 release.yml 不同,别搞混):
# 本 job 的 needs 是 [go, race, cross, gui, csrc, docs]。前置检查
# exit 1 ⇒ 本 job 红 ⇒ **main 的整条 CI 红**(不是只红一格)。
# 而 release.yml 的 sync-gitcode 只红在镜像同步这一格,GitHub 侧
# release 产物已经发出去了。
#
# 以后再改名:gitcode 侧仓名一变,只改这一行(+ release.yml 与
# upload_assets.py 各一行,共三个定义点)。
# ⚠ 失败面:本 job 的 needs 是 [go, race, cross, gui, csrc, docs]。
# 前置检查 exit 1 ⇒ 本 job 红 ⇒ **main 的整条 CI 红**(不是只红一格)。
# release.yml 的 sync-gitcode 只红在镜像同步这一格(GitHub 侧产物已发)。
# ══════════════════════════════════════════════════════════════════
GC_REPO: JianFeeeee/HologramAgent
steps:
@ -280,48 +268,22 @@ jobs:
# 若不在这里显式探测,缺少目标仓时只会得到 git 的原始报错
# ("repository not found" 混在 push 输出里),看不出是「仓没建」。
# 语义:token 未配置 = 可选项跳过(上面那步);token 有但仓不存在 = 硬失败。
#
# ★ 有界重试(2026-10-21 dfc4e16 的教训):首次跑这一步时 gitcode 网络抖动,
# 一次 ls-remote 判失败 ⇒ job 红;重跑立刻全绿。现在只对**网络层**失败
# 重试(3 次,退避 5s/10s),而「仓不存在」与「凭据问题」**立即失败不重试**
# —— 后两者重试只会把真问题拖长、并把它埋在噪声里。
#
# 实现落在 .github/scripts/check-gitcode-repo.sh,与 release.yml 的
# sync-gitcode **共用同一个文件**(同口径靠同一份代码保证,不靠人抄两遍)。
- name: 前置检查 gitcode 目标仓是否存在
if: steps.tok.outputs.ok == 'true'
env:
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
run: |
set -euo pipefail
GC_URL="https://JianFeeeee:${GC_TOKEN}@gitcode.com"
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
# 用 ls-remote 探测:与后面那次 push 走同一个 URL、同一套凭据,
# 比另调一次 API 更贴近真实失败面。
if out=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads \
"${GC_URL}/${GC_REPO}.git" 2>&1); then
echo " 目标仓可访问:gitcode.com/${GC_REPO}"
else
# 日志脱敏:git 报错可能把 URL 的 userinfo(token 就在里面)带出来。
# 先按 URL 结构遮 https://<userinfo>@ —— 与 token 内容无关,稳;
# 再尽力遮一次字面量(token 含特殊字符时可能不生效,故只做兜底)。
err=$(printf '%s\n' "$out" | sed -E 's#(https?://)[^@/[:space:]]*@#\1***@#g')
err=$(printf '%s\n' "$err" | sed "s|${GC_TOKEN}|***|g" 2>/dev/null || printf '%s\n' "$err")
case "$err" in
# gitcode 真实报错(2026-10 实测):目标仓不存在时返回
# remote: <CH.00905403> The project you were looking for could not be found.
# fatal: ... The requested URL returned error: 403
# 注意是 "could not be found" 与 403,不是 github 那种 "Repository not found"/404。
*"could not be found"*|*"not found"*|*"Not Found"*|*"404"*|*"does not exist"*|*"does not appear to be a git repository"*)
echo " ✗ gitcode 目标仓不存在:gitcode.com/${GC_REPO}"
echo " 请先在该账号下创建镜像仓(只读镜像),再重跑本 job:"
echo " https://gitcode.com/${GC_REPO}"
;;
*"403"*|*"401"*|*"Forbidden"*|*"Unauthorized"*)
echo " ✗ 无法访问 gitcode 目标仓 gitcode.com/${GC_REPO}:仓不存在,或 token 无权访问"
echo " 若仓已建,请确认 GITCODE_TOKEN 属于该仓的推送账号:"
echo " https://gitcode.com/${GC_REPO}"
;;
*)
echo " ✗ 无法访问 gitcode 目标仓 gitcode.com/${GC_REPO}(网络或凭据问题):"
printf '%s\n' "$err" | sed 's/^/ /'
;;
esac
# 显式失败 —— 不静默跳过,也不 echo 一句就当成功。
exit 1
fi
bash .github/scripts/check-gitcode-repo.sh
- name: 推 main 到 gitcode(快进)
if: steps.tok.outputs.ok == 'true'
env:

View File

@ -356,24 +356,21 @@ jobs:
# ══════════════════════════════════════════════════════════════════
# ★★ gitcode 镜像目标仓名 —— **本 job 的唯一定义处**。
# 改 gitcode 仓名时只改这一行;下面所有消费点都走 ${GC_REPO}。
# 与 ci.yml 的 sync-gitcode-main 保持同值(两个 workflow 各自独立
# 定义:改的时候两个文件都要改,但每个文件内部只有一个定义处)。
#
# 取值依据(实测 2026-10-21 20:1x,直连不带代理,非推测):
# $ curl -s -o /dev/null -w '%{http_code}' \
# https://gitcode.com/JianFeeeee/<repo>
#
# 取值依据(实测 2026-10-21,直连不带代理,非推测):
# https://gitcode.com/JianFeeeee/HologramAgent → 200 ← 当前用这个
# https://gitcode.com/JianFeeeee/HomeAgent → 301 ← 旧名,跳转到上面
# https://gitcode.com/JianFeeeee/homeagent-sdk → 200(SDK 仓,另一对象)
# https://gitcode.com/JianFeeeee/hologramagentsdk → 200(SDK 仓,另一对象)
# https://gitcode.com/JianFeeeee/homeagentsdk → 404
# https://gitcode.com/JianFeeeee/HologramAgent-sdk → 404
#
# 背景:gitcode 侧镜像仓已由 HomeAgent 改名为 HologramAgent(与 GitHub
# 主仓同名)。旧地址 HomeAgent 仍 301 跳转,跟随后也能到达,但走重定向
# ⇒ 别再用它。下面几处消费点(ls-remote 探测、push tag、POST /releases、
# upload_assets 的 GITCODE_REPO)全部走 ${GC_REPO},因此只改本行即可。
# 主仓同名)。旧地址仍 301 跳转,跟随后也能到达,但走重定向 ⇒ 别再用它。
# 判定两仓同一对象:两地址 git ls-remote 的 refs 列表 md5 相同。
#
# 以后再改名:只改这一行(+ ci.yml 与 upload_assets.py 各一行,共三个定义点)。
# ⚠ 失败面(与 ci.yml 不同,别搞混):本 job 只红在镜像同步这一格,
# GitHub 侧 release 产物此时已经发出去了;ci.yml 那个会红整条 main CI。
# ══════════════════════════════════════════════════════════════════
GC_REPO: JianFeeeee/HologramAgent
steps:
@ -396,46 +393,19 @@ jobs:
# 改名(HomeAgent → HologramAgent)后 gitcode 目标仓需要**人工新建**,
# 不探的话失败会埋在 push/curl 的原始输出里,看不出是「仓没建」。
# 语义:token 未配 = 可选项跳过(上一步);token 有但仓不存在 = 硬失败。
#
# ★ 有界重试(与 ci.yml 的 sync-gitcode-main 同口径):只对**网络层**失败
# 重试(3 次,退避 5s/10s);「仓不存在」与「凭据问题」**立即失败不重试**。
# 两处逻辑落在同一个 .github/scripts/check-gitcode-repo.sh —— 不各写一份,
# 因为两份「逐字一致」的脚本迟早会漂。
- name: 前置检查 gitcode 目标仓是否存在
if: steps.tok.outputs.ok == 'true'
env:
GC_TOKEN: ${{ secrets.GITCODE_TOKEN }}
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
run: |
set -euo pipefail
GC_URL="https://JianFeeeee:${GC_TOKEN}@gitcode.com"
# GC_REPO 来自 job 级 env(唯一定义处),此处不重复字面量。
if out=$(GIT_TERMINAL_PROMPT=0 git ls-remote --heads \
"${GC_URL}/${GC_REPO}.git" 2>&1); then
echo " 目标仓可访问:gitcode.com/${GC_REPO}"
else
# 日志脱敏:git 报错可能把 URL 的 userinfo(token 就在里面)带出来。
# 先按 URL 结构遮 https://<userinfo>@ —— 与 token 内容无关,稳;
# 再尽力遮一次字面量(token 含特殊字符时可能不生效,故只做兜底)。
err=$(printf '%s\n' "$out" | sed -E 's#(https?://)[^@/[:space:]]*@#\1***@#g')
err=$(printf '%s\n' "$err" | sed "s|${GC_TOKEN}|***|g" 2>/dev/null || printf '%s\n' "$err")
case "$err" in
# gitcode 真实报错(2026-10 实测):目标仓不存在时返回
# remote: <CH.00905403> The project you were looking for could not be found.
# fatal: ... The requested URL returned error: 403
# 注意是 "could not be found" 与 403,不是 github 那种 "Repository not found"/404。
*"could not be found"*|*"not found"*|*"Not Found"*|*"404"*|*"does not exist"*|*"does not appear to be a git repository"*)
echo " ✗ gitcode 目标仓不存在:gitcode.com/${GC_REPO}"
echo " 请先在该账号下创建镜像仓(只读镜像),再重跑本 job:"
echo " https://gitcode.com/${GC_REPO}"
;;
*"403"*|*"401"*|*"Forbidden"*|*"Unauthorized"*)
echo " ✗ 无法访问 gitcode 目标仓 gitcode.com/${GC_REPO}:仓不存在,或 token 无权访问"
echo " 若仓已建,请确认 GITCODE_TOKEN 属于该仓的推送账号:"
echo " https://gitcode.com/${GC_REPO}"
;;
*)
echo " ✗ 无法访问 gitcode 目标仓 gitcode.com/${GC_REPO}(网络或凭据问题):"
printf '%s\n' "$err" | sed 's/^/ /'
;;
esac
# 显式失败 —— 不静默跳过,也不 echo 一句就当成功。
exit 1
fi
bash .github/scripts/check-gitcode-repo.sh
- name: 推 tag 与附件到 gitcode
if: steps.tok.outputs.ok == 'true'
env: