feat(assistant): 存取庫掃描接上 {repo} 代入點
新增 tools/scan-repos.sh:掃出工作目錄底下的存取庫,交出 {repo} 要代的
那份清單,順便對出每一個存取庫的 REPO_{HASH} 盤點頁頁名(雜湊規則與
盤點頁那一支相同,實測值一致)。
掃描起點刻意沒有預設值。給一個預設就等於猜,而猜錯的後果不是掃不到,
是在猜錯的那些目錄底下跑指令——往上一層是家目錄、再往上是整台機器,
而那一輪沒有人看得到它跑到哪裡去了。沒設就回 3 並說要設哪一個變數。
深度預設一層、上限三層;點開頭的目錄、符號連結、路徑帶空白或殼層特殊
字元的三種一律不交出去,但逐個印出來——那是真的存在卻沒被盤點到的
存取庫,只印一個總數會讀成整批都掃過了。
run-due.sh 改成兩個代入點都代:{cli} 取偵測到的 CLI 代號、{repo} 取
掃到的存取庫,兩個都帶的那一筆目標數是乘積。待辦簿那一筆的 repo 欄
有值時只代那一個,值可以是路徑也可以是 {owner}/{repo};對不出來就
印 held=,不退回全部存取庫——那一筆指名了一個目標。代入之後再驗一次
禁止字元,因為代進去的值是這一輪現場算出來的。
schedule.sh 把 JSC_ASSIST_SCAN_ROOT 與 JSC_ASSIST_SCAN_EXCLUDE 快照進
排程條目。少了掃描起點,帶 {repo} 的內建項每一輪都代不出目標,而那一輪
只印一行 held=,看起來像這一批還沒接上、不像一個變數沒進條目。
同時補兩個只有真的執行才發現得了的缺陷:
一、cron 一行的長度上限只在寫入那一刻由 crontab 自己擋,--dry-run 那一路
根本不碰它,所以預演每一次都過。實測踩過一次:整條 PATH 快照進條目那一批
預演全綠、安裝回結束碼 4。改成建好條目就量,兩個模式都印 entry_len= 與
上限,達到上限當場拒絕並指出長度是從哪幾個快照變數來的。
二、run-due.sh 截錯誤訊息用的 cut -c 數的是位元組不是字元,中文字剛好被
截在中間就在報告上留下一個替代字元。改成截完再過一次 iconv -c 丟掉那個
不完整的序列。這一種不會有人來報:亂碼不影響結束碼。
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
+122
-24
@@ -46,12 +46,16 @@
|
||||
#
|
||||
# --- 代不出目標不算失敗 ---
|
||||
#
|
||||
# {cli} 由偵測到的 CLI 代號代入,{repo} 由掃到的存取庫工作目錄代入。後者的掃描還沒做出來,
|
||||
# 所以帶 {repo} 的那幾筆這一輪代不出目標。處置是印一行 held= 就跳過,**不動那一筆的
|
||||
# last_run,也不加失敗次數**。
|
||||
# 那一筆沒有做錯任何事:代不出目標是這一支還缺一塊,記成失敗會讓一個沒有人修得動的計數
|
||||
# {cli} 由偵測到的 CLI 代號代入,{repo} 由 scan-repos.sh 掃到的存取庫工作目錄代入。
|
||||
# 兩個代入點都代得出來才跑;任一個代不出來就印一行 held= 跳過,**不動那一筆的 last_run,
|
||||
# 也不加失敗次數**。
|
||||
# 那一筆沒有做錯任何事:代不出目標是環境還缺一塊,記成失敗會讓一個沒有人修得動的計數
|
||||
# 一路往上爬,而那個計數存在的理由是指出「有一筆壞掉的項目每輪重試而沒人知道」。
|
||||
# 把「還沒接上」記成「壞掉」,等於用假的壞掉把真的壞掉蓋掉。
|
||||
#
|
||||
# {repo} 代不出來最常見的成因是 JSC_ASSIST_SCAN_ROOT 沒設。那不是預設值漏填,是掃描那一支
|
||||
# 刻意不猜:猜錯掃描起點的後果是在猜錯的那些目錄底下跑指令,而那一輪沒有人看得到它跑到
|
||||
# 哪裡去了。所以這裡照它回的話原樣記進 held= 的理由欄,人才看得出要去設哪一個變數。
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
@@ -60,6 +64,16 @@ usage() {
|
||||
}
|
||||
|
||||
die() { _c=$1; shift; printf '[jsc][助理執行][ERR]:%s\n' "$*" >&2; exit "$_c"; }
|
||||
|
||||
# 取第一行、截到指定長度。cut 數的是位元組不是字元,中文字剛好被切成一半時尾巴就變成
|
||||
# 替代字元——實測踩到:一行中文錯誤訊息截在第 200 個位元組,報告上留下一個亂碼字。
|
||||
# 截完再過一次 iconv -c 把那個不完整的序列丟掉。iconv 不在這台機器上就退回原樣,
|
||||
# 不自己拼一套 UTF-8 邊界判定:那是在錯誤訊息這條路上加一個會出錯的新東西。
|
||||
clip() { # $1=位元組上限;讀標準輸入
|
||||
_raw=$(head -n1 | cut -c"1-$1")
|
||||
_cln=$(printf '%s' "$_raw" | iconv -c -f UTF-8 -t UTF-8 2>/dev/null)
|
||||
if [ -n "$_cln" ]; then printf '%s' "$_cln"; else printf '%s' "$_raw"; fi
|
||||
}
|
||||
note() { printf '[jsc][助理執行]:%s\n' "$*" >&2; }
|
||||
warn() { printf '[jsc][助理執行][WARN]:%s\n' "$*" >&2; }
|
||||
|
||||
@@ -186,9 +200,52 @@ if [ -n "${JSC_ASSIST_CLIS:-}" ]; then
|
||||
done
|
||||
fi
|
||||
|
||||
# {repo} 的代入來源還沒做出來。這裡不猜一個掃描規則頂替:猜錯就是在整台機器上跑指令,
|
||||
# 而那一輪沒有人看得到它跑到哪裡去了。
|
||||
REPO_READY=0
|
||||
# --- {repo} 的代入來源 ---
|
||||
|
||||
# 掃到的存取庫工作目錄,一行一條。掃描規則、掃描起點與排除清單全在 scan-repos.sh 那一支,
|
||||
# 這裡不自己再判一次:兩邊各有一套掃描規則就會漂移,而漂移的那一天沒有人會收到通知。
|
||||
# 兩份:REPOS 是路徑清單,給沒綁存取庫的那幾筆代成每一個目標;REPOMAP 多帶一欄
|
||||
# {owner}/{repo},給綁了存取庫的那幾筆對出它綁的是哪一條路徑。
|
||||
REPOS="$TMPD/repos.txt"
|
||||
REPOMAP="$TMPD/repomap.txt"
|
||||
: >"$REPOS"; : >"$REPOMAP"
|
||||
REPO_WHY=''
|
||||
REPO_SCAN='-'
|
||||
SCAN_SH="$HERE/scan-repos.sh"
|
||||
[ -f "$SCAN_SH" ] || SCAN_SH=$(find_tool assist tools/scan-repos.sh) || SCAN_SH=''
|
||||
if [ -z "$SCAN_SH" ]; then
|
||||
REPO_WHY='找不到 scan-repos.sh,{repo} 沒有代入來源'
|
||||
REPO_SCAN='missing'
|
||||
else
|
||||
"$SCAN_SH" list --root "$ROOT" >"$TMPD/scan.out" 2>"$TMPD/scan.err"
|
||||
_src=$?
|
||||
REPO_SCAN="rc$_src"
|
||||
case "$_src" in
|
||||
# 0 是全部都對得出盤點頁頁名,1 是有幾個對不出來或路徑被拒——後者交出來的那幾條照用,
|
||||
# 對不出頁名不影響唯讀盤點跑得動。其餘各碼一律當成沒有來源,並把它印的理由帶上。
|
||||
0|1)
|
||||
sed -n 's/^repo=\([^ ]*\) slug=\([^ ]*\).*/\1'"$TAB"'\2/p' "$TMPD/scan.out" >"$REPOMAP"
|
||||
cut -f1 "$REPOMAP" >"$REPOS" ;;
|
||||
*) REPO_WHY=$(sed -n 's/^\[jsc\]\[助理掃描\]\[ERR\]://p' "$TMPD/scan.err" 2>/dev/null | clip 200)
|
||||
[ -n "$REPO_WHY" ] || REPO_WHY="存取庫掃描回結束碼 $_src,{repo} 代不出目標" ;;
|
||||
esac
|
||||
# 掃描那一支的逐項判定行被導進暫存檔,所以外面看不到。原樣轉出來:被拒的那幾個存取庫是
|
||||
# 這一輪「掃到但沒交出去」的名單,那份名單不能只留在暫存檔裡。
|
||||
sed -n 's/^\(rejected=\|unnamed=\|excluded=\|skipped_link=\)/repo_scan_note=\1/p' \
|
||||
"$TMPD/scan.out" "$TMPD/scan.err" 2>/dev/null
|
||||
fi
|
||||
N_REPOS=$(awk 'END{print NR+0}' "$REPOS")
|
||||
if [ -z "$REPO_WHY" ] && [ "$N_REPOS" -eq 0 ]; then
|
||||
REPO_WHY='存取庫掃描一個都沒掃到,{repo} 代不出目標'
|
||||
fi
|
||||
|
||||
# 這一筆綁的存取庫對出一條掃到的路徑。綁的值可以是工作目錄路徑,也可以是 {owner}/{repo}——
|
||||
# 待辦簿的欄位說明只寫「這一筆綁哪一個存取庫」,兩種寫法都有人會填,所以兩種都認。
|
||||
# 對不出來一律回失敗,不退回「全部存取庫」:那一筆指名了一個目標,代成全部就是跑了
|
||||
# 十九個沒有人要它跑的地方。
|
||||
repo_path_of() { # $1=綁定值
|
||||
awk -F"$TAB" -v want="$1" '$1==want || $2==want {print $1; found=1; exit} END{exit !found}' "$REPOMAP"
|
||||
}
|
||||
|
||||
# --- 判斷動作是哪一種 ---
|
||||
|
||||
@@ -221,6 +278,25 @@ cmd_shape_ok() { # $1=指令
|
||||
return 0
|
||||
}
|
||||
|
||||
# 把一個代入點換成清單裡的每一個值,一個值一行。清單是空的又剛好命中代入點,那一行就
|
||||
# 整行消失——所以呼叫端要先確定清單非空,別靠這裡回錯:目標數變成 0 的那一筆會被當成
|
||||
# 「沒有東西要跑」而算成功,而它其實一次都沒跑。
|
||||
expand_over() { # $1=代入點 $2=值清單檔 $3=輸入檔 $4=輸出檔
|
||||
: >"$4" 2>/dev/null || return 1
|
||||
while IFS= read -r _line; do
|
||||
[ -n "$_line" ] || continue
|
||||
case "$_line" in
|
||||
*"$1"*)
|
||||
while IFS= read -r _v; do
|
||||
[ -n "$_v" ] || continue
|
||||
printf '%s\n' "$(printf '%s' "$_line" | sed "s|$1|$_v|g")" >>"$4"
|
||||
done <"$2" ;;
|
||||
*) printf '%s\n' "$_line" >>"$4" ;;
|
||||
esac
|
||||
done <"$3"
|
||||
return 0
|
||||
}
|
||||
|
||||
# --- 逐筆處理 ---
|
||||
|
||||
N_DUE=0; N_RUN=0; N_OK=0; N_FAIL=0; N_HELD=0; N_SKIP=0; N_WRITE_BAD=0
|
||||
@@ -272,23 +348,42 @@ while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recu
|
||||
_targets="$TMPD/targets.$c_id"
|
||||
: >"$_targets" 2>/dev/null || die 5 "暫存檔寫不進去:$_targets。"
|
||||
_hold=''
|
||||
# 沒綁存取庫的那一筆代成掃到的每一個;綁了的只代那一個。
|
||||
_repovals="$REPOS"
|
||||
case "$c_action" in
|
||||
*'{repo}'*)
|
||||
[ "$REPO_READY" -eq 1 ] || _hold='存取庫掃描還沒做出來,{repo} 代不出目標' ;;
|
||||
if [ -n "$REPO_WHY" ]; then
|
||||
_hold="$REPO_WHY"
|
||||
else
|
||||
_bind=$(sed -n 's/^repo=//p' "$JSC_HOME_RESOLVED/assistant/tasks/$c_id" 2>/dev/null | head -n1)
|
||||
if [ -n "$_bind" ]; then
|
||||
if _bp=$(repo_path_of "$_bind"); then
|
||||
_repovals="$TMPD/repoval.$c_id"
|
||||
printf '%s\n' "$_bp" >"$_repovals" 2>/dev/null || die 5 "暫存檔寫不進去:$_repovals。"
|
||||
else
|
||||
_hold="這一筆綁的存取庫「$_bind」這一輪沒掃到,代不出目標"
|
||||
fi
|
||||
fi
|
||||
fi ;;
|
||||
esac
|
||||
case "$c_action" in
|
||||
*'{cli}'*) [ "$N_CLI" -gt 0 ] || _hold='這台機器偵測不到任何一支 CLI,{cli} 代不出目標' ;;
|
||||
esac
|
||||
if [ -z "$_hold" ]; then
|
||||
case "$c_action" in
|
||||
*'{cli}'*)
|
||||
if [ "$N_CLI" -eq 0 ]; then
|
||||
_hold='這台機器偵測不到任何一支 CLI,{cli} 代不出目標'
|
||||
else
|
||||
while IFS= read -r _c; do
|
||||
[ -n "$_c" ] || continue
|
||||
printf '%s\n' "$(printf '%s' "$c_action" | sed "s|{cli}|$_c|g")" >>"$_targets"
|
||||
done <"$CLIS"
|
||||
fi ;;
|
||||
*) printf '%s\n' "$c_action" >>"$_targets" ;;
|
||||
esac
|
||||
# 兩個代入點分兩段代,一段一個暫存檔。同一筆同時帶 {repo} 與 {cli} 時,目標數是兩邊
|
||||
# 的乘積——三個存取庫乘五支 CLI 就是十五個目標,一個目標跑一次。
|
||||
printf '%s\n' "$c_action" >"$TMPD/x0.$c_id"
|
||||
expand_over '{repo}' "$_repovals" "$TMPD/x0.$c_id" "$TMPD/x1.$c_id" \
|
||||
|| die 5 "暫存檔寫不進去:$TMPD/x1.$c_id。"
|
||||
expand_over '{cli}' "$CLIS" "$TMPD/x1.$c_id" "$_targets" \
|
||||
|| die 5 "暫存檔寫不進去:$_targets。"
|
||||
# 代入之後再驗一次禁止字元。種入那一支與上面的 cmd_shape_ok 驗的是還沒代入的字面,
|
||||
# 代進去的值是這一輪現場算出來的——路徑與 CLI 代號都有可能帶進新的字元,而帶進來的
|
||||
# 那一刻就是送進殼的前一刻。
|
||||
if grep -q '[$`;|&~]' "$_targets" 2>/dev/null; then
|
||||
_hold='代入之後出現金錢符號、反引號、分號、管線、連接符號或波浪號,代入來源有問題'
|
||||
: >"$_targets"
|
||||
fi
|
||||
fi
|
||||
if [ -n "$_hold" ]; then
|
||||
N_HELD=$((N_HELD + 1))
|
||||
@@ -323,9 +418,9 @@ while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recu
|
||||
printf 'target_ok=%s rc=0 cmd=%s\n' "$c_id" "$_t"
|
||||
else
|
||||
_entry_rc=1
|
||||
[ -n "$_first_err" ] || _first_err=$(printf '%s' "$_out" | head -n1 | cut -c1-160)
|
||||
[ -n "$_first_err" ] || _first_err=$(printf '%s' "$_out" | clip 160)
|
||||
printf 'target_fail=%s rc=%s cmd=%s detail=%s\n' \
|
||||
"$c_id" "$_rc" "$_t" "$(printf '%s' "$_out" | head -n1 | cut -c1-160)"
|
||||
"$c_id" "$_rc" "$_t" "$(printf '%s' "$_out" | clip 160)"
|
||||
fi
|
||||
done <"$_targets"
|
||||
|
||||
@@ -362,10 +457,13 @@ while IFS="$TAB" read -r c_id c_verdict c_state c_kind c_action c_trigger c_recu
|
||||
fi
|
||||
done <"$ROWS"
|
||||
|
||||
printf 'mode=%s rows=%s root=%s due=%s ran=%s ok=%s failed=%s held=%s skipped=%s write_failed=%s clis=%s cli_missing=%s\n' \
|
||||
printf 'mode=%s rows=%s root=%s due=%s ran=%s ok=%s failed=%s held=%s skipped=%s write_failed=%s clis=%s cli_missing=%s repos=%s repo_scan=%s\n' \
|
||||
"$([ "$DRYRUN" -eq 1 ] && echo plan || echo run)" "$ROWS" "${ROOT:--}" \
|
||||
"$N_DUE" "$N_RUN" "$N_OK" "$N_FAIL" "$N_HELD" "$N_SKIP" "$N_WRITE_BAD" "$N_CLI" "${CLI_MISSING:--}"
|
||||
"$N_DUE" "$N_RUN" "$N_OK" "$N_FAIL" "$N_HELD" "$N_SKIP" "$N_WRITE_BAD" "$N_CLI" "${CLI_MISSING:--}" \
|
||||
"$N_REPOS" "$REPO_SCAN"
|
||||
|
||||
[ -n "$REPO_WHY" ] && note "存取庫清單這一輪是空的:$REPO_WHY。帶 {repo} 代入點的那幾筆全部跳過,逐筆印在上面的 held= 那幾行。"
|
||||
[ "$REPO_SCAN" = 'rc1' ] && note "存取庫掃描回 1:掃到的存取庫裡有幾個對不出 REPO_{HASH} 盤點頁頁名,或路徑帶了空白與殼層特殊字元而被拒。交出來的那幾條照用,被拒的那幾個這一輪沒有被盤點到——理由逐個印在掃描那一支的 rejected= 與 unnamed= 那幾行。"
|
||||
[ -n "$CLI_MISSING" ] && warn "排程條目記著這台機器有 $JSC_ASSIST_CLIS,但這一輪只偵測到 $N_CLI 支,少了:$CLI_MISSING。帶 {cli} 代入點的那幾筆這一輪少跑了那幾支,而且**全部成功也不代表全部跑過**。成因通常是那支 CLI 裝在會過期的目錄底下(例如帶行程編號的多殼層目錄),條目帶的 PATH 是安裝當下拍的,那條路徑現在不在了。重跑一次 schedule.sh install patrol 會重新拍一份快照;那支 CLI 反覆消失就要考慮把它裝到穩定位置。"
|
||||
[ "$N_HELD" -gt 0 ] && note "有 $N_HELD 筆代不出目標或指令形狀不對,這一輪跳過,逐筆印在上面的 held= 那幾行。**那幾筆的執行紀錄與失敗次數一個字都沒動**——代不出目標不是那一筆做錯了什麼,記成失敗會讓一個沒有人修得動的計數一路往上爬。"
|
||||
[ "$N_SKIP" -gt 0 ] && note "有 $N_SKIP 筆這一批不跑:動作是只提醒的、動作是技能名的、還有不是內建項的,逐筆印在上面的 skip= 那幾行。"
|
||||
|
||||
Executable
+273
@@ -0,0 +1,273 @@
|
||||
#!/usr/bin/env sh
|
||||
# scan-repos.sh — 掃出工作目錄底下的存取庫,交出 {repo} 代入點要用的那份清單。
|
||||
#
|
||||
# 用法:
|
||||
# scan-repos.sh list [--scan-root {字面絕對路徑}] [--depth {0..3}] [--root {字面絕對根目錄}]
|
||||
# scan-repos.sh paths [同上]
|
||||
#
|
||||
# list 一行一個存取庫,帶工作目錄、{owner}/{repo} 與對應的 REPO_{HASH} 頁名
|
||||
# paths 只印工作目錄,一行一條。給 {repo} 代入點直接讀
|
||||
#
|
||||
# 環境變數:
|
||||
# JSC_ASSIST_SCAN_ROOT 掃描起點。--scan-root 優先於它
|
||||
# JSC_ASSIST_SCAN_EXCLUDE 排除清單,空白分隔,比對目錄名(不是整條路徑),可用萬用字元
|
||||
# JSC_ASSIST_HASH_ID hash-id 路徑覆寫;找不到並排存取庫時才設
|
||||
#
|
||||
# 結束碼:
|
||||
# 0 掃到至少一個存取庫,而且每一個都對得出 REPO_{HASH}、路徑也都送得進殼
|
||||
# 1 掃到了,但有幾個對不出頁名或路徑被拒。可用的那幾個照印,被拒的逐行印在 rejected=
|
||||
# 2 用法錯誤:不認得的子命令或選項、選項缺值、--scan-root 不是絕對路徑、--depth 超出範圍
|
||||
# 3 掃描起點沒設定,或那條路徑不是存在的目錄——**什麼都沒掃**,不等於「這台機器沒有存取庫」
|
||||
# 4 掃過了,一個 git 存取庫都沒有
|
||||
#
|
||||
# --- 為什麼掃描起點不給預設值 ---
|
||||
#
|
||||
# 這份清單的用途是把存取庫路徑代進待辦簿的指令,然後在無人值守那一輪送進殼執行。
|
||||
# 猜錯掃描起點的後果不是掃不到東西,是**在猜錯的那些目錄底下跑指令**,而那一輪沒有人
|
||||
# 看得到它跑到哪裡去了。往上猜一層就從工作目錄變成家目錄,再往上就是整台機器。
|
||||
# 所以沒設就回結束碼 3 並說清楚要設哪一個變數,不退回任何一條路徑。
|
||||
# 排程那一輪的值由 schedule.sh install 當下從殼裡快照進條目,跟 wiki 存取庫那幾個變數同一條路。
|
||||
#
|
||||
# --- 為什麼預設只掃一層 ---
|
||||
#
|
||||
# 工作目錄的版面是「一個存取庫一個子目錄」,一層就夠。往下遞歸會踩到三種東西:
|
||||
# 存取庫自己的 node_modules 與 vendor 底下的第三方存取庫、封存目錄裡的舊版存取庫、
|
||||
# 還有 worktree 的複本。三種都會被當成現役存取庫盤點,而盤點結果讀起來完全正常。
|
||||
# --depth 最多收到 3:真的有嵌套版面時夠用,而再深就不是「工作目錄那一層」了,
|
||||
# 那種版面該把掃描起點指到那一層,不是把界線調鬆。
|
||||
#
|
||||
# --- 三種東西一定跳過,不看排除清單 ---
|
||||
#
|
||||
# 一、名稱以點開頭的目錄。快取、封存、暫存都藏在那裡,掃進去會把封存版當現役版盤點。
|
||||
# 二、符號連結。連結會把掃描帶出掃描起點,而那條界線一旦被繞過就沒有第二道。
|
||||
# 三、路徑裡有空白或殼層特殊字元的存取庫。執行那一支是用 `sh -c` 送出指令的,那條路徑
|
||||
# 會被殼再解一次——帶空白的路徑會被切成兩個參數,帶金錢符號的會被展開成別的東西。
|
||||
# 這種一律列進 rejected= 不交出去,不是靜靜跳過:那是一個真的存在的存取庫,
|
||||
# 它沒有被盤點到這件事要有人知道。
|
||||
set -u
|
||||
|
||||
usage() {
|
||||
echo 'usage: scan-repos.sh {list|paths} [--scan-root 絕對路徑] [--depth 0..3] [--root 絕對路徑]' >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
die() { _c=$1; shift; printf '[jsc][助理掃描][ERR]:%s\n' "$*" >&2; exit "$_c"; }
|
||||
# 逐項的判定行與收尾統計。paths 那個模式的標準輸出要是純路徑——呼叫端拿它當代入點來源,
|
||||
# 混一行 unnamed= 進去就會被代成一條不存在的路徑然後送進殼。所以那個模式一律改走標準錯誤。
|
||||
emit() { if [ "$MODE" = 'paths' ]; then printf '%s\n' "$*" >&2; else printf '%s\n' "$*"; fi; }
|
||||
note() { printf '[jsc][助理掃描]:%s\n' "$*" >&2; }
|
||||
warn() { printf '[jsc][助理掃描][WARN]:%s\n' "$*" >&2; }
|
||||
|
||||
[ "$#" -ge 1 ] || usage
|
||||
MODE=$1; shift
|
||||
case "$MODE" in
|
||||
list|paths) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
SCAN_ROOT=''
|
||||
OPT_ROOT=''
|
||||
DEPTH=1
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--scan-root) [ "$#" -ge 2 ] || usage; SCAN_ROOT=$2; shift 2 ;;
|
||||
--root) [ "$#" -ge 2 ] || usage; OPT_ROOT=$2; shift 2 ;;
|
||||
--depth) [ "$#" -ge 2 ] || usage; DEPTH=$2; shift 2 ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$DEPTH" in
|
||||
0|1|2|3) ;;
|
||||
*) die 2 "--depth 只收 0 到 3,收到的是「$DEPTH」。0 只看掃描起點自己,1 是它底下那一層。要掃更深的版面就把掃描起點指到那一層。" ;;
|
||||
esac
|
||||
|
||||
case "$OPT_ROOT" in
|
||||
''|/*) ;;
|
||||
*) die 2 "--root 要給字面絕對路徑,收到的是「$OPT_ROOT」。" ;;
|
||||
esac
|
||||
|
||||
[ -n "$SCAN_ROOT" ] || SCAN_ROOT="${JSC_ASSIST_SCAN_ROOT:-}"
|
||||
if [ -z "$SCAN_ROOT" ]; then
|
||||
die 3 '掃描起點沒設定。這一支不猜:猜錯的後果是在猜錯的那些目錄底下跑指令,而無人值守那一輪沒有人看得到它跑到哪裡去了。請帶 --scan-root,或在殼裡設 JSC_ASSIST_SCAN_ROOT 之後重跑一次 schedule.sh install patrol,把值快照進排程條目。'
|
||||
fi
|
||||
case "$SCAN_ROOT" in
|
||||
/*) ;;
|
||||
*) die 2 "掃描起點要是字面絕對路徑,收到的是「$SCAN_ROOT」。權限層比對的是還沒展開的指令字面,帶變數或波浪號的路徑進不了允許清單。" ;;
|
||||
esac
|
||||
SCAN_ROOT=${SCAN_ROOT%/}
|
||||
# 去掉尾斜線之後空字串就是根目錄。掃根目錄那一層等於把整台機器的第一層當成工作目錄,
|
||||
# 那不是設定錯誤就是打錯字,兩種都不該照著跑。
|
||||
[ -n "$SCAN_ROOT" ] || die 2 '掃描起點是根目錄。掃根目錄那一層等於把整台機器的第一層當成工作目錄,這一支不接。'
|
||||
[ -d "$SCAN_ROOT" ] || die 3 "掃描起點不是存在的目錄:$SCAN_ROOT。什麼都沒掃,跟「這台機器沒有存取庫」不是同一件事——後者要回 4。"
|
||||
|
||||
TAB=$(printf '\t')
|
||||
TMPD=$(mktemp -d 2>/dev/null) || die 3 '暫存目錄建不起來。'
|
||||
trap 'rm -rf "$TMPD"' EXIT
|
||||
|
||||
# --- 找 hash-id ---
|
||||
|
||||
HERE=$(CDPATH= cd -P -- "$(dirname -- "$0")" && pwd -P)
|
||||
ROOT="$OPT_ROOT"
|
||||
[ -n "$ROOT" ] || ROOT=$(CDPATH= cd -- "$HERE/../.." 2>/dev/null && pwd -L) || ROOT=''
|
||||
|
||||
hash_id_sh() {
|
||||
if [ -n "${JSC_ASSIST_HASH_ID:-}" ] && [ -f "$JSC_ASSIST_HASH_ID" ]; then
|
||||
printf '%s' "$JSC_ASSIST_HASH_ID"; return 0
|
||||
fi
|
||||
for _d in "jsc-gitea" "gitea"; do
|
||||
[ -n "$ROOT" ] && [ -f "$ROOT/$_d/tools/hash-id" ] && { printf '%s' "$ROOT/$_d/tools/hash-id"; return 0; }
|
||||
done
|
||||
return 1
|
||||
}
|
||||
HASH_ID=$(hash_id_sh) || HASH_ID=''
|
||||
|
||||
# 頁名的雜湊規則跟 jsc-sdlc 盤點頁是同一條:對那個存取庫自己的 {owner}/{repo} 取完整四十碼
|
||||
# 大寫 SHA-1。共用那一支找不到就退回本機的 sha1sum——同一條規則、同一個值,只是少一層共用。
|
||||
# 兩邊都算不出來就不硬湊一個頁名:湊出來的頁名指向沒有人讀的那一頁。
|
||||
#
|
||||
# 用哪一種先決定好,不在每一筆裡各判一次。算雜湊那一段跑在命令替換裡,也就是子殼裡,
|
||||
# 在那裡設「這一輪用了退路」的旗標,回到外面就沒了——那句提示會永遠印不出來。
|
||||
HASH_MODE='none'
|
||||
if [ -n "$HASH_ID" ] && [ -n "$(printf '%s' probe | "$HASH_ID" 2>/dev/null)" ]; then
|
||||
HASH_MODE='shared'
|
||||
elif command -v sha1sum >/dev/null 2>&1; then
|
||||
HASH_MODE='sha1sum'
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
HASH_MODE='shasum'
|
||||
fi
|
||||
hash40() { # $1=要算的字串
|
||||
case "$HASH_MODE" in
|
||||
shared) printf '%s' "$1" | "$HASH_ID" 2>/dev/null ;;
|
||||
sha1sum) printf '%s' "$1" | sha1sum | awk '{print toupper($1)}' ;;
|
||||
shasum) printf '%s' "$1" | shasum -a 1 | awk '{print toupper($1)}' ;;
|
||||
*) return 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# --- 找候選目錄 ---
|
||||
|
||||
# 排除清單比對目錄名,不比對整條路徑:整條路徑會隨掃描起點變,同一份清單換一台機器就失效。
|
||||
EXCLUDED_NAMES="${JSC_ASSIST_SCAN_EXCLUDE:-}"
|
||||
excluded() { # $1=目錄名
|
||||
[ -n "$EXCLUDED_NAMES" ] || return 1
|
||||
for _pat in $EXCLUDED_NAMES; do
|
||||
# shellcheck disable=SC2254
|
||||
case "$1" in
|
||||
$_pat) return 0 ;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
CAND="$TMPD/cand.txt"
|
||||
: >"$CAND"
|
||||
LINKS="$TMPD/links.txt"
|
||||
: >"$LINKS"
|
||||
|
||||
# find 預設不跟符號連結走,所以連結目錄不會出現在 -type d 的結果裡。這裡另外撈一次 -type l
|
||||
# 是為了把它們印出來:一個被跳過的連結跟一個不存在的目錄,在報告上看起來不能是同一件事。
|
||||
#
|
||||
# 排除清單不寫進 find 的剪枝條件,改在下面逐個判定時濾掉。理由是剪掉的目錄就不會出現在
|
||||
# 結果裡,也就報不出「這個被排除了」——而一份沒說自己排除了什麼的清單,讀起來跟沒有排除
|
||||
# 清單一樣。代價是深度兩層以上時會走進被排除的目錄裡再一層,而深度上限是 3,所以最多一層。
|
||||
if [ "$DEPTH" -ge 1 ]; then
|
||||
find "$SCAN_ROOT" -mindepth 1 -maxdepth "$DEPTH" \
|
||||
-name '.*' -prune -o -type l -print 2>/dev/null >"$LINKS"
|
||||
find "$SCAN_ROOT" -mindepth 1 -maxdepth "$DEPTH" \
|
||||
-name '.*' -prune -o -type d -print 2>/dev/null >"$CAND"
|
||||
fi
|
||||
# depth 0 與更深的版面都要把掃描起點自己算進來:工作目錄本身也可能就是一個存取庫。
|
||||
printf '%s\n' "$SCAN_ROOT" >>"$CAND"
|
||||
sort -u -o "$CAND" "$CAND"
|
||||
|
||||
# --- 逐個判定 ---
|
||||
|
||||
N_REPO=0; N_REJ=0; N_UNNAMED=0; N_EXCL=0; N_LINK=0
|
||||
OUT="$TMPD/out.txt"
|
||||
: >"$OUT"
|
||||
|
||||
while IFS= read -r d; do
|
||||
[ -n "$d" ] || continue
|
||||
_name=$(basename -- "$d")
|
||||
if excluded "$_name"; then
|
||||
N_EXCL=$((N_EXCL + 1))
|
||||
emit "excluded=$d reason=在排除清單上"
|
||||
continue
|
||||
fi
|
||||
# .git 可能是目錄(一般存取庫),也可能是檔案(worktree 與子模組)。兩種都算存取庫。
|
||||
[ -e "$d/.git" ] || continue
|
||||
|
||||
case "$d" in
|
||||
*[!A-Za-z0-9/._-]*)
|
||||
N_REJ=$((N_REJ + 1))
|
||||
emit "rejected=$d reason=路徑裡有空白或殼層特殊字元,送進 sh -c 會被再解一次"
|
||||
continue ;;
|
||||
esac
|
||||
|
||||
_url=$(git -C "$d" remote get-url origin 2>/dev/null) || _url=''
|
||||
_slug=''
|
||||
if [ -n "$_url" ]; then
|
||||
# 兩種形狀:git@host:owner/repo.git 與 https://host/owner/repo.git。都只取最後兩段。
|
||||
_s=${_url%.git}
|
||||
_s=${_s#*://}
|
||||
_s=${_s#*@}
|
||||
case "$_s" in
|
||||
*:*) _s=${_s#*:} ;;
|
||||
esac
|
||||
_s=${_s#/}
|
||||
_owner=''
|
||||
_repo=${_s##*/}
|
||||
_rest=${_s%/*}
|
||||
[ "$_rest" != "$_s" ] && _owner=${_rest##*/}
|
||||
[ -n "$_owner" ] && [ -n "$_repo" ] && _slug="$_owner/$_repo"
|
||||
fi
|
||||
|
||||
_page='-'
|
||||
if [ -n "$_slug" ]; then
|
||||
_h=$(hash40 "$_slug") || _h=''
|
||||
[ -n "$_h" ] && _page="REPO_$_h"
|
||||
fi
|
||||
if [ "$_page" = '-' ]; then
|
||||
N_UNNAMED=$((N_UNNAMED + 1))
|
||||
if [ -z "$_slug" ]; then
|
||||
emit "unnamed=$d reason=沒有 origin 遠端,或網址對不出 {owner}/{repo},算不出盤點頁頁名"
|
||||
else
|
||||
emit "unnamed=$d slug=$_slug reason=這台機器算不出 SHA-1,算不出盤點頁頁名"
|
||||
fi
|
||||
fi
|
||||
|
||||
N_REPO=$((N_REPO + 1))
|
||||
printf '%s%s%s%s%s\n' "$d" "$TAB" "${_slug:--}" "$TAB" "$_page" >>"$OUT"
|
||||
done <"$CAND"
|
||||
|
||||
while IFS= read -r l; do
|
||||
[ -n "$l" ] || continue
|
||||
N_LINK=$((N_LINK + 1))
|
||||
emit "skipped_link=$l reason=符號連結,跟著走會把掃描帶出掃描起點"
|
||||
done <"$LINKS"
|
||||
|
||||
if [ "$MODE" = 'paths' ]; then
|
||||
cut -f1 "$OUT"
|
||||
else
|
||||
while IFS="$TAB" read -r p s g; do
|
||||
printf 'repo=%s slug=%s page=%s\n' "$p" "$s" "$g"
|
||||
done <"$OUT"
|
||||
fi
|
||||
|
||||
emit "scan_root=$SCAN_ROOT depth=$DEPTH repos=$N_REPO unnamed=$N_UNNAMED rejected=$N_REJ excluded=$N_EXCL skipped_links=$N_LINK"
|
||||
|
||||
case "$HASH_MODE" in
|
||||
sha1sum|shasum) note '找不到共用那一支 hash-id(jsc-gitea 的 tools/hash-id),這一輪的頁名改用本機的 SHA-1 算。兩者是同一條規則、值相同,只是少了一層共用;--root 餵對就會用共用那一支。' ;;
|
||||
none) warn '這台機器既沒有共用那一支 hash-id,也沒有 sha1sum 與 shasum,這一輪一個盤點頁頁名都算不出來。存取庫路徑照樣交出去代 {repo},唯讀盤點跑得動;盤點結果沒有頁可以寫回去。' ;;
|
||||
esac
|
||||
[ "$N_REJ" -gt 0 ] && warn "有 $N_REJ 個存取庫的路徑送不進殼,這一份清單裡沒有它們,逐個印在上面的 rejected= 那幾行。那幾個是真的存在的存取庫,只是路徑帶了空白或殼層特殊字元——要盤點得到就得換路徑。"
|
||||
[ "$N_UNNAMED" -gt 0 ] && warn "有 $N_UNNAMED 個存取庫對不出 REPO_{HASH} 盤點頁頁名,逐個印在上面的 unnamed= 那幾行。它們的路徑照樣交出去代 {repo},唯讀盤點跑得動;但盤點結果沒有頁可以寫回去。"
|
||||
|
||||
if [ "$N_REPO" -eq 0 ]; then
|
||||
die 4 "掃過 $SCAN_ROOT 底下 $DEPTH 層,一個 git 存取庫都沒有。掃描起點指錯一層就會長這樣——工作目錄的版面是「一個存取庫一個子目錄」,指到某一個存取庫裡面就只剩它自己那一個,指到家目錄就一個都沒有。"
|
||||
fi
|
||||
if [ "$N_REJ" -gt 0 ] || [ "$N_UNNAMED" -gt 0 ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
+29
-1
@@ -125,6 +125,9 @@
|
||||
# JSC_WIKI_REPO_{TYPE} 各頁型的 wiki 存取庫。已設定的全部快照進條目。名單是當下從
|
||||
# 環境撈出來的,不寫死,所以新增頁型自動涵蓋,這支不必跟著改。
|
||||
# 目錄頁那一支專用變數也在裡面
|
||||
# JSC_ASSIST_SCAN_ROOT 存取庫掃描的起點。install 當下快照進條目;沒有它,帶 {repo}
|
||||
# 代入點的內建項每一輪都代不出目標
|
||||
# JSC_ASSIST_SCAN_EXCLUDE 存取庫掃描的排除清單。install 當下快照進條目
|
||||
set -u
|
||||
|
||||
MARK_PREFIX='# jsc-assist:assistant'
|
||||
@@ -133,6 +136,7 @@ STATE_DIR="$JSC_HOME/assistant"
|
||||
CURRENT="$JSC_HOME/current"
|
||||
LOG="$STATE_DIR/schedule.log"
|
||||
CRONTAB_CMD="${JSC_ASSIST_CRONTAB_CMD:-crontab}"
|
||||
CRON_LINE_MAX="${JSC_ASSIST_CRON_LINE_MAX:-1000}"
|
||||
TASK_PREFIX='jsc-assist-assistant'
|
||||
|
||||
DRYRUN=0
|
||||
@@ -344,7 +348,12 @@ snapshot_names() {
|
||||
# 巡檢那一輪要跑的指令本身走的是字面絕對路徑、不靠 PATH;靠 PATH 的是那幾支腳本自己
|
||||
# 呼叫的外部程式,所以修在條目這一層,不是逐支腳本各自去猜安裝路徑——猜就要維護一份
|
||||
# 路徑清單,而清單會過期。
|
||||
printf '%s\n' GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL
|
||||
# 掃描起點與排除清單也要快照。存取庫掃描那一支刻意不猜掃描起點,沒有值就整個代入點
|
||||
# 代不出來,帶 {repo} 的內建項每一輪都會被跳過——而那一輪只會印一行 held=,看起來像
|
||||
# 「這一批還沒接上」,不像「有一個變數沒進條目」。排除清單少了同樣要命:殼裡排除掉的
|
||||
# 那幾個目錄,在排程那一輪會被當成要盤點的存取庫。
|
||||
printf '%s\n' GITEA_HOST GITEA_TOKEN JSC_HOME JSC_ASSISTANT_HEARTBEAT_TTL \
|
||||
JSC_ASSIST_SCAN_ROOT JSC_ASSIST_SCAN_EXCLUDE
|
||||
env 2>/dev/null \
|
||||
| sed -n 's/^\(JSC_WIKI_REPO\)=.*/\1/p; s/^\(JSC_WIKI_REPO_[A-Za-z0-9_]*\)=.*/\1/p' \
|
||||
| sort -u
|
||||
@@ -708,6 +717,25 @@ crontab_install() {
|
||||
printf '\n' >>"$_new"
|
||||
done
|
||||
|
||||
# 條目長度先量過再往下走,兩個模式都量。
|
||||
#
|
||||
# cron 的一行有長度上限,超過就整批寫不進去,而錯誤訊息是 crontab 自己吐的一句
|
||||
# 「command too long」。實測踩過:把整條 PATH 快照進條目之後 install 回 4——而同一組參數的
|
||||
# --dry-run 全綠,因為那一路根本不碰 crontab。一道只在真的寫入時才會發現的限制,等於
|
||||
# 沒有預檢。
|
||||
# 上限取 1000:那是 vixie-cron 的 MAX_COMMAND,不是這台機器上量出來的,所以留一個變數
|
||||
# 可以覆寫。這台機器實測 810 字元的條目裝得進去,加上整條 PATH 的那一次裝不進去。
|
||||
for _job in $JOBS; do
|
||||
_len=$(cron_entry "$_job" | wc -c | tr -d ' ')
|
||||
printf 'entry_len=%s job=%s limit=%s\n' "$_len" "$_job" "$CRON_LINE_MAX"
|
||||
if [ "$_len" -ge "$CRON_LINE_MAX" ]; then
|
||||
die 4 "$_job 的條目有 $_len 個位元組,達到 cron 一行的上限 $CRON_LINE_MAX,這一批裝不進去。條目長度的來源是環境變數快照($SNAPSHOT_NAMES)與 PATH 快照——先看哪一個值特別長,多半是掃描起點或排除清單寫得太長。縮短那個值,或用 JSC_ASSIST_CRON_LINE_MAX 覆寫上限(覆寫之前先確認這台機器的 cron 真的收得下)。"
|
||||
fi
|
||||
if [ "$_len" -ge $((CRON_LINE_MAX - 100)) ]; then
|
||||
warn "$_job 的條目有 $_len 個位元組,距離 cron 一行的上限 $CRON_LINE_MAX 不到 100。再多加一個環境變數快照就會裝不進去,而那一次的錯誤訊息只會說 command too long。"
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$DRYRUN" -eq 1 ]; then
|
||||
for _job in $JOBS; do
|
||||
printf 'dryrun=crontab job=%s entry=%s\n' "$_job" "$(cron_entry "$_job" | mask_secret)"
|
||||
|
||||
Reference in New Issue
Block a user