108 lines
6.5 KiB
Bash
108 lines
6.5 KiB
Bash
#!/usr/bin/env bash
|
||
# 管什麼: 直接打 Gitea API 把一個里程碑改成 closed,卻沒有先記帳(估多久/花多久/
|
||
# 差多少/為什麼差),就擋下來。
|
||
# 為什麼: leo 2026-08-27 說那天「拖時間」,但拖了多久、比預計多拖多少、為什麼拖——
|
||
# 一個數字都沒有。偵測器早就有了(mainline-idle-guard/factory-idle-guard),
|
||
# **但沒有人把它算成帳**,所以事後只能說「這次比較慢」,下次還是一樣。
|
||
# 誤觸時怎麼關: 走正門 `scripts/milestone-account close <owner/repo#編號>`
|
||
# (它會自己算三個數字、自己去 Gitea 的時間軸取證據、然後幫你關)。
|
||
# 真的有理由要繞過去 ⇒ 指令裡加 `milestone-account-ok`(留痕,commit 說明理由)。
|
||
#
|
||
# milestone-account-guard.sh — 結案要記帳(PreToolUse/Bash;inkstone/ISEP#85)
|
||
#
|
||
# ── 這支跟 milestone-due-guard.sh 是同一條線的兩端 ────────────────────
|
||
# 那支管**開**里程碑要有真的期限(沒有期限就沒有「估多久」可言)。
|
||
# 這支管**關**里程碑要留下那三個數字。少了任何一端,帳都算不出來。
|
||
#
|
||
# ── 判準:三件同時成立才擋,缺一放行 ──────────────────────────────
|
||
# ① 指令位置真的有人在發網路請求(curl/wget/python/node/gh)
|
||
# ——「講到它」跟「執行它」是兩件事,這是本 repo 已經證明過的判準
|
||
# (release-tag-guard.sh 的 `git tag` 位置比對、strip_heredoc.py 的來由)。
|
||
# ② URL 指名了**某一個**里程碑:`/repos/<owner>/<repo>/milestones/<數字>`
|
||
# ——沒有數字的(建里程碑、列清單)不是本閘管的形狀。
|
||
# ③ 有寫入動詞(PATCH/POST/.patch(/.post()**而且**內容把 state 設成 closed。
|
||
#
|
||
# ⇒ 純 GET、建里程碑、改標題改期限、`cat` 這支腳本、把指令寫進文件(heredoc 內文
|
||
# 先被 strip_heredoc.py 剝掉)、走正門 `scripts/milestone-account close`
|
||
# ——全部放行,一條都不擋。
|
||
#
|
||
# ── 抽不出 owner/repo/編號就不擋 ────────────────────────────────────
|
||
# fail-open on 解析失敗(不是 fail-open on 檢查結果)。理由同 release-tag-guard.sh:
|
||
# 誤攔比漏擋更該修——**誤攔會懲罰謹慎**,而被誤攔幾次之後人就學會繞過這支閘。
|
||
#
|
||
# ── 已知的漏擋(寫出來,不假裝沒有)────────────────────────────────
|
||
# `bash -c 'curl -X PATCH …'` 這種把真正的指令包進引號裡的寫法,本閘看不到。
|
||
# 這是刻意的取捨:要接住它就得去猜引號裡的內容,而那條路的盡頭是關鍵字黑名單
|
||
# (leo 2026-08-17 已證偽:當日 8 次誤攔、0 次正確攔截)。
|
||
#
|
||
# 迴歸測試:hooks/tests/milestone-account-guard.test.sh(離線,不打 Gitea)
|
||
set -uo pipefail
|
||
|
||
HOOKDIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||
INPUT=$(cat 2>/dev/null || echo '{}')
|
||
CMD=$(printf '%s' "$INPUT" | python3 -c '
|
||
import json, sys
|
||
try: print(json.load(sys.stdin).get("tool_input", {}).get("command", "") or "")
|
||
except Exception: print("")
|
||
' 2>/dev/null || printf '')
|
||
[ -n "$CMD" ] || exit 0
|
||
|
||
# 明講的豁免(留痕)
|
||
case "$CMD" in *milestone-account-ok*) exit 0 ;; esac
|
||
|
||
# heredoc 的內文是資料不是指令——先剝掉,理由見 hooks/lib/strip_heredoc.py 檔頭
|
||
if [ -f "$HOOKDIR/lib/strip_heredoc.py" ]; then
|
||
CMD=$(printf '%s' "$CMD" | python3 "$HOOKDIR/lib/strip_heredoc.py" 2>/dev/null || printf '%s' "$CMD")
|
||
fi
|
||
|
||
# ① 指令位置真的有人在發請求
|
||
printf '%s' "$CMD" | grep -qE '(^|[;&|(`]|&&|\|\|)[[:space:]]*(sudo[[:space:]]+)?(curl|wget|http|python3?|node|gh)[[:space:]]' || exit 0
|
||
|
||
# ② 指名某一個里程碑
|
||
REF=$(printf '%s' "$CMD" | grep -oE '/repos/[A-Za-z0-9_.-]+/[A-Za-z0-9_.-]+/milestones/[0-9]+' | head -1 || printf '')
|
||
[ -n "$REF" ] || exit 0
|
||
|
||
# ③ 寫入動詞 + 把 state 設成 closed
|
||
printf '%s' "$CMD" | grep -qiE -- '-X[[:space:]]*[^A-Za-z]{0,3}(PATCH|POST)|--request[[:space:]]*[^A-Za-z]{0,3}(PATCH|POST)|\.(patch|post)\(|method[[:space:]]*=[[:space:]]*[^A-Za-z]{0,3}(PATCH|POST)' || exit 0
|
||
printf '%s' "$CMD" | grep -qiE 'state[^A-Za-z0-9]{1,8}closed' || exit 0
|
||
|
||
OWNER=$(printf '%s' "$REF" | cut -d/ -f3)
|
||
REPO=$(printf '%s' "$REF" | cut -d/ -f4)
|
||
NUM=$(printf '%s' "$REF" | cut -d/ -f6)
|
||
TARGET="$OWNER/$REPO#$NUM"
|
||
|
||
# 已經記過帳的就放行——正門 `milestone-account close` 是**先記帳、再關**,
|
||
# 所以「帳本裡有這一筆」就代表三個數字已經留下來了。
|
||
LEDGER="${MILESTONE_ACCOUNT_LEDGER:-}"
|
||
if [ -z "$LEDGER" ] && [ -x "$HOOKDIR/../scripts/milestone-account" ]; then
|
||
LEDGER=$(python3 "$HOOKDIR/../scripts/milestone-account" ledger-path 2>/dev/null || printf '')
|
||
fi
|
||
if [ -n "$LEDGER" ] && [ -f "$LEDGER" ] && grep -qF "\"ref\": \"$TARGET\"" "$LEDGER" 2>/dev/null; then
|
||
exit 0
|
||
fi
|
||
|
||
cat >&2 <<MSG
|
||
⏱️ 結案要記帳:**$TARGET 還沒有留下「估多久/花多久/差多少」。**
|
||
|
||
【inkstone/ISEP#85】leo 2026-08-27 說那天「**拖時間**」。
|
||
但拖了多久、比預計多拖多少、為什麼拖——**沒有任何數字**。
|
||
「偵測器早就有了,但沒有人把它算成帳」⇒ 每次事後只能說「這次比較慢」,下次還是一樣。
|
||
|
||
━━ 走正門,它會把該做的一次做完 ━━━━━━━━━━━━━━━━━━━━━━━━
|
||
先看時間軸怎麼說: scripts/milestone-account audit $TARGET
|
||
結案(會自己算、自己關):
|
||
scripts/milestone-account close $TARGET [--reason <代號>]
|
||
|
||
三個數字**它自己算**(取自 Gitea 的 created_at/due_on/closed_at),不用你手填。
|
||
差超過 ±25%(超時或提早都算)才需要 --reason,而且代號只能從那七個裡面挑:
|
||
scripts/milestone-account codes
|
||
代號要有**時間軸撐得住**——證據取自 Gitea,不採信任何人的自述。
|
||
|
||
━━ 為什麼不是「你自己 curl 一下就好」 ━━━━━━━━━━━━━━━━━━━━
|
||
這一關掉,那個里程碑就再也算不出帳了(closed_at 之後沒有人會回頭補)。
|
||
⇒ 這是**不可逆**的一步,所以閘長在這裡,不長在誰的記性上。
|
||
|
||
真的有理由要手動關 ⇒ 指令裡加 `milestone-account-ok`(留痕,commit 說明理由)。
|
||
MSG
|
||
exit 2
|