From c25babf4bbb0cbc01090ab2a77d6ca436326518d Mon Sep 17 00:00:00 2001 From: richblack Date: Tue, 21 Jul 2026 11:18:37 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20GitHub=20public=20mirror=20=E7=AE=A1?= =?UTF-8?q?=E7=B7=9A=EF=BC=88=E6=88=90=E5=93=81=E6=AB=A5=E7=AA=97=EF=BC=8C?= =?UTF-8?q?=E9=9D=9E=E5=B7=A5=E4=BD=9C=E7=8F=BE=E5=A0=B4=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit leo 2026-07-21:「應該要給的是一個適合別人用的濃縮結果,不需要有過程。 以後我還是在私有的 template 工作,但 publish 到 GitHub 就把它當貼文。」 - publish-github.sh:移植自 arcrun-rag 既有管線(乾淨歷史、憑證走 header 不進 URL) +修一個真 bug:原版 sanitize 失敗不中止 → 會把未淨化樹 rsync 進 mirror 並推出去 - github-publish-exclude.txt:濾掉工作現場(system-dev/ .claude/ docs/ CLAUDE.md CHANGELOG.md) 保留成品(README/scripts/skills/template) - github-publish-sanitize.py:改寫來源網址 + 發佈前驗證,殘留失效來源即中止 ① git.uncle6.me(private 且即將換 CF 版,別人抓不到) ② uncle6me-web(已 suspend 的舊 GitHub 帳號——README 安裝指令原本還指著它, 別人照著跑會失敗,與 07-20 update.sh 同一顆雷) 公開樹實測:只剩 README/scripts/skills/template,安裝指令指向 youlinhsieh,零殘留。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .gitignore | 3 + scripts/github-publish-exclude.txt | 34 +++++++++++ scripts/github-publish-sanitize.py | 95 ++++++++++++++++++++++++++++++ scripts/publish-github.sh | 87 +++++++++++++++++++++++++++ 4 files changed, 219 insertions(+) create mode 100644 scripts/github-publish-exclude.txt create mode 100755 scripts/github-publish-sanitize.py create mode 100755 scripts/publish-github.sh diff --git a/.gitignore b/.gitignore index 48f45d1..fed4fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -35,3 +35,6 @@ skills/editorial-image/ /docs/4-guides/ /docs/5-records/ /docs/6-user/ + +# GitHub public mirror 產出(本機暫存,不進私有真相源) +.github-public/ diff --git a/scripts/github-publish-exclude.txt b/scripts/github-publish-exclude.txt new file mode 100644 index 0000000..c61b4d4 --- /dev/null +++ b/scripts/github-publish-exclude.txt @@ -0,0 +1,34 @@ +# github-publish-exclude.txt — 公開 mirror 要濾掉的路徑(一行一個,相對 repo 根) +# +# 原則(leo 2026-07-21): +# 「應該要給的是一個適合別人用的**濃縮結果,不需要有過程**。 +# 以後我還是在私有的 template 工作,但 publish 到 GitHub 就把它當**貼文**。」 +# +# → GitHub = 給別人用的成品櫥窗;Gitea(未來 CF 版)= 我們的工作現場。 +# 凡屬「我們怎麼做出來的」一律不出去;凡屬「別人怎麼用」一律留下。 + +# ── 我們自己的工作現場(過程,不是成品)── +# 本 repo 自用的 wiki/SDD/內部決策紀錄。別人裝了 template 會長出他自己的, +# 不需要看我們的內容(且含真實事故、人名、內部帳號脈絡)。 +system-dev + +# 本 repo 自用的 CC 設定與 hook 註冊(含本機路徑、內部 guard)。 +# 注意:template/.claude/ 是「要發給別人的樣板」,不在此列,照樣公開。 +.claude + +# 開發過程文件(設計討論、內部指南) +docs + +# 我們自己給 CC 的工作指示(含內部鐵律與事故引用) +CLAUDE.md + +# 內部發版沿革(含事故經過與人名對話)——公開端改用 README 的「更新重點」概述 +CHANGELOG.md + +# 發佈管線本身不需要出現在成品裡 +scripts/publish-github.sh +scripts/github-publish-exclude.txt +scripts/github-publish-sanitize.py + +# mirror 產出目錄(若曾在本機產生) +.github-public diff --git a/scripts/github-publish-sanitize.py b/scripts/github-publish-sanitize.py new file mode 100755 index 0000000..afc025b --- /dev/null +++ b/scripts/github-publish-sanitize.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +"""github-publish-sanitize.py — 對匯出樹做內容級改寫,讓公開版真的能被別人使用。 + +由 publish-github.sh 自動呼叫(步驟 3.5),參數=匯出樹的暫存目錄。 + +為什麼需要(leo 2026-07-21): + 「我不想給它 Gitea 網址,因為我們已經計劃要把 gitea 改用 CF 版本了。」 + + install.sh / update.sh 內建的抓取來源指向我們的 **private Gitea**: + - 別人抓不到(private)→ 裝不起來 + - 且該網址即將換成 CF 版 → 公開出去的網址馬上失效 + + → 公開版一律改指 GitHub raw。私有工作區維持 Gitea 不動, + 兩邊不必手工維護兩份(改寫發生在發佈當下)。 +""" +import pathlib +import re +import sys + +GITEA_BASE = "https://git.uncle6.me/Leo/system-dev-template/raw/branch/main" +GITHUB_BASE = "https://raw.githubusercontent.com/youlinhsieh/system-dev-template/main" + +# 純文字取代(來源網址)。放這裡的規則要「冪等」——重跑不會壞。 +REPLACEMENTS = [ + (GITEA_BASE, GITHUB_BASE), + # 保險:任何殘留的 Gitea 主機名(例如註解、文件裡的說明連結) + ("https://git.uncle6.me/Leo/system-dev-template", "https://github.com/youlinhsieh/system-dev-template"), + # 🔴 舊 GitHub 帳號 uncle6me-web **已被 suspend**:README 的安裝指令仍指向它 + # → 別人照著跑會抓不到(2026-07-20 已在 update.sh 撞過同一顆雷)。 + ("raw.githubusercontent.com/uncle6me-web/", "raw.githubusercontent.com/youlinhsieh/"), + ("github.com/uncle6me-web/", "github.com/youlinhsieh/"), + # 文件註解裡拿舊帳號當範例 → 對外改通用佔位符(別人看到我們的帳號名沒意義) + ("(例:uncle6me-web)", "(例:your-github-account)"), + ("(e.g. uncle6me-web)", "(e.g. your-github-account)"), +] + +TEXT_SUFFIXES = {".sh", ".md", ".json", ".py", ".yaml", ".yml", ".txt"} + + +def main(root_arg: str) -> int: + root = pathlib.Path(root_arg) + if not root.is_dir(): + print(f"❌ sanitize:找不到匯出樹 {root}", file=sys.stderr) + return 1 + + changed = [] + for path in root.rglob("*"): + if not path.is_file() or path.suffix not in TEXT_SUFFIXES: + continue + try: + original = path.read_text(encoding="utf-8") + except (UnicodeDecodeError, OSError): + continue + + text = original + for old, new in REPLACEMENTS: + text = text.replace(old, new) + + if text != original: + path.write_text(text, encoding="utf-8") + changed.append(str(path.relative_to(root))) + + if changed: + print(f"🧼 sanitize:改寫來源網址 → GitHub({len(changed)} 檔)") + for c in changed: + print(f" {c}") + else: + print("🧼 sanitize:無需改寫") + + # 驗證:公開樹不得殘留「別人抓不到的來源」——漏了就是別人裝不起來。 + # ① git.uncle6.me = 我們的 private Gitea(且即將換 CF 版) + # ② uncle6me-web = 已被 suspend 的舊 GitHub 帳號 + BAD_HOSTS = ("git.uncle6.me", "uncle6me-web") + leaked = [] + for path in root.rglob("*"): + if not path.is_file() or path.suffix not in TEXT_SUFFIXES: + continue + try: + content = path.read_text(encoding="utf-8") + if any(bad in content for bad in BAD_HOSTS): + leaked.append(str(path.relative_to(root))) + except (UnicodeDecodeError, OSError): + continue + + if leaked: + print("❌ sanitize:公開樹仍殘留失效來源(Gitea/suspend 帳號),中止發佈:", file=sys.stderr) + for f in leaked: + print(f" {f}", file=sys.stderr) + return 1 + + return 0 + + +if __name__ == "__main__": + sys.exit(main(sys.argv[1] if len(sys.argv) > 1 else ".")) diff --git a/scripts/publish-github.sh b/scripts/publish-github.sh new file mode 100755 index 0000000..4611677 --- /dev/null +++ b/scripts/publish-github.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# publish-github.sh — 產生過濾後的公開樹,維護 GitHub public mirror(乾淨歷史) +# +# 模型(D22 拍板):Gitea = 私有真相源(全量,含內部 wiki/SDD) +# GitHub = 公開櫥窗(過濾樹 + 每次發版一個 release commit,不帶內部歷史) +# 流量紅線(D20):push 前需 leo 親跑 github-arm.sh 解保險;低頻手動、單 repo、不掛 Actions。 +# +# 用法: +# scripts/publish-github.sh # 只建/更新本機 mirror(.github-public/),不 push +# scripts/publish-github.sh --push # 另加 push(需 env GITHUB_REMOTE=https://github.com/<帳號>/.git) +# +# 排除清單:scripts/github-publish-exclude.txt(一行一個路徑,相對 repo 根;# 開頭為註解) +set -euo pipefail + +REPO_ROOT="$(git rev-parse --show-toplevel)" +EXCLUDE_FILE="$REPO_ROOT/scripts/github-publish-exclude.txt" +MIRROR_DIR="${MIRROR_DIR:-$REPO_ROOT/.github-public}" +GITHUB_REMOTE="${GITHUB_REMOTE:-}" + +TMP_EXPORT="$(mktemp -d)" +trap 'rm -rf "$TMP_EXPORT"' EXIT + +# 1) 只匯出 HEAD 的 tracked 檔(.env / credentials 等 untracked 永不混入) +git -C "$REPO_ROOT" archive HEAD | tar -x -C "$TMP_EXPORT" + +# 2) LFS 檔用實體內容取代 pointer(公開端不吃 LFS) +if command -v git-lfs >/dev/null 2>&1; then + git -C "$REPO_ROOT" lfs ls-files --name-only 2>/dev/null | while IFS= read -r f; do + if [ -n "$f" ] && [ -f "$REPO_ROOT/$f" ]; then + mkdir -p "$TMP_EXPORT/$(dirname "$f")" + cp "$REPO_ROOT/$f" "$TMP_EXPORT/$f" + fi + done +fi + +# 2.5) 公開端不吃 LFS:刪掉 .gitattributes,否則 mirror commit 時 git-lfs 會把 +# 實體檔又轉回 pointer 上傳(GitHub README 圖就破了——2026-07-18 實撞) +rm -f "$TMP_EXPORT/.gitattributes" + +# 3) 套排除清單 +while IFS= read -r p; do + case "$p" in ''|\#*) continue ;; esac + rm -rf "${TMP_EXPORT:?}/$p" +done < "$EXCLUDE_FILE" + +# 3.5) repo 自備的遮蔽腳本(可選):對匯出樹做內容級遮蔽(如指南去帳密) +if [ -f "$REPO_ROOT/scripts/github-publish-sanitize.py" ]; then + # sanitize 失敗=公開樹仍含「別人抓不到的來源」或未遮蔽內容。 + # 必須中止:原版沒擋 → 會把未淨化的樹 rsync 進 mirror,下次 --push 就送出去了。 + if ! python3 "$REPO_ROOT/scripts/github-publish-sanitize.py" "$TMP_EXPORT"; then + echo "❌ sanitize 未通過 → 中止發佈(mirror 未被更動)" >&2 + exit 1 + fi +fi + +# 4) 同步進常駐 mirror(mirror 自己的 .git = 公開端乾淨歷史) +mkdir -p "$MIRROR_DIR" +[ -d "$MIRROR_DIR/.git" ] || git -C "$MIRROR_DIR" init -q -b main +rsync -a --delete --exclude='.git' "$TMP_EXPORT/" "$MIRROR_DIR/" + +cd "$MIRROR_DIR" +git add -A +if git diff --cached --quiet && git rev-parse -q --verify HEAD >/dev/null; then + echo "ℹ️ 無變更,mirror 已是最新" +else + git -c user.name="Arcrun Release" -c user.email="release@arcrun.dev" \ + commit -q -m "release: snapshot $(git -C "$REPO_ROOT" rev-parse --short HEAD) ($(git -C "$REPO_ROOT" log -1 --format=%cd --date=short))" + echo "✅ mirror 已更新:$(git log --oneline -1)" +fi +echo "📁 mirror:${MIRROR_DIR}($(git rev-list --count HEAD) 個公開 commit)" + +if [ "${1:-}" = "--push" ]; then + if [ -z "$GITHUB_REMOTE" ]; then + echo "❌ 缺 GITHUB_REMOTE(例:https://github.com/<帳號>/.git)" >&2 + exit 1 + fi + git remote remove github 2>/dev/null || true + git remote add github "$GITHUB_REMOTE" + # 憑證不進 URL/指令行:有 GITHUB_MIRROR_TOKEN 就用 Basic header 注入(PAT) + if [ -n "${GITHUB_MIRROR_TOKEN:-}" ]; then + AUTH_B64="$(printf '%s:%s' "${GITHUB_ACCOUNT_NAME:-git}" "$GITHUB_MIRROR_TOKEN" | base64 | tr -d '\n')" + git -c http."$GITHUB_REMOTE".extraheader="Authorization: Basic $AUTH_B64" push -u github main + else + git push -u github main + fi + echo "🚀 已 push → $GITHUB_REMOTE" +fi