- infra/cicd:docker-compose(gitea/jenkins)、JCasC(凭据统一 ${ENV} 注入,无硬编码)、
备份/恢复、健康检查、凭据轮换与 git 历史清除脚本
- docs/deployment/cicd:安装、高可用备份监控、凭据事故复盘
- .env.example 仅为占位模板;真实 .env 由 .gitignore 排除
190 lines
6.7 KiB
Bash
Executable File
190 lines
6.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ============================================================
|
||
# 清除 git 历史中的明文凭据
|
||
#
|
||
# 背景(2026-09-14 实测确认已入库并推送到公网可达远程):
|
||
# 泄露位置见 docs/deployment/cicd/03-credential-incident.md 第 2 节
|
||
#
|
||
# ⚠️ 强制推送会重写远程历史:
|
||
# - 所有协作者必须重新 clone(旧本地仓库 push 会把泄露内容带回来)
|
||
# - 若 Gitea 上有未合入的 PR / fork,会失效
|
||
# - 建议:先轮换凭据(rotate-credentials.sh),再清史,最后强制推送
|
||
#
|
||
# 前置:pip install git-filter-repo (或 brew install git-filter-repo)
|
||
#
|
||
# ⚠️ 本脚本【不】内置泄露明文。曾踩坑:把泄露值硬编码进本脚本的注释与
|
||
# 替换规则里,一旦提交等于把泄露物重新入库一次,清史变成滚雪球。
|
||
# 替换规则从仓库外文件读取(默认 ~/.novalon-purge-secrets.txt)。
|
||
#
|
||
# 用法:
|
||
# SECRETS_FILE=~/.novalon-purge-secrets.txt \
|
||
# ./purge-git-history.sh --dry-run # 只扫描并列出命中,不改写
|
||
# ./purge-git-history.sh # 执行清史(会先做 mirror 备份)
|
||
# ./purge-git-history.sh --no-push # 清史但不强推,留自己复核
|
||
# ============================================================
|
||
|
||
set -Eeuo pipefail
|
||
|
||
DRY_RUN=0
|
||
NO_PUSH=0
|
||
for a in "$@"; do
|
||
case "$a" in
|
||
--dry-run) DRY_RUN=1 ;;
|
||
--no-push) NO_PUSH=1 ;;
|
||
esac
|
||
done
|
||
|
||
REPO_DIR="${REPO_DIR:-$(cd "$(dirname "$0")/../../.." && pwd)}"
|
||
BACKUP_DIR="${BACKUP_DIR:-$REPO_DIR/.workbuddy/tmp/git-purge-backup}"
|
||
REMOTE="${REMOTE:-origin}"
|
||
BRANCH="${BRANCH:-$(git -C "$REPO_DIR" rev-parse --abbrev-ref HEAD 2>/dev/null || echo main)}"
|
||
SECRETS_FILE="${SECRETS_FILE:-$HOME/.novalon-purge-secrets.txt}"
|
||
|
||
# 需要整体删除的文件(含明文配置,且本身不该入库)
|
||
FILES_TO_DROP=(
|
||
"scripts/setup-cicd/cicd.config"
|
||
)
|
||
|
||
log() { echo "[purge] $*"; }
|
||
die() { echo "[purge] ❌ $*" >&2; exit 1; }
|
||
|
||
cd "$REPO_DIR" || die "无法进入仓库: $REPO_DIR"
|
||
[[ -d .git ]] || die "不是 git 仓库: $REPO_DIR"
|
||
|
||
# ============================================================
|
||
# 读取替换规则(格式: 明文==>***REMOVED*** ,每行一条)
|
||
# ============================================================
|
||
if [[ ! -s "$SECRETS_FILE" ]]; then
|
||
cat >&2 <<EOF
|
||
[purge] ❌ 未找到替换规则文件: $SECRETS_FILE
|
||
|
||
为避免把泄露明文写进仓库,本脚本不内置规则。请在【仓库外】创建(不要入 git):
|
||
|
||
cat > $SECRETS_FILE <<'EOF'
|
||
<泄露明文1>==>***REMOVED***
|
||
<泄露明文2>==>***REMOVED***
|
||
<泄露明文3>==>***REMOVED***
|
||
EOF
|
||
chmod 600 $SECRETS_FILE
|
||
|
||
具体哪些值:见 docs/deployment/cicd/03-credential-incident.md 第 2 节列出的
|
||
文件与变量名(cicd.config 的三个值 + setup-registry-auth.sh 的 gto_ secret)。
|
||
创建后重新运行本脚本。
|
||
EOF
|
||
exit 1
|
||
fi
|
||
|
||
# 提取明文值用于扫描命中
|
||
mapfile -t SECRET_VALUES < <(sed 's/==>.*$//' "$SECRETS_FILE" | grep -v '^[[:space:]]*$')
|
||
if (( ${#SECRET_VALUES[@]} == 0 )); then
|
||
die "替换规则文件为空或格式错误: $SECRETS_FILE"
|
||
fi
|
||
log "已加载 ${#SECRET_VALUES[@]} 条替换规则(值不回显)"
|
||
|
||
# 构造 git grep 的 -e 参数数组
|
||
GREP_ARGS=()
|
||
for v in "${SECRET_VALUES[@]}"; do
|
||
GREP_ARGS+=( -e "$v" )
|
||
done
|
||
|
||
scan_hits() {
|
||
# shellcheck disable=SC2086
|
||
git grep -l -F "${GREP_ARGS[@]}" $(git rev-list --all) -- 2>/dev/null | wc -l | tr -d ' '
|
||
}
|
||
|
||
# ---------- 扫描命中 ----------
|
||
log "----- 扫描历史中的命中 -----"
|
||
git log --all --oneline -- "${FILES_TO_DROP[@]}" scripts/setup-registry-auth.sh 2>/dev/null | head -20 || true
|
||
HIT_COUNT=$(scan_hits)
|
||
log "历史中含泄露凭据的 blob 数: $HIT_COUNT"
|
||
|
||
if (( DRY_RUN == 1 )); then
|
||
log "dry-run 结束,未做任何改写。"
|
||
exit 0
|
||
fi
|
||
|
||
if [[ "$HIT_COUNT" == "0" ]]; then
|
||
log "未发现命中,无需清史。"
|
||
exit 0
|
||
fi
|
||
|
||
# ---------- 备份 ----------
|
||
mkdir -p "$BACKUP_DIR"
|
||
MIRROR="$BACKUP_DIR/$(basename "$REPO_DIR")-$(date +%Y%m%d_%H%M%S).git"
|
||
log "----- 备份镜像到 $MIRROR -----"
|
||
git clone --mirror "$REPO_DIR" "$MIRROR" || die "备份失败,已中止(不冒险改写)"
|
||
log "✅ 备份完成"
|
||
|
||
# ---------- 工具检查 ----------
|
||
if command -v git-filter-repo >/dev/null 2>&1; then
|
||
TOOL="filter-repo"
|
||
elif python3 -c "import git_filter_repo" >/dev/null 2>&1; then
|
||
TOOL="python-module"
|
||
else
|
||
die "未找到 git-filter-repo。安装: pip install git-filter-repo 或 brew install git-filter-repo"
|
||
fi
|
||
|
||
# ---------- 二次确认 ----------
|
||
echo
|
||
echo "⚠️ 即将重写 git 历史并强制推送到 $REMOTE。"
|
||
echo "⚠️ 所有协作者必须重新 clone,未合入的 PR 可能失效。"
|
||
read -r -p "输入 'PURGE' 继续: " C
|
||
if [[ "$C" != "PURGE" ]]; then
|
||
log "未确认,已中止。"
|
||
exit 1
|
||
fi
|
||
|
||
# ---------- 执行清史 ----------
|
||
log "----- 执行清史(工具: $TOOL) -----"
|
||
if [[ "$TOOL" == "filter-repo" ]]; then
|
||
git filter-repo --replace-text "$SECRETS_FILE" \
|
||
$(printf -- '--path %s --invert-paths ' "${FILES_TO_DROP[@]}") \
|
||
--force
|
||
else
|
||
python3 -m git_filter_repo --replace-text "$SECRETS_FILE" \
|
||
$(printf -- '--path %s --invert-paths ' "${FILES_TO_DROP[@]}") \
|
||
--force
|
||
fi
|
||
log "✅ 历史已重写"
|
||
|
||
# ---------- 本地清理 ----------
|
||
log "----- 本地 GC -----"
|
||
git reflog expire --expire=now --all
|
||
git gc --prune=now --aggressive
|
||
log "✅ 本地对象已清理"
|
||
|
||
# ---------- 校验 ----------
|
||
log "----- 校验:重新扫描是否还有残留 -----"
|
||
REMAIN=$(scan_hits)
|
||
if [[ "$REMAIN" != "0" ]]; then
|
||
die "仍有 $REMAIN 处残留!请检查 .git/packed-refs 或其他分支。备份在: $MIRROR"
|
||
fi
|
||
log "✅ 无残留"
|
||
|
||
# ---------- 推送 ----------
|
||
if (( NO_PUSH == 1 )); then
|
||
log "--no-push:已跳过强制推送。复核后手动执行:"
|
||
log " git push origin --force --all && git push origin --force --tags"
|
||
else
|
||
read -r -p "确认强制推送到 $REMOTE ? (yes/N): " P
|
||
if [[ "$P" == "yes" ]]; then
|
||
git push "$REMOTE" --force --all
|
||
git push "$REMOTE" --force --tags
|
||
log "✅ 已强制推送"
|
||
cat <<EOF
|
||
|
||
================ 收尾(必做) =================
|
||
1. 通知所有协作者:rm -rf 本地仓库后重新 clone(旧仓库 push 会带回泄露内容)
|
||
2. 在 Gitea 上执行「仓库设置 → 维护 → 运行垃圾回收」,删除远程的旧对象
|
||
(否则旧 blob 仍可通过 commit sha 直接访问)
|
||
3. 检查是否有 fork / 镜像 / CI 缓存仍持有旧历史
|
||
4. 确认 rotate-credentials.sh 已执行完毕(清史不能替代轮换!)
|
||
5. 加 pre-commit 钩子防复发:gitleaks / trufflehog
|
||
6. 处置完毕后删除 $SECRETS_FILE
|
||
===============================================
|
||
EOF
|
||
else
|
||
log "已跳过推送。备份镜像保留在: $MIRROR"
|
||
fi
|
||
fi
|