Compare commits

...
3 Commits
Author SHA1 Message Date
JefferyandClaude Opus 4.8 e567759c06 chore(AI 助理設定): 移除 AI 助理設定與指示檔
移除 CLAUDE.md、GEMINI.md 與 .claude/.codex/.gemini/.amazonq 及 Copilot 的設定與指示檔。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:18:41 +08:00
JefferyandClaude Opus 4.8 6cad836ce3 test(清理成品): 新增 tag 清理測試並抽出共用 jq mock
新增 entrypoint_tag_cleanup.sh 驗證孤兒 tag 刪除、保留指定 release 的 tag;分頁測試改為端點感知並共用 jq mock。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:14:56 +08:00
JefferyandClaude Opus 4.8 30f5dce8dc feat(清理成品): 新增刪除未指定 release 的 tag 步驟並抽出分頁共用函式
刪除舊版本成品後,重新取得 release 清單並刪除所有未指定到 release 的 tag。

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-23 16:14:51 +08:00
13 changed files with 298 additions and 288 deletions
-14
View File
@@ -1,14 +0,0 @@
# Triage Findings
When the task is to triage review findings, follow this workflow:
1. Merge all findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1 after sorting.
5. Fix real issues with the smallest safe change.
6. Add false positives to `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check the issue after each fix.
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
-29
View File
@@ -1,29 +0,0 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
-45
View File
@@ -1,45 +0,0 @@
---
name: triage-findings
description: Merge code-review findings, sort and renumber them by severity, resolve real issues, and move false positives into exclusions.
---
# Triage Findings
## When To Use
Use this skill when you receive multiple review findings, screenshots, comments, or issue lists that need to become one final triaged list.
It is also used when some findings are false positives and should be moved into the exclusions list.
## Workflow
1. Collect all findings into one list.
2. Merge duplicates into a single finding when they describe the same issue.
3. Sort the final list by severity:
- critical
- warning
- info
4. Renumber the sorted list from 1 upward.
5. Rewrite each finding concisely so the final list reads cleanly and consistently.
6. If a finding is a false positive, do not keep it in the final list.
7. Add false positives to the exclusions list using the existing schema in the repo or task context, and preserve the original finding wording as much as possible, including language and semantics.
## Resolution Flow
After the list is merged and ordered, resolve the remaining findings one by one.
1. Start from the highest severity item.
2. Identify the root cause in the relevant file or context.
3. Apply the smallest safe change that fixes the issue.
4. Add or update tests when behavior changes.
5. Re-check the issue after the change.
6. If the item is confirmed false positive, move it to exclusions instead of changing code.
7. Continue until the list is either fixed or explicitly excluded.
## Output Rules
- Keep the final findings list in severity order, then by any stable secondary order needed to make it readable.
- Keep numbering contiguous after filtering and merging.
- Preserve useful details like file path, location, and suggested fix.
- Keep exclusions entries minimal and consistent with the project schema.
- When writing exclusions, prefer the original issue text and language; only paraphrase if needed to fit the schema.
- If the source already provides a severity or title, keep it unless it conflicts with the final ordering.
@@ -1,4 +0,0 @@
interface:
display_name: "Triage Findings"
short_description: "Triage, sort, fix, and exclude review findings"
default_prompt: "Use $triage-findings to merge review findings, sort and renumber them by severity, resolve real issues one by one, and add false positives to exclusions."
-29
View File
@@ -1,29 +0,0 @@
---
name: triage-findings
description: Triage findings, fix real issues, and exclude false positives.
---
# Triage Findings
## Use
直接輸入:`triage-findings 問題原始檔(文字或截圖)`
## Workflow
1. Merge all findings.
2. Sort by severity:
- critical
- warning
- info
3. Renumber from 1.
4. Fix real issues.
5. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
6. Add tests when behavior changes.
## Output Rules
- Keep the final list short.
- Keep numbering contiguous.
- Preserve file path, location, and fix.
- When writing exclusions, prefer the original issue text over paraphrased rewrites.
-14
View File
@@ -1,14 +0,0 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The full reusable skill lives in `.claude/skills/triage-findings/SKILL.md`.
-14
View File
@@ -1,14 +0,0 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
-16
View File
@@ -1,16 +0,0 @@
# Triage Findings
When the task is to triage review findings, follow this workflow:
1. Merge all findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1 after sorting.
5. Fix real issues with the smallest safe change.
6. Add false positives to `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check the issue after each fix.
Use the repo-local `triage-findings` skill for the same workflow when running in Codex.
Trigger it with `/triage-findings`.
-14
View File
@@ -1,14 +0,0 @@
# Triage Findings
Use the triage-finding workflow for review issue lists:
1. Merge findings into one list.
2. Remove duplicates.
3. Sort by severity: `critical` -> `warning` -> `info`.
4. Renumber from 1.
5. Fix real issues with the smallest safe change.
6. Put false positives into `.gitea/ai-review/exclusions.json`, preserving the original wording, language, and semantics as much as possible.
7. Add or update tests when behavior changes.
8. Re-check after each fix.
The reusable skill lives in `.gemini/skills/triage-findings/SKILL.md`.
+64 -18
View File
@@ -52,6 +52,27 @@ require_integer() {
fi
}
fetch_all_pages() {
local base_url="$1"
local all_json='[]'
local page=1
local page_url page_json
while :; do
page_url="$base_url?page=$page"
page_json="$(curl -fsS "${auth_header[@]}" "$page_url")"
if [ "$(jq 'length' <<<"$page_json")" -eq 0 ]; then
break
fi
all_json="$(jq -s 'add' <<<"$all_json"$'\n'"$page_json")"
page=$((page + 1))
done
printf '%s' "$all_json"
}
section "參數檢查"
require_value "GITEA_SERVER_URL" "$GITEA_SERVER_URL"
require_value "GITEA_REPOSITORY" "$GITEA_REPOSITORY"
@@ -74,21 +95,7 @@ fi
section "取得成品資訊"
info "GET $release_api_url"
release_json='[]'
page=1
while :; do
page_url="$release_api_url?page=$page"
page_json="$(curl -fsS "${auth_header[@]}" "$page_url")"
if [ "$(jq 'length' <<<"$page_json")" -eq 0 ]; then
break
fi
release_json="$(jq -s 'add' <<<"$release_json"$'\n'"$page_json")"
page=$((page + 1))
done
release_json="$(fetch_all_pages "$release_api_url")"
release_json="$(jq -e 'sort_by(.created_at) | reverse' <<<"$release_json")"
release_count="$(jq 'length' <<<"$release_json")"
@@ -97,9 +104,7 @@ info "KEEP_COUNT=$KEEP_COUNT"
if [ "$release_count" -le "$KEEP_COUNT" ]; then
success "沒有需要清理的舊版本成品"
exit 0
fi
else
section "刪除舊版本成品"
release_to_delete="$(jq -c ".[$KEEP_COUNT:]" <<<"$release_json")"
@@ -127,5 +132,46 @@ while IFS= read -r release_item; do
fail "刪除失敗: $release_tag ($release_name), HTTP $delete_code"
fi
done < <(jq -c '.[]' <<<"$release_to_delete")
fi
section "刪除未指定 release 的 tag"
# 重新取得 release 清單,得到刪除舊版本後仍指定 tag 的成品
current_release_json="$(fetch_all_pages "$release_api_url")"
release_tags_json="$(jq -c '[.[].tag_name]' <<<"$current_release_json")"
tag_api_url="$GITEA_SERVER_URL/api/v1/repos/$GITEA_REPOSITORY/tags"
info "GET $tag_api_url"
tag_json="$(fetch_all_pages "$tag_api_url")"
tag_count="$(jq 'length' <<<"$tag_json")"
info "TAG_COUNT=$tag_count"
while IFS= read -r tag_item; do
[ -z "$tag_item" ] && continue
tag_name="$(jq -r '.name' <<<"$tag_item")"
if is_empty_or_null "$tag_name"; then
warn "略過沒有名稱的 tag"
continue
fi
if [ "$(jq --arg name "$tag_name" 'any(.[]; . == $name)' <<<"$release_tags_json")" = "true" ]; then
info "保留指定 release 的 tag: $tag_name"
continue
fi
delete_url="$tag_api_url/$tag_name"
info "DELETE tag $tag_name"
delete_code="$(curl -sS -o /dev/null -w "%{http_code}" -X DELETE "${auth_header[@]}" "$delete_url")"
if [ "$delete_code" -eq 204 ]; then
success "成功刪除未指定 release 的 tag: $tag_name"
else
fail "刪除 tag 失敗: $tag_name, HTTP $delete_code"
fi
done < <(jq -c '.[]' <<<"$tag_json")
separator
+20 -79
View File
@@ -1,6 +1,9 @@
#!/usr/bin/env bash
set -Eeuo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
@@ -22,14 +25,20 @@ if [[ " $* " == *" -X DELETE "* ]]; then
exit 0
fi
# tags 端點沒有需要清理的 tag
if [[ "$last_arg" == */tags?* ]]; then
printf '%s' '[]'
exit 0
fi
case "$last_arg" in
*page=1)
*/releases?*page=1)
printf '%s' '[{"id":4,"tag_name":"v4","name":"release-4","created_at":"2024-04-01T00:00:00Z"},{"id":3,"tag_name":"v3","name":"release-3","created_at":"2024-03-01T00:00:00Z"}]'
;;
*page=2)
*/releases?*page=2)
printf '%s' '[{"id":2,"tag_name":"v2","name":"release-2","created_at":"2024-02-01T00:00:00Z"},{"id":1,"tag_name":"v1","name":"release-1","created_at":"2024-01-01T00:00:00Z"}]'
;;
*page=3)
*/releases?*page=3)
printf '%s' '[]'
;;
*)
@@ -40,80 +49,7 @@ esac
EOF
chmod +x "$bin_dir/curl"
cat >"$bin_dir/jq.py" <<'PY'
#!/usr/bin/env python3
import json
import re
import sys
expr = ""
flags = set()
args = sys.argv[1:]
while args:
current = args.pop(0)
if current in {"-e", "-c", "-r", "-s"}:
flags.add(current)
continue
if current == "--":
if not args:
raise SystemExit("missing jq expression")
expr = args.pop(0)
break
if current.startswith("-"):
flags.add(current)
continue
expr = current
break
raw = sys.stdin.read()
def dump(value):
if "-r" in flags and isinstance(value, (str, int, float)) and not isinstance(value, bool):
sys.stdout.write(str(value))
else:
sys.stdout.write(json.dumps(value, separators=(",", ":")))
if expr == "length":
data = json.loads(raw or "null")
print(len(data))
raise SystemExit(0)
if expr == "add":
arrays = [json.loads(line) for line in raw.splitlines() if line.strip()]
merged = []
for item in arrays:
merged.extend(item)
dump(merged)
raise SystemExit(0)
if expr == "sort_by(.created_at) | reverse":
data = json.loads(raw or "[]")
dump(sorted(data, key=lambda item: item["created_at"], reverse=True))
raise SystemExit(0)
match = re.fullmatch(r"\.\[(\d+):\]", expr)
if match:
data = json.loads(raw or "[]")
dump(data[int(match.group(1)):])
raise SystemExit(0)
if expr == ".[]":
data = json.loads(raw or "[]")
for item in data:
dump(item)
sys.stdout.write("\n")
raise SystemExit(0)
match = re.fullmatch(r"\.(id|tag_name|name)", expr)
if match:
data = json.loads(raw or "null")
value = data.get(match.group(1))
dump(value)
raise SystemExit(0)
raise SystemExit(f"unsupported jq expression: {expr}")
PY
cp "$script_dir/lib/jq_mock.py" "$bin_dir/jq.py"
chmod +x "$bin_dir/jq.py"
cat >"$bin_dir/jq" <<'EOF'
@@ -129,7 +65,7 @@ GITEA_SERVER_URL="https://gitea.example.test" \
GITEA_REPOSITORY="owner/repo" \
KEEP_COUNT="2" \
RUNNER_TOKEN="" \
bash ./entrypoint.sh >/dev/null
bash "$repo_root/entrypoint.sh" >/dev/null
grep -q 'page=1' "$curl_log"
grep -q 'page=2' "$curl_log"
@@ -138,6 +74,11 @@ grep -q '/releases/2' "$curl_log"
grep -q '/releases/1' "$curl_log"
if grep -q '/releases/3' "$curl_log" || grep -q '/releases/4' "$curl_log"; then
printf 'unexpected deletion request in %s\n' "$curl_log" >&2
printf 'unexpected release deletion request in %s\n' "$curl_log" >&2
exit 1
fi
if grep -q '/tags/' "$curl_log"; then
printf 'unexpected tag deletion request in %s\n' "$curl_log" >&2
exit 1
fi
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env bash
set -Eeuo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "$script_dir/.." && pwd)"
tmpdir="$(mktemp -d)"
trap 'rm -rf "$tmpdir"' EXIT
curl_log="$tmpdir/curl.log"
bin_dir="$tmpdir/bin"
mkdir -p "$bin_dir"
cat >"$bin_dir/curl" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
log_file="${CURL_LOG:?}"
printf '%s\n' "$*" >>"$log_file"
last_arg="${!#}"
if [[ " $* " == *" -X DELETE "* ]]; then
printf '204'
exit 0
fi
# 倉庫所有 tagv1~v4 加上兩個未指定 release 的孤兒 tag
if [[ "$last_arg" == */tags?* ]]; then
case "$last_arg" in
*page=1)
printf '%s' '[{"name":"v4"},{"name":"v3"},{"name":"v2"},{"name":"v1"},{"name":"tmp-build"},{"name":"v0"}]'
;;
*)
printf '%s' '[]'
;;
esac
exit 0
fi
# /releases GET:刪除舊版本後重新取得時,只回傳仍保留的 release
if grep -Eq -- '-X DELETE .*/releases/[0-9]' "$log_file"; then
case "$last_arg" in
*page=1)
printf '%s' '[{"id":4,"tag_name":"v4","name":"release-4","created_at":"2024-04-01T00:00:00Z"},{"id":3,"tag_name":"v3","name":"release-3","created_at":"2024-03-01T00:00:00Z"}]'
;;
*)
printf '%s' '[]'
;;
esac
exit 0
fi
case "$last_arg" in
*page=1)
printf '%s' '[{"id":4,"tag_name":"v4","name":"release-4","created_at":"2024-04-01T00:00:00Z"},{"id":3,"tag_name":"v3","name":"release-3","created_at":"2024-03-01T00:00:00Z"}]'
;;
*page=2)
printf '%s' '[{"id":2,"tag_name":"v2","name":"release-2","created_at":"2024-02-01T00:00:00Z"},{"id":1,"tag_name":"v1","name":"release-1","created_at":"2024-01-01T00:00:00Z"}]'
;;
*)
printf '%s' '[]'
;;
esac
EOF
chmod +x "$bin_dir/curl"
cp "$script_dir/lib/jq_mock.py" "$bin_dir/jq.py"
chmod +x "$bin_dir/jq.py"
cat >"$bin_dir/jq" <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
exec python3 "$0.py" "$@"
EOF
chmod +x "$bin_dir/jq"
PATH="$bin_dir:$PATH" \
CURL_LOG="$curl_log" \
GITEA_SERVER_URL="https://gitea.example.test" \
GITEA_REPOSITORY="owner/repo" \
KEEP_COUNT="2" \
RUNNER_TOKEN="" \
bash "$repo_root/entrypoint.sh" >/dev/null
# 舊版本 release 仍被刪除
grep -q '/releases/2' "$curl_log"
grep -q '/releases/1' "$curl_log"
# 未指定 release 的 tag(含被刪除 release 留下的 tag 與其他孤兒 tag)被刪除
for tag in v2 v1 tmp-build v0; do
if ! grep -q "/tags/$tag" "$curl_log"; then
printf 'expected tag deletion /tags/%s not found in %s\n' "$tag" "$curl_log" >&2
exit 1
fi
done
# 仍指定 release 的 tag 不可被刪除
for tag in v4 v3; do
if grep -q "/tags/$tag" "$curl_log"; then
printf 'unexpected deletion of release-assigned tag /tags/%s in %s\n' "$tag" "$curl_log" >&2
exit 1
fi
done
+98
View File
@@ -0,0 +1,98 @@
#!/usr/bin/env python3
"""Minimal jq stand-in for entrypoint.sh tests.
Only the jq expressions used by entrypoint.sh are implemented. Unknown
expressions fail loudly so new jq usage is noticed instead of silently
producing wrong results.
"""
import json
import re
import sys
expr = ""
flags = set()
named_args = {}
args = sys.argv[1:]
while args:
current = args.pop(0)
if current in {"-e", "-c", "-r", "-s"}:
flags.add(current)
continue
if current == "--arg":
key = args.pop(0)
value = args.pop(0)
named_args[key] = value
continue
if current == "--":
if not args:
raise SystemExit("missing jq expression")
expr = args.pop(0)
break
if current.startswith("-"):
flags.add(current)
continue
expr = current
break
raw = sys.stdin.read()
def dump(value):
if "-r" in flags and isinstance(value, (str, int, float)) and not isinstance(value, bool):
sys.stdout.write(str(value))
else:
sys.stdout.write(json.dumps(value, separators=(",", ":")))
if expr == "length":
data = json.loads(raw or "null")
print(len(data))
raise SystemExit(0)
if expr == "add":
arrays = [json.loads(line) for line in raw.splitlines() if line.strip()]
merged = []
for item in arrays:
merged.extend(item)
dump(merged)
raise SystemExit(0)
if expr == "sort_by(.created_at) | reverse":
data = json.loads(raw or "[]")
dump(sorted(data, key=lambda item: item["created_at"], reverse=True))
raise SystemExit(0)
if expr == "[.[].tag_name]":
data = json.loads(raw or "[]")
dump([item["tag_name"] for item in data])
raise SystemExit(0)
match = re.fullmatch(r"any\(\.\[\]; \. == \$(\w+)\)", expr)
if match:
data = json.loads(raw or "[]")
needle = named_args[match.group(1)]
sys.stdout.write("true" if any(item == needle for item in data) else "false")
raise SystemExit(0)
match = re.fullmatch(r"\.\[(\d+):\]", expr)
if match:
data = json.loads(raw or "[]")
dump(data[int(match.group(1)):])
raise SystemExit(0)
if expr == ".[]":
data = json.loads(raw or "[]")
for item in data:
dump(item)
sys.stdout.write("\n")
raise SystemExit(0)
match = re.fullmatch(r"\.(id|tag_name|name)", expr)
if match:
data = json.loads(raw or "null")
value = data.get(match.group(1))
dump(value)
raise SystemExit(0)
raise SystemExit(f"unsupported jq expression: {expr}")