fix/skill-check-compliance-and-flow #13

Merged
admin merged 4 commits from fix/skill-check-compliance-and-flow into develop 2026-08-31 03:16:18 +00:00
2 changed files with 131 additions and 5 deletions
Showing only changes of commit 338c2da89c - Show all commits
+34 -5
View File
@@ -1,25 +1,37 @@
#!/usr/bin/env sh #!/usr/bin/env sh
# build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。 # build-test.sh — 偵測並執行專案的建置與測試,先建置再測試。
# 用法:build-test.sh <ecosystem> <project-dir> # 用法:
# build-test.sh <ecosystem> <project-dir> 偵測後實際執行
# build-test.sh --detect <ecosystem> <project-dir> 只偵測,唯讀,不執行任何建置或測試
# ecosystem:nodejs | python | dotnet # ecosystem:nodejs | python | dotnet
# 偵測方式: # 偵測方式:
# nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有) # nodejs → package.json 的 scripts.build 與 scripts.test(npm init 產生的佔位 test 視為沒有)
# python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py # python → pytest 設定(pytest.ini、setup.cfg、tox.ini、pyproject.toml)或 tests 目錄與 test_*.py
# dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet # dotnet → *.sln 或 *.csproj,建置與測試都用 dotnet
# 輸出:底層指令的原始輸出。 # 為什麼要有 --detect:推不出指令(5)本來要等所有版本都改寫完才發現,使用者又答不出指令時,
# 前面全數作廢還要走還原。--detect 讓呼叫方在動任何檔案之前先問出結果,工作區完全沒被動過。
# --detect 只做偵測與前置檢查,一個檔案都不寫。
# 輸出:底層指令的原始輸出;--detect 模式輸出偵測到的指令名稱,一行一個(nodejs 是 build、test,
# python 是 pytest,dotnet 是 dotnet build、dotnet test)。
# 結束碼(慣例見 README「工具」章的結束碼總表): # 結束碼(慣例見 README「工具」章的結束碼總表):
# 0 建置與測試都通過 # 0 建置與測試都通過;--detect 模式是推得出指令
# 1 參數個數不對 # 1 參數個數不對
# 2 ecosystem 不認識 # 2 ecosystem 不認識
# 4 需要的指令不存在(npm、python3、pytest、dotnet) # 4 需要的指令不存在(npm、python3、pytest、dotnet)
# 5 推不出任何建置或測試指令——呼叫方要改問使用者 # 5 推不出任何建置或測試指令——呼叫方要改問使用者
# 6 找不到專案目錄——呼叫方停手 # 6 找不到專案目錄——呼叫方停手
# 其他 建置或測試失敗,帶回失敗指令的結束碼 # 其他 建置或測試失敗,帶回失敗指令的結束碼;--detect 模式不會回這一類
# 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。 # 本工具不用 3:3 保留給「該 ecosystem 沒有來源檔案」,這支改用 5 表達推不出指令。
set -u set -u
DETECT=0
if [ "${1:-}" = "--detect" ]; then
DETECT=1
shift
fi
if [ "$#" -ne 2 ]; then if [ "$#" -ne 2 ]; then
echo "用法:build-test.sh <ecosystem> <project-dir>" >&2 echo "用法:build-test.sh [--detect] <ecosystem> <project-dir>" >&2
exit 1 exit 1
fi fi
@@ -49,6 +61,10 @@ EOF
) )
[ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ -n "$scripts" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; } command -v npm >/dev/null 2>&1 || { echo "npm not found" >&2; exit 4; }
if [ "$DETECT" -eq 1 ]; then
printf '%s\n' "$scripts"
exit 0
fi
for s in $scripts; do for s in $scripts; do
( cd "$DIR" && npm run --if-present "$s" ) || exit $? ( cd "$DIR" && npm run --if-present "$s" ) || exit $?
done done
@@ -67,9 +83,17 @@ EOF
fi fi
[ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ "$has_tests" -eq 1 ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
if command -v pytest >/dev/null 2>&1; then if command -v pytest >/dev/null 2>&1; then
if [ "$DETECT" -eq 1 ]; then
echo "pytest"
exit 0
fi
( cd "$DIR" && pytest ) ( cd "$DIR" && pytest )
exit $? exit $?
elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then elif command -v python3 >/dev/null 2>&1 && python3 -m pytest --version >/dev/null 2>&1; then
if [ "$DETECT" -eq 1 ]; then
echo "python3 -m pytest"
exit 0
fi
( cd "$DIR" && python3 -m pytest ) ( cd "$DIR" && python3 -m pytest )
exit $? exit $?
else else
@@ -82,6 +106,11 @@ EOF
projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null) projs=$(find "$DIR" -maxdepth 3 \( -name '*.sln' -o -name '*.csproj' \) 2>/dev/null)
[ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; } [ -n "$projs" ] || { echo "no build or test command inferred for $DIR" >&2; exit "$NO_COMMAND"; }
command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; } command -v dotnet >/dev/null 2>&1 || { echo "dotnet not found" >&2; exit 4; }
if [ "$DETECT" -eq 1 ]; then
echo "dotnet build"
echo "dotnet test"
exit 0
fi
( cd "$DIR" && dotnet build ) || exit $? ( cd "$DIR" && dotnet build ) || exit $?
( cd "$DIR" && dotnet test ) || exit $? ( cd "$DIR" && dotnet test ) || exit $?
exit 0 exit 0
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env sh
# git-guard.sh — 還原路線的前提把關與還原本身,兩個子指令一支腳本。
# 用法:
# git-guard.sh check <project-dir>
# 確認目標是 git 工作樹,而且工作區乾淨。任何一步改檔案之前先跑這支。
# git-guard.sh revert <project-dir> --confirm-destructive
# 還原工作區:git checkout -- . 之後 git clean -fd,最後複驗乾淨。
#
# 為什麼還原要多一個 --confirm-destructive:
# revert 會跑 git clean -fd,未追蹤檔案刪掉就沒有 reflog 可救。把「確實是 git 工作樹」與
# 「呼叫端真的要還原」兩個前提寫進腳本,誤觸就退回 exit 1 或 exit 5,工作區一個位元組都不動。
# 前提逐條擋在動作之前,順序是:目錄存在 → git 指令存在 → 確實是 git 工作樹(不是裸存取庫)
# → 旗標給了 → 目標不是檔案系統根目錄。五條全過才會執行第一個破壞性指令。
#
# 範圍:check 的 status --porcelain 是整個存取庫(比較嚴,髒在別的目錄也擋得住);
# revert 的 checkout 與 clean 只作用在 <project-dir> 以下(比較窄,不會波及上層目錄)。
# clean 用 -fd 不用 -fdx:被 gitignore 的 node_modules、__pycache__、bin、obj 要留著。
#
# 輸出:
# 成功 → stdout 最後一行是 `check ok <project-dir>` 或 `revert ok <project-dir>`;
# revert 會先帶出 git clean 自己的 Removing 清單,那是刪掉哪些檔案的紀錄,要留給呼叫方回報。
# 前提不成立 → stderr 第一行是原因(not a git repository、working tree not clean、
# still dirty after revert),工作區不乾淨時後面接 git status --porcelain 的原始輸出。
#
# 結束碼(慣例見 README「工具」章的結束碼總表):
# 0 check:是 git 工作樹而且乾淨/revert:還原完成而且乾淨
# 1 參數個數不對,或 revert 少了 --confirm-destructive——本工具不動任何檔案
# 2 子指令不認識(只收 check、revert)
# 4 需要的指令不存在(git)
# 5 前提不成立。check 是「不是 git 工作樹」或「工作區不乾淨」;revert 是「不是 git 工作樹」
# 或「還原後仍不乾淨」。呼叫方停手,stderr 第一行講明是哪一種
# 6 找不到專案目錄——呼叫方停手
# 其他 底層 git 指令的結束碼
set -u
usage() {
echo "用法:git-guard.sh check <project-dir>" >&2
echo " git-guard.sh revert <project-dir> --confirm-destructive" >&2
}
[ "$#" -ge 1 ] || { usage; exit 1; }
cmd="$1"
case "$cmd" in
check) [ "$#" -eq 2 ] || { usage; exit 1; } ;;
revert) [ "$#" -eq 3 ] || { usage; exit 1; } ;;
*) echo "unknown subcommand: $cmd" >&2; usage; exit 2 ;;
esac
DIR="$2"
[ -d "$DIR" ] || { echo "project dir not found: $DIR" >&2; exit 6; }
command -v git >/dev/null 2>&1 || { echo "git not found" >&2; exit 4; }
# 確實是 git 工作樹才有還原路線。裸存取庫沒有工作樹,checkout 與 clean 都無從跑起。
inside=$(git -C "$DIR" rev-parse --is-inside-work-tree 2>/dev/null || true)
[ "$inside" = "true" ] || { echo "not a git repository: $DIR" >&2; exit 5; }
if [ "$cmd" = "check" ]; then
dirty=$(git -C "$DIR" status --porcelain 2>/dev/null)
rc=$?
[ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; }
if [ -n "$dirty" ]; then
# 未追蹤的 ?? 行也算髒:還原會跑 git clean -fd,那些檔案刪掉沒有還原路徑。
echo "working tree not clean: $DIR" >&2
printf '%s\n' "$dirty" >&2
exit 5
fi
echo "check ok $DIR"
exit 0
fi
# 以下只有 revert 走得到。破壞性指令的最後兩道前提。
[ "$3" = "--confirm-destructive" ] || {
echo "revert 會執行 git clean -fd,必須明確傳入 --confirm-destructive" >&2
usage
exit 1
}
# 目標是檔案系統根目錄時直接拒絕:路徑組錯的時候,這是唯一擋得住的地方。
abs=$(CDPATH= cd -- "$DIR" 2>/dev/null && pwd) || { echo "project dir not found: $DIR" >&2; exit 6; }
[ "$abs" != "/" ] || { echo "refusing to revert the filesystem root" >&2; exit 5; }
git -C "$DIR" checkout -- . || exit $?
git -C "$DIR" clean -fd || exit $?
left=$(git -C "$DIR" status --porcelain 2>/dev/null)
rc=$?
[ "$rc" -eq 0 ] || { echo "git status failed: $DIR" >&2; exit "$rc"; }
if [ -n "$left" ]; then
echo "still dirty after revert: $DIR" >&2
printf '%s\n' "$left" >&2
exit 5
fi
echo "revert ok $DIR"
exit 0