#!/usr/bin/env sh # report-error.sh — 失敗回報流程:把一筆 hook 或工具異常寫成 wiki 的 ERROR_{HASH}, # 並在 ERROR_CONTENTS 附上一列索引。頁面內容套用 templates/ 的兩份範本, # 範本是文案的唯一來源,本腳本只填欄位;真正寫進 wiki 前,還會先走 # Gitea 寫入確認。 # # 用法: # report-error.sh --hook {名稱} --exit {碼} --summary {摘要} # [--repo {owner}/{repo}] [--cli {名稱}] [--session {id}] # [--source {stdin|env|command}] [--symptom {現象}] # [--cause {可能原因}] [--action {處理結果}] # 相關輸出(stdout/stderr 摘要)由標準輸入讀入,可省略。 # # 輸出: # 成功印出「{頁名} {網址}」一行。網址在異常頁寫成功之後才取,頁名的 hash 帶時間戳, # 每次都是全新的頁,寫之前查一定是 404。取不到網址時只印頁名,原因走 stderr,仍然 exit 0。 # 下列四種情形安靜降級:不寫任何頁、不輸出任何內容、exit 0。回報失敗不該再變成一次失敗。 # 1. 找不到 gitea.sh # 2. 解析不出異常頁的 wiki 存取庫(JSC_WIKI_REPO_ERROR 與 JSC_WIKI_REPO 都沒設) # 3. 算不出 HASH(hash-id 失敗或回空字串) # 4. 建不出暫存檔(mktemp 失敗) # 異常頁與目錄頁分屬兩個存取庫,各解各的:解不出異常頁的存取庫就整支降級;解得出 # 異常頁、只解不出目錄頁的存取庫,就只寫異常頁、跳過目錄頁更新,印出頁名,仍然 exit 0。 # 一份寫得成的異常紀錄,不該因為目錄頁沒地方放就整份丟掉。 # 寫入 wiki 失敗才以 exit 4 回報,訊息走 stderr。 # # 連結: # 目錄頁那一列指向異常頁,一律寫成 [{文字}]({連結}),網址取 jsc-gitea 的 # gitea.sh wiki-url,不自己組路徑。 # 寫入前先把那個網址交給 jsc-gitea 的 link-check.sh,結束碼 0 才把連結寫進那一列。 # 驗不過就只留純文字頁名:那一列照寫、異常頁照寫、結束碼照舊。這一段一律不改結束碼, # 本腳本是失敗回報路徑,回報失敗不該再變成一次失敗。 # # 結束碼: 0=已寫入異常頁並印出頁名(取得網址就一併印出),或以上列四種安靜降級原因之一 # 結束、沒有寫出任何頁也沒有任何輸出——回報失敗不該再變成一次失敗 # 2=用法錯誤(缺 --hook 或 --summary) # 4=寫入 wiki 失敗(異常頁與索引目錄頁,任一支寫不進去就算),或目錄頁的舊內容 # 讀不回來(wiki-get 回 7 金鑰失效、8 其他 API 失敗)而放棄寫入;訊息走 stderr。 # 讀不回來就不寫,是為了不拿範本蓋掉一份還在的目錄頁 # 註: 本檔以 `. "$ROOT/hooks/lib.sh"` 載入共用函式,沒有接 `|| true`。lib.sh 讀不到時 sh 會 # 就地結束並回 2,跟用法錯誤同碼;分不出是哪一種時,先確認 hooks/lib.sh 在不在。 # # 頁名: # ERROR_{HASH},HASH 取「{owner}/{repo} {hook} {時間}」的 SHA-1 完整 40 碼大寫十六進位 # (共用 hash 規則)。三段以空白相連當 hash 輸入。 # 時間放進 hash:同一種失敗再發生時要另開新頁,不覆寫舊紀錄。 # 索引目錄頁的頁名固定,不帶 HASH。 # # 誰來呼叫: # 由操作者手動執行,或由技能步驟執行(`jsc-hooks:hooks-install` 在 wire-cli.sh 回報 # status=failed 時呼叫)。**不接在失敗的 hook 上自動觸發**:hook 一律安靜 exit 0, # 而且自我回報要走網路寫 wiki,失敗的 hook 再去回報自己會疊出迴圈。 set -u HERE=$(cd "$(dirname "$0")" && pwd) ROOT=$(cd "$HERE/.." && pwd) . "$ROOT/hooks/lib.sh" STDIN_JSON="" # 本腳本的標準輸入是錯誤輸出摘要,不是 JSON hook=""; code=""; summary=""; repo=""; cli=""; session="" source_kind=""; symptom=""; cause=""; action="" while [ $# -gt 0 ]; do case "$1" in --hook) hook="${2:-}"; shift 2 ;; --exit) code="${2:-}"; shift 2 ;; --summary) summary="${2:-}"; shift 2 ;; --repo) repo="${2:-}"; shift 2 ;; --cli) cli="${2:-}"; shift 2 ;; --session) session="${2:-}"; shift 2 ;; --source) source_kind="${2:-}"; shift 2 ;; --symptom) symptom="${2:-}"; shift 2 ;; --cause) cause="${2:-}"; shift 2 ;; --action) action="${2:-}"; shift 2 ;; *) shift ;; esac done if [ -z "$hook" ] || [ -z "$summary" ]; then echo "用法:report-error.sh --hook {名稱} --exit {碼} --summary {摘要} [...]" >&2 exit 2 fi gsh=$(jsc_gitea_sh) || exit 0 # 異常頁與索引目錄頁落在兩個不同的存取庫,各解各的:異常頁走自己的型別,目錄頁走目錄專用 # 型別。異常頁的存取庫解不出來就整支降級,連異常都沒地方寫,做下去也沒意義。 wrepo=$(sh "$gsh" wiki-repo ERROR 2>/dev/null) || exit 0 [ -n "$wrepo" ] || exit 0 # 目錄頁的存取庫解不出來不算失敗:異常頁照寫,只跳過目錄頁更新,仍然 exit 0。 # 一份寫得成的異常紀錄,不該因為索引沒地方放就整份丟掉。 crepo=$(sh "$gsh" wiki-repo CONTENTS 2>/dev/null || true) # 存取庫名稱未指定就取工作目錄的 origin(只用來標記異常屬於哪個存取庫) if [ -z "$repo" ]; then origin=$(git config --get remote.origin.url 2>/dev/null || true) # 尾綴的 .git 在這裡剝掉:hash 工具刻意不做輸入正規化,同一份輸入要算出同一個 HASH, # 正規化就是呼叫端的責任。帶不帶 .git 會算出兩個不同的頁,同一個存取庫就分裂成兩份紀錄。 repo=$(printf '%s' "$origin" \ | sed -n 's#.*[/:]\([^/]*\)/\([^/]*\)$#\1/\2#p' | sed 's/\.git$//') fi [ -n "$repo" ] || repo="-" [ -n "$cli" ] || cli=$(cli_name) [ -n "$session" ] || session=$(session_id) [ -n "$code" ] || code="-" [ -n "$source_kind" ] || source_kind="command" [ -n "$symptom" ] || symptom="$summary" [ -n "$cause" ] || cause="待查" [ -n "$action" ] || action="待處理" ts=$(date +'%Y-%m-%d %H:%M:%S') ticket_ts=$(date +'%Y%m%d_%H%M%S') if [ -t 0 ]; then detail=""; else detail=$(cat 2>/dev/null | tr '\n' ' ' | cut -c1-500); fi [ -n "$detail" ] || detail="(無)" # 摘要與相關輸出都落在 markdown 表格欄位裡,半形 | 會把欄位切斷,改成全形 detail=$(printf '%s' "$detail" | sed 's/|/|/g') summary=$(printf '%s' "$summary" | sed 's/|/|/g') hash=$(sh "$gsh" hash-id "$repo $hook $ts" 2>/dev/null) || exit 0 [ -n "$hash" ] || exit 0 page="ERROR_$hash" # sed 取代值要先轉義:& 與分隔字元 | 會被 sed 當語法,換行會整行斷掉 esc() { printf '%s' "$1" | tr '\n' ' ' | sed 's/[\\&|]/\\&/g'; } fill() { sed "s|$1|$(esc "$2")|g"; } tmp_page=$(mktemp) || exit 0 tmp_list=$(mktemp) || { rm -f "$tmp_page"; exit 0; } trap 'rm -f "$tmp_page" "$tmp_list"' EXIT fill '{HASH}' "$hash" < "$ROOT/templates/error-page.md" \ | fill '{yyyy-MM-dd HH:mm:ss}' "$ts" \ | fill '{owner}/{repo}' "$repo" \ | fill '{cli}' "$cli" \ | fill '{session_id}' "$session" \ | fill '{hook_name}' "$hook" \ | fill '{exit_code}' "$code" \ | fill '{error_summary}' "$summary" \ | fill '{現象描述}' "$symptom" \ | fill '{可能原因}' "$cause" \ | fill '{處理方式}' "$action" \ | fill '{stdin / env / command}' "$source_kind" \ | fill '{stdout / stderr 摘要}' "$detail" \ | fill '{yyyyMMdd}_{HHmmss}' "$ticket_ts" > "$tmp_page" # 異常頁先寫,網址後取。頁名的 hash 帶時間戳,每次回報都是一個全新的頁,寫進去之前查網址 # 一定是 404,拿到的必然是空字串;目錄頁那一列會變成沒有連結的死字,stdout 也少一半。 if ! sh "$gsh" wiki-put "$wrepo" "$page" "$tmp_page" >/dev/null 2>&1; then echo "[jsc] 寫入 $page 失敗($wrepo)。" >&2 exit 4 fi # 目錄頁那一列指向異常頁,連結一律寫成 [{文字}]({連結}),網址取 gitea.sh wiki-url。 # wiki 自己那種雙中括號寫法只在同一個 wiki 裡解得開,寫錯不會報錯,畫面上看起來像正常 # 文字或死連結,巡不到也修不了。 # 網址取不到不算失敗:異常頁已經寫成功了,只是這一列少一條連結。這裡把原因記下來走 stderr, # 結束碼照舊——安靜降級仍是 exit 0,回報失敗不該再變成一次失敗。 url=$(sh "$gsh" wiki-url "$wrepo" "$page" 2>/dev/null) url_code=$? url_note='' if [ "$url_code" -ne 0 ]; then url='' case "$url_code" in 4) url_note='頁面查不到(wiki-url 回 4),寫入後尚未生效' ;; 5) url_note='回應裡沒有 html_url(wiki-url 回 5)' ;; 7) url_note='金鑰失效或權限不足(wiki-url 回 7)' ;; *) url_note="wiki-url 結束碼 $url_code" ;; esac echo "[jsc] 取不到 $page 的網址:$url_note。目錄頁那一列與輸出只留頁名。" >&2 fi emit() { # 異常頁已經寫成功,頁名一定要印;網址取不到就只印頁名,不印一個空欄位 if [ -n "$url" ]; then printf '%s %s\n' "$page" "$url"; else printf '%s\n' "$page"; fi } # 連結先驗證連得到,才寫進目錄頁那一列。沒驗過的連結寫進去,異常頁一樣會在目錄頁長出 # 死連結,而目錄頁是別人查問題的入口。驗不過就只留純文字頁名,那一列照寫。 link_cell="$page" if [ -n "$url" ]; then # link-check.sh 與 gitea.sh 同一個 tools 目錄,路徑直接由已經解出來的那一支推得, # 不另寫一套搜尋,兩邊才不會一支解到開發版面、一支解到安裝版面。 lcs="$(dirname "$gsh")/link-check.sh" if [ ! -f "$lcs" ]; then echo "[jsc] 找不到 $lcs,這一列的連結沒驗過,只留頁名;$page 已建立。" >&2 else sh "$lcs" "$url" >/dev/null 2>&1 lc_code=$? case "$lc_code" in 0) link_cell=$(printf '[%s](%s)' "$page" "$url") ;; # 7 是金鑰失效,不是死連結。金鑰過期時私有存取庫的回應與「頁不存在」分不出來, # 把它當成死連結就會連還在的頁一起判死。 7) echo "[jsc] 連結驗證遇上金鑰失效(link-check.sh 回 7),不判成死連結,這一列只留頁名;$page 已建立。" >&2 ;; 3) echo "[jsc] GITEA_HOST 未設定(link-check.sh 回 3),連結沒驗過,這一列只留頁名;$page 已建立。" >&2 ;; *) echo "[jsc] 連結驗不過(link-check.sh 結束碼 $lc_code),這一列只留頁名;$page 已建立。" >&2 ;; esac fi fi row=$(printf '| %s | %s | %s | %s | %s | %s |' \ "$ts" "$link_cell" "$repo" "$hook" "$code" "$summary") build_contents() { # 用範本建一份全新的目錄頁;只有確定舊頁不存在時才可以呼叫 # 範本的示範列整列換成本次這一列,不逐格填。連結那一格已經驗過也組好了,拆成頁名與 # 網址兩個佔位再填,會在驗不過的時候留下一個空網址的死連結。 ROW="$row" awk ' index($0, "| {yyyy-MM-dd HH:mm:ss} |") == 1 { print ENVIRON["ROW"]; next } { print } ' "$ROOT/templates/error-contents.md" > "$tmp_list" } if [ -z "$crepo" ]; then echo "[jsc] 目錄頁的 wiki 存取庫解不出來,只寫異常頁,跳過目錄;$page 已建立($wrepo)。" >&2 emit exit 0 fi # 異常目錄頁一律附加,不整頁覆蓋。頁上每一列都是別人回報的異常,本腳本沒有從別處讀過 # 那些列,所以先把舊頁讀回來、把新列附在文末(最新一筆在最後),再整頁寫回。 # 這個語意完全靠「讀得回舊內容」撐著,因此依 wiki-get 的結束碼分流: # 0 → 讀到既有內容,附加新列(讀得到但整頁是空的,沒有既有列會被蓋掉,套範本才安全) # 4 → 頁面真的還不存在,只有這個碼可以用範本建立新頁 # 7 → 金鑰失效或權限不足,舊內容未知,放棄目錄頁寫入 # 8 → 其他 API 失敗,舊內容一樣未知,處置同 7 # 為什麼 7 與 8 不能當成「頁面不存在」:拿範本蓋掉一份讀不回來的目錄頁,等於刪光所有既有 # 列,而 wiki-put 不做合併、也不留備份,蓋掉就救不回來。 sh "$gsh" wiki-get "$crepo" ERROR_CONTENTS > "$tmp_list" 2>/dev/null get_code=$? case "$get_code" in 0) if [ -s "$tmp_list" ]; then printf '%s\n' "$row" >> "$tmp_list"; else build_contents; fi ;; 4) build_contents ;; *) echo "[jsc] 讀取目錄頁失敗($crepo,wiki-get 結束碼 $get_code),舊內容未知,不寫目錄頁;$page 已建立。" >&2 exit 4 ;; esac if ! sh "$gsh" wiki-put "$crepo" ERROR_CONTENTS "$tmp_list" >/dev/null 2>&1; then echo "[jsc] 寫入目錄頁失敗($crepo),$page 已建立。" >&2 exit 4 fi emit