Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
87 changes: 87 additions & 0 deletions .github/scripts/govulncheck-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env sh
# govulncheck の結果に「引き上げ方針」を適用するゲート(CLAUDE.md「Go バージョンの引き上げ方針」)。
#
# 到達可能な指摘なし → pass
# 到達可能な非 stdlib(依存モジュール)の指摘がある → fail(stdlib 例外を適用しない)
# 到達可能な stdlib が固定中のマイナー内のパッチで直る → fail(据え置く理由が無い)
# 到達可能な stdlib がマイナー跨ぎの引き上げを要する → warning で許容
#
# 一律 warning にしていた頃は誰も読まず、固定版が3パッチ分の既知 CVE を抱えたまま
# 気づけなかった。だから同マイナー内で直るものだけは fail にする。
#
# 判定は `-format json` の構造化出力で行う。テキスト要約の grep は
# - 件数1件で単数形になる
# - 約80桁で折り返す
# - 成分の並び順(module と stdlib)に依存する
# - Symbol/Package/Module の全セクションの `Fixed in:` を拾ってしまう
# という4つの罠があり、いずれも実際にバグを生んだ(LESSONS 参照)。
#
# JSON の finding では
# 到達可能 = trace[0].function が存在する(テキストの "=== Symbol Results ===" と一致)
# 脆弱なモジュール = trace[0].module(stdlib なら "stdlib")
# trace は「脆弱な関数が先頭・自コードのエントリポイントが末尾」の
# 呼び出し経路。末尾を見ると自分のモジュール名を拾ってしまう。
# 修正版 = fixed_version(stdlib は "v1.26.6" 形式=go 接頭辞は付かない)
#
# 使い方(モジュールディレクトリで実行する):
# .github/scripts/govulncheck-gate.sh <.go-version のパス> [判定対象の JSON ファイル]
#
# 第2引数を渡すと govulncheck を実行せず、その JSON を判定対象にする(テスト用)。
set -eu

VERSION_FILE="${1:?usage: govulncheck-gate.sh <go-version-file> [captured-json-file]}"
CAPTURED="${2-}"

MINOR=$(tr -d "[:space:]" < "$VERSION_FILE" | cut -d. -f1,2)

if [ -n "$CAPTURED" ]; then
JSON=$(cat "$CAPTURED")
else
# govulncheck 自体の失敗(ネットワーク断・引数誤り等)と「脆弱性を検出した」を
# 区別する。-format json は脆弱性検出でも 0 を返すため、非 0 は道具の失敗。
ERR=$(mktemp)
if ! JSON=$("$(go env GOPATH)/bin/govulncheck" -format json ./... 2>"$ERR"); then
echo "::error::govulncheck failed to run"
cat "$ERR" >&2
rm -f "$ERR"
exit 1
fi
rm -f "$ERR"
fi

# 到達可能な finding を "<module>\t<fixed_version>" に落とす。出力は JSON の
# ストリーム(連結オブジェクト)なので -n '[inputs]' でまとめて読む。
REACHABLE=$(printf '%s' "$JSON" | jq -rn '
[inputs]
| map(select(has("finding")) | .finding)
| map(select((.trace // []) | length > 0 and (.[0] | has("function"))))
| map(((.trace[0].module) // "unknown") + "\t" + (.fixed_version // ""))
| unique | .[]
')

[ -z "$REACHABLE" ] && exit 0

# ここから先は何かしら報告する。人が読める形をログに残す(判断には使わない)。
# 到達可能な指摘が無い大多数のケースで2回スキャンしないよう、ここまで来てから実行する。
if [ -z "$CAPTURED" ]; then
"$(go env GOPATH)/bin/govulncheck" ./... 2>&1 || true
fi

# 依存モジュール側の到達可能な指摘は stdlib 例外の対象外。先に判定する。
MODULES=$(printf '%s\n' "$REACHABLE" | awk -F'\t' '$1 != "stdlib" { print $1 }' | sort -u)
if [ -n "$MODULES" ]; then
echo "::error::Reachable vulnerabilities in modules you depend on. Update the dependency:"
printf '%s\n' "$MODULES" | sed 's/^/ /'
exit 1
fi

# 固定中のマイナー内のパッチで直る stdlib の指摘。stdlib の fixed_version は "v1.26.6"。
PATCHABLE=$(printf '%s\n' "$REACHABLE" | awk -F'\t' -v m="v$MINOR." 'index($2, m) == 1 { print $2 }' | sort -u)
if [ -n "$PATCHABLE" ]; then
echo "::error::Reachable stdlib vulnerabilities are fixed within go${MINOR}. Bump .go-version to the latest go${MINOR}.x, run ./build.sh, and commit the binaries. Fixed in:"
printf '%s\n' "$PATCHABLE" | sed 's/^/ /'
exit 1
fi

echo "::warning::Tolerating reachable stdlib vulnerabilities that need a minor-version bump (see the Go upgrade policy in CLAUDE.md)"
exit 0
79 changes: 79 additions & 0 deletions .github/scripts/govulncheck-gate.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
#!/usr/bin/env sh
# govulncheck-gate.sh の判定テーブルを、govulncheck -format json と同じ構造の
# フィクスチャで検証する。到達可能性は trace[0].function の有無で表す。
set -eu

cd "$(dirname "$0")"
GATE=./govulncheck-gate.sh
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
FAILED=0

# finding <osv> <fixed_version> <脆弱なモジュール> <reachable|info>
# trace は govulncheck と同じ向き(脆弱な関数が先頭・自コードのエントリポイントが末尾)。
finding() {
if [ "$4" = reachable ]; then
trace='[{"module":"'$3'","function":"Vulnerable"},{"module":"example.com/app","function":"main"}]'
else
trace='[{"module":"'$3'","version":"v0.0.0"}]'
fi
printf '{"finding":{"osv":"%s","fixed_version":"%s","trace":%s}}\n' "$1" "$2" "$trace"
}

# run <期待終了コード> <期待出力パターン|-> <pinned version> <JSON 本文>
run() {
want_code="$1"; want_pat="$2"
printf '%s\n' "$3" > "$TMP/version"
printf '%s' "$4" > "$TMP/out.json"

got=$("$GATE" "$TMP/version" "$TMP/out.json" 2>&1) && code=0 || code=$?
if [ "$code" -ne "$want_code" ]; then
echo "FAIL [$CASE] exit=$code want=$want_code"; echo " output: $got"; FAILED=1; return
fi
if [ "$want_pat" != "-" ] && ! printf '%s' "$got" | grep -q "$want_pat"; then
echo "FAIL [$CASE] output missing '$want_pat'"; echo " output: $got"; FAILED=1; return
fi
echo "ok [$CASE]"
}

CASE="到達可能な指摘なし → pass"
run 0 "-" 1.26.8 "$(finding GO-1 v1.26.9 stdlib info)"

CASE="到達可能 stdlib・同マイナー内で修正 → fail"
run 1 "::error::" 1.26.5 "$(finding GO-1 v1.26.6 stdlib reachable)"

CASE="到達可能 stdlib が1件だけ・同マイナー内 → fail(単数形の罠を構造で回避)"
run 1 "Fixed in" 1.26.5 "$(finding GO-1 v1.26.6 stdlib reachable)"

CASE="到達可能 stdlib・マイナー跨ぎが必要 → warning で許容"
run 0 "::warning::" 1.26.8 "$(finding GO-1 v1.27.2 stdlib reachable)"

CASE="到達可能 stdlib はマイナー跨ぎ + 到達不能 stdlib が同マイナー内 → warning で許容"
run 0 "::warning::" 1.26.8 "$(finding GO-1 v1.27.2 stdlib reachable)$(finding GO-2 v1.26.9 stdlib info)"

CASE="混在: 到達可能な依存モジュール + 到達可能 stdlib はマイナー跨ぎ → fail"
run 1 "modules you depend on" 1.26.8 "$(finding GO-1 v1.27.2 stdlib reachable)$(finding GO-2 v1.2.3 golang.org/x/text reachable)"

CASE="到達可能な依存モジュールのみ → fail"
run 1 "modules you depend on" 1.26.8 "$(finding GO-1 v1.2.3 golang.org/x/text reachable)"

CASE="到達不能な依存モジュールのみ → pass"
run 0 "-" 1.26.8 "$(finding GO-1 v1.2.3 golang.org/x/text info)"

CASE="到達可能 stdlib が複数・一部が同マイナー内 → fail"
run 1 "::error::" 1.26.5 "$(finding GO-1 v1.27.2 stdlib reachable)$(finding GO-2 v1.26.6 stdlib reachable)"

CASE="CRLF の .go-version でもマイナーを取り違えない"
printf '1.26.5\r\n' > "$TMP/version"
printf '%s' "$(finding GO-1 v1.26.6 stdlib reachable)" > "$TMP/out.json"
if "$GATE" "$TMP/version" "$TMP/out.json" >/dev/null 2>&1; then
echo "FAIL [$CASE] expected exit 1"; FAILED=1
else
echo "ok [$CASE]"
fi

CASE="finding が1件も無い(空出力)→ pass"
run 0 "-" 1.26.8 '{"config":{"protocol_version":"v1.0.0"}}'

[ "$FAILED" -eq 0 ] && echo "--- all gate cases passed" || echo "--- FAILURES"
exit "$FAILED"
80 changes: 17 additions & 63 deletions .github/workflows/build-verify.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ on:
- 'build.sh'
- 'build.ps1'
- '.go-version'
- '.github/scripts/**'
- '.github/workflows/build-verify.yml'
push:
branches: [master]
Expand All @@ -31,6 +32,7 @@ on:
- 'build.sh'
- 'build.ps1'
- '.go-version'
- '.github/scripts/**'
- '.github/workflows/build-verify.yml'

# 決定論ビルドは Go ツールチェーンのパッチ版まで一致が必要。版の定義は
Expand All @@ -39,6 +41,14 @@ on:
# ビルドフラグ自体は build.sh に集約している(重複定義を避けるため)。

jobs:
# govulncheck ゲートの判定テーブル(単数/複数形・行折り返し・マイナー跨ぎ)を検証する。
# ロジックは全プラグインジョブが共有するため、ここが唯一のテスト箇所。
gate-logic:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: .github/scripts/govulncheck-gate.test.sh

github:
runs-on: ubuntu-latest
defaults:
Expand All @@ -54,15 +64,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh github
working-directory: .
Expand Down Expand Up @@ -90,15 +92,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh validator
working-directory: .
Expand Down Expand Up @@ -126,15 +120,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh ast-grep
working-directory: .
Expand Down Expand Up @@ -162,15 +148,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh retro-status
working-directory: .
Expand Down Expand Up @@ -198,15 +176,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh go-lsp
working-directory: .
Expand Down Expand Up @@ -234,15 +204,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh settings-advisor
working-directory: .
Expand Down Expand Up @@ -270,15 +232,7 @@ jobs:
- name: govulncheck
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
OUTPUT=$("$(go env GOPATH)/bin/govulncheck" ./... 2>&1) || STATUS=$?
echo "$OUTPUT"
if [ "${STATUS:-0}" -ne 0 ]; then
if echo "$OUTPUT" | grep -q "Your code is affected by [0-9]* vulnerabilities from the Go standard library"; then
echo "::warning::Tolerating known toolchain vulnerabilities from pinned Go standard library"
else
exit "$STATUS"
fi
fi
"$GITHUB_WORKSPACE/.github/scripts/govulncheck-gate.sh" "$GITHUB_WORKSPACE/.go-version"
- name: Deterministic rebuild
run: ./build.sh worktree-manager
working-directory: .
Expand Down
2 changes: 1 addition & 1 deletion .go-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
1.26.5
1.26.8
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Go ソースを変更したら、リポジトリルートで対象を決定論
./build.sh <github|validator|ast-grep|go-lsp|retro-status|settings-advisor|worktree-manager>
```

Windows PowerShell では同じ target を `./build.ps1` に渡します。決定論ビルドが前提とする Go のパッチ版は `.go-version` に定義され、`build.sh`/`build.ps1` が `GOTOOLCHAIN` で強制します(未取得なら Go が自動ダウンロード)。素の `go build` で焼いたバイナリはローカル Go 版によって別物になるため、必ずビルドスクリプト経由で再ビルドしてください。
Windows PowerShell では同じ target を `./build.ps1` に渡します。決定論ビルドが前提とする Go のパッチ版は `.go-version` に定義され、`build.sh`/`build.ps1` が `GOTOOLCHAIN` で強制します(未取得なら Go が自動ダウンロード)。素の `go build` で焼いたバイナリはローカル Go 版によって別物になるため、必ずビルドスクリプト経由で再ビルドしてください。`.go-version` を独断で書き換えないこと。引き上げ条件は CLAUDE.md「Go バージョンの引き上げ方針」に定義され、条件1(固定中のマイナー内のパッチで直る stdlib 脆弱性)は CI の govulncheck が fail で通知します。

## Verification and Review

Expand Down
13 changes: 13 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,19 @@ cd agy-plugin-kit/validator/src && go vet ./... && go test ./...

**ソース変更時は必ず `./build.sh` で再ビルドしてコミット**(`agy plugin install` はビルドせず git 追跡バイナリをコピーするだけ)。決定論フラグは `build.sh` に、Go のパッチ版は `.go-version` に集約され(`build.sh`/`build.ps1`/CI が同じファイルを読む)、bit-identical になる。CI の stale 検出ゲート(`.github/workflows/build-verify.yml`)がこれを前提にする。

## Go バージョンの引き上げ方針

固定版は `.go-version` の1箇所(`build.sh`/`build.ps1`/CI が読む)。**上げるのは以下のいずれかに当たる時だけ**で、「新しい版が出たから」では上げない。1回の引き上げは21バイナリの再ビルド=約 157MB の新規 blob を伴う。

1. **到達可能な stdlib 脆弱性が、固定中のマイナー内のパッチで修正済み** → そのマイナーの最新パッチへ。言語変更が無く再ビルドのみで済む。**CI の govulncheck がこのケースを fail させる**ので、検知は自動。
2. **固定中のマイナーが EOL**(Go は最新2マイナーのみ patch。1.28 リリース時点で 1.26 が該当)→ サポート内マイナーへ移行。`go vet` の新チェックや挙動変更を見込んでテストを流す。
3. 依存の `go.mod` 下限が固定版を超えた。
4. 必要な言語 / stdlib 機能がある。

マイナー移行は **EOL 直前まで据え置く**(最新マイナーへの追従はしない)。

手順は `.go-version` を書き換えて `./build.sh`(全プラグイン)→ 21バイナリをコミット。`go.mod` の `go` ディレクティブは**言語の下限**であって固定版とは別の軸なので、引き上げに合わせて動かさない。

## 実機検証(tmux + agy)

agy の対話セッションは PTY を要するため tmux 経由で起こす。クリーン install → ツール実行までを実環境で確認する。
Expand Down
Loading
Loading