From 1b9a6858900e97f66db080932b993f3943679b22 Mon Sep 17 00:00:00 2001 From: sasakiyugo Date: Sat, 12 Sep 2026 07:18:05 +0900 Subject: [PATCH 1/2] chore: pin the Go toolchain via .go-version and GOTOOLCHAIN The deterministic rebuild gate requires a bit-identical build, which requires the exact Go patch version. That requirement was only stated in comments, and nothing enforced it: go.mod's `go 1.26.5` is a lower bound and GOTOOLCHAIN defaults to `auto`, so a newer local toolchain is used silently. Running ./build.sh with go1.27.1 produced 18 modified binaries that are indistinguishable from a legitimate rebuild. - Add .go-version as the single definition of the pinned patch version. - build.sh exports GOTOOLCHAIN from it, before the line that echoes `go version`, so the printed version is proof the pin took effect. Go downloads the toolchain on demand, so no developer setup is needed. - build.ps1 does the same, saving and restoring $env:GOTOOLCHAIN in the existing finally block so it does not leak into the caller's session. - CI reads the same file via setup-go's go-version-file, replacing the duplicated GO_VERSION env, and .go-version joins the paths triggers so the gate covers its own inputs. - Point the docs at .go-version instead of repeating the number. Binary-neutral: a full ./build.sh leaves all 21 committed binaries unchanged. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/build-verify.yml | 23 ++++++++++++----------- .go-version | 1 + AGENTS.md | 2 +- CLAUDE.md | 4 ++-- LESSONS.md | 6 ++++++ agy-plugin-kit/README_ja.md | 2 +- build.ps1 | 23 +++++++++++++++++------ build.sh | 18 +++++++++++++----- github/README_ja.md | 4 ++-- 9 files changed, 55 insertions(+), 28 deletions(-) create mode 100644 .go-version diff --git a/.github/workflows/build-verify.yml b/.github/workflows/build-verify.yml index dad5d0f..4fdf1a4 100644 --- a/.github/workflows/build-verify.yml +++ b/.github/workflows/build-verify.yml @@ -16,6 +16,7 @@ on: - 'worktree-manager/**' - 'build.sh' - 'build.ps1' + - '.go-version' - '.github/workflows/build-verify.yml' push: branches: [master] @@ -29,13 +30,13 @@ on: - 'worktree-manager/**' - 'build.sh' - 'build.ps1' + - '.go-version' - '.github/workflows/build-verify.yml' -# 決定論ビルドは Go ツールチェーンのバージョンまで一致が必要。 -# 上げる時はこことローカル環境を揃えて全バイナリを再ビルドすること。 +# 決定論ビルドは Go ツールチェーンのパッチ版まで一致が必要。版の定義は +# リポジトリルートの .go-version 1箇所だけ(build.sh / build.ps1 も同じファイルを読む)。 +# 上げる時は .go-version を書き換え、全バイナリを再ビルドしてコミットすること。 # ビルドフラグ自体は build.sh に集約している(重複定義を避けるため)。 -env: - GO_VERSION: '1.26.5' jobs: github: @@ -47,7 +48,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -83,7 +84,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -119,7 +120,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -155,7 +156,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -191,7 +192,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -227,7 +228,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck @@ -263,7 +264,7 @@ jobs: - uses: actions/checkout@v4 - uses: actions/setup-go@v5 with: - go-version: ${{ env.GO_VERSION }} + go-version-file: .go-version - run: go vet ./... - run: go test ./... - name: govulncheck diff --git a/.go-version b/.go-version new file mode 100644 index 0000000..8fe00a5 --- /dev/null +++ b/.go-version @@ -0,0 +1 @@ +1.26.5 diff --git a/AGENTS.md b/AGENTS.md index 773ec7a..95d9425 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -79,7 +79,7 @@ Go ソースを変更したら、リポジトリルートで対象を決定論 ./build.sh ``` -Windows PowerShell では同じ target を `./build.ps1` に渡します。決定論ビルドは `go 1.26.5` を前提とします。ローカル Go バージョンが異なる場合、コミット済みバイナリとの差分を正しい更新とみなさず、使用できなかったことを報告してください。 +Windows PowerShell では同じ target を `./build.ps1` に渡します。決定論ビルドが前提とする Go のパッチ版は `.go-version` に定義され、`build.sh`/`build.ps1` が `GOTOOLCHAIN` で強制します(未取得なら Go が自動ダウンロード)。素の `go build` で焼いたバイナリはローカル Go 版によって別物になるため、必ずビルドスクリプト経由で再ビルドしてください。 ## Verification and Review diff --git a/CLAUDE.md b/CLAUDE.md index 4752e44..3b2faf5 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,14 +21,14 @@ Go プラグインは **`src/`(ソース)+ `bin/`(配布物)** に分 cd github/src && go vet ./... && go test ./... cd agy-plugin-kit/validator/src && go vet ./... && go test ./... # 他プラグイン(ast-grep / retro-status / settings-advisor / go-lsp)も同じ流儀(/src で go vet ./... && go test ./...) -# バイナリ再ビルド(go 1.26.5。Windows は ./build.ps1) +# バイナリ再ビルド(Go 版は .go-version に固定。build.sh が GOTOOLCHAIN で強制するため事前準備不要。Windows は ./build.ps1) ./build.sh # 全プラグイン ./build.sh github # github だけ ./build.sh validator # validator だけ # 他ターゲット: ast-grep | retro-status | settings-advisor | go-lsp ``` -**ソース変更時は必ず `./build.sh` で再ビルドしてコミット**(`agy plugin install` はビルドせず git 追跡バイナリをコピーするだけ)。決定論フラグは `build.sh` に集約され、Go 1.26.5 固定で bit-identical になる。CI の stale 検出ゲート(`.github/workflows/build-verify.yml`)がこれを前提にする。 +**ソース変更時は必ず `./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`)がこれを前提にする。 ## 実機検証(tmux + agy) diff --git a/LESSONS.md b/LESSONS.md index d0736cf..6d8cf6a 100644 --- a/LESSONS.md +++ b/LESSONS.md @@ -1,5 +1,11 @@ # LESSONS(実装知見ログ) +## 2026-09-12: Go ツールチェーン版を `.go-version` に集約し `GOTOOLCHAIN` で強制する + +- 却下した案: (a) 従来どおり「go 1.26.5 前提」をコメント・README に書くだけで運用する。(b) 固定自体をやめ、stale ゲートを bit-identical 比較からソースハッシュ照合へ変更する。 +- 決め手: (a) は実測で破綻していた。go.mod の `go 1.26.5` は**下限**でしかなく `GOTOOLCHAIN` の既定は `auto` のため、ローカル go1.27.1 で `./build.sh` を回すと 18 バイナリが「正常な再ビルド出力と見分けのつかない差分」として生成された(PR #20 の Codex P1 指摘の原因でもある)。`export GOTOOLCHAIN=go$(cat .go-version)` 追加後は、環境変数を渡さずに全ビルドしても差分 0。(b) は成果物そのものを検証しなくなり、ハッシュの手書き換えや別アーキ・破損バイナリが素通りする。 +- 覆す条件: stale ゲートを bit-identical 比較以外(CI 側でビルドしてコミットする方式など)に変えた場合。その時は版一致が不要になるため固定も外せる。 + ## 2026-09-12: settings-advisor のパス判定を root 相対の「パス成分」へ統一(PR #20 レビュー対応) - 却下した案: Codex の指摘どおり CI 検知(`.circleci` 等の部分文字列一致)と本番設定検知(絶対パスのディレクトリ名走査)を**別々に**直す案。前者に区切り境界チェックを足し、後者だけ root 相対化する。 diff --git a/agy-plugin-kit/README_ja.md b/agy-plugin-kit/README_ja.md index 83ff4fe..f1f568e 100644 --- a/agy-plugin-kit/README_ja.md +++ b/agy-plugin-kit/README_ja.md @@ -37,7 +37,7 @@ agy(Antigravity CLI)プラグインを**正しく・速く量産する**た agy plugin install https://github.com/kwrkb/agy-plugins/agy-plugin-kit ``` -`validator/bin/` のネイティブバイナリ(`validator-linux-amd64` / `validator-darwin-arm64` / `validator.exe`)は `validator/src/main.go` から `go build` した成果物を**全 OS 分とも同梱**(コミット済み)。再ビルドはリポジトリルートのビルドスクリプトを使います(**Go 1.26.5**。決定論フラグはスクリプトに集約。CI の検証ゲート `.github/workflows/build-verify.yml` がこの結果との bit-identical 一致を要求し、Go のバージョンがずれると fail します)。 +`validator/bin/` のネイティブバイナリ(`validator-linux-amd64` / `validator-darwin-arm64` / `validator.exe`)は `validator/src/main.go` から `go build` した成果物を**全 OS 分とも同梱**(コミット済み)。再ビルドはリポジトリルートのビルドスクリプトを使います(Go のパッチ版は `.go-version` に固定され、スクリプトが `GOTOOLCHAIN` で強制します。決定論フラグもスクリプトに集約。CI の検証ゲート `.github/workflows/build-verify.yml` がこの結果との bit-identical 一致を要求します)。 ```bash ./build.sh validator # validator のネイティブバイナリ(linux-amd64/darwin-arm64/windows)を再ビルド。Windows は ./build.ps1 validator diff --git a/build.ps1 b/build.ps1 index 01888e1..807ba10 100644 --- a/build.ps1 +++ b/build.ps1 @@ -1,6 +1,6 @@ #!/usr/bin/env pwsh # build.sh の Windows(PowerShell) 版。決定論フラグ・対象は build.sh と完全に揃える。 -# 同一 Go バージョン(1.26.5) + 同一フラグ + CGO 無効のため、build.sh と bit-identical な +# 同一 Go バージョン + 同一フラグ + CGO 無効のため、build.sh と bit-identical な # バイナリを生成する(CI の検証ゲートはどちらでビルドしても通る)。 # # 使い方: @@ -8,29 +8,39 @@ # ./build.ps1 github # github プラグインのみ # ./build.ps1 validator # agy-plugin-kit の validator のみ # -# 注意: 決定論ビルドは Go ツールチェーンのバージョン一致が前提(現状 go 1.26.5)。 +# 注意: 決定論ビルドは Go ツールチェーンのパッチ版まで一致が前提。版は .go-version が +# 唯一の定義箇所(build.sh・CI も同じファイルを読む)。 param([string]$Target = 'all') $ErrorActionPreference = 'Stop' # 決定論フラグ(build.sh の FLAGS と一致させること) $Flags = @('-trimpath', '-buildvcs=false', '-ldflags=-buildid=') +# Go ツールチェーン固定版(build.sh の GOTOOLCHAIN と同じ .go-version を読む)。 +$GoToolchain = 'go' + (Get-Content (Join-Path $PSScriptRoot '.go-version') -Raw).Trim() + # Build # /src/ のソースから、ネイティブバイナリを /bin/ に生成する。 # -linux-amd64 -darwin-arm64 .exe # 拡張子なしの (OS 分岐 dispatcher)は build.sh / build.ps1 では触らない # (git 追跡のテキストスクリプト)。Windows の agy は .exe を直接起動する。 function Build([string]$dir, [string]$base) { - $ver = (go version) -split ' ' | Select-Object -Index 2 - Write-Host "==> building $base (linux-amd64, darwin-arm64, windows) from $dir/src/ [$ver]" - Push-Location "$dir/src" # $env: はプロセス環境を書き換えるため、対話セッションで ./build.ps1 を # 実行すると呼び出し元シェルを汚染する。退避し finally で必ず復元する # (build.sh はコマンド単位 env + subshell なので汚染しない。それと挙動を揃える)。 + # GOTOOLCHAIN も同じ扱い(build.sh 側は export だが、あちらは子プロセスで完結する)。 $oldCgo = $env:CGO_ENABLED $oldArch = $env:GOARCH $oldOs = $env:GOOS + $oldToolchain = $env:GOTOOLCHAIN + $pushed = $false try { + # 固定は go version の表示より先に行う。表示された版が「固定が効いた証拠」になる。 + $env:GOTOOLCHAIN = $GoToolchain + $ver = (go version) -split ' ' | Select-Object -Index 2 + Write-Host "==> building $base (linux-amd64, darwin-arm64, windows) from $dir/src/ [$ver]" + Push-Location "$dir/src" + $pushed = $true $env:CGO_ENABLED = '0' $env:GOARCH = 'amd64' $env:GOOS = 'linux' @@ -49,7 +59,8 @@ function Build([string]$dir, [string]$base) { $env:CGO_ENABLED = $oldCgo $env:GOARCH = $oldArch $env:GOOS = $oldOs - Pop-Location + $env:GOTOOLCHAIN = $oldToolchain + if ($pushed) { Pop-Location } } } diff --git a/build.sh b/build.sh index 49a3478..b011931 100755 --- a/build.sh +++ b/build.sh @@ -8,14 +8,25 @@ # ./build.sh github # github プラグインのみ # ./build.sh validator # agy-plugin-kit の validator のみ # -# 注意: 決定論ビルドは Go ツールチェーンのバージョン一致が前提(現状 go 1.26.5)。 -# バージョンを上げる時は全バイナリを再ビルドしてコミットすること。 +# 注意: 決定論ビルドは Go ツールチェーンのパッチ版まで一致が前提。版は .go-version が +# 唯一の定義箇所で、CI(setup-go の go-version-file)もこのファイルを読む。 +# バージョンを上げる時は .go-version を書き換え、全バイナリを再ビルドしてコミットすること。 # Windows ネイティブで実行する場合は WSL または git-bash を使う。 set -eu +# スクリプトの位置をリポジトリルートとして扱う(呼び出し元 CWD に依存しない) +cd "$(dirname "$0")" + # 決定論フラグ(CI ゲートと一致させる唯一の定義箇所) FLAGS="-trimpath -buildvcs=false -ldflags=-buildid=" +# Go ツールチェーンを .go-version に固定する。GOTOOLCHAIN の既定は auto で、 +# go.mod の `go` ディレクティブは下限でしかないため、ローカルに新しい Go があると +# 黙ってそちらが使われ、正常な再ビルド出力と見分けのつかない差分が出る(stale ゲートが落ちる)。 +# 指定版が無ければ Go が自動ダウンロードするので、開発者側の事前準備は不要。 +GOTOOLCHAIN="go$(cat .go-version)" +export GOTOOLCHAIN + # build # /src/ のソースから、ネイティブバイナリを /bin/ に生成する。 # -linux-amd64 -darwin-arm64 .exe @@ -34,9 +45,6 @@ build() { ) } -# スクリプトの位置をリポジトリルートとして扱う(呼び出し元 CWD に依存しない) -cd "$(dirname "$0")" - target="${1:-all}" case "$target" in github) build github github ;; diff --git a/github/README_ja.md b/github/README_ja.md index 4c0f371..07a4166 100644 --- a/github/README_ja.md +++ b/github/README_ja.md @@ -54,7 +54,7 @@ agy plugin install https://github.com/kwrkb/agy-plugins/github ### バイナリの再ビルド -リポジトリルートのビルドスクリプトを使います(**Go 1.26.5**。決定論フラグはスクリプトに集約。CI の検証ゲート `.github/workflows/build-verify.yml` がこの結果との bit-identical 一致を要求し、Go のバージョンがずれると fail します)。 +リポジトリルートのビルドスクリプトを使います(Go のパッチ版は `.go-version` に固定され、スクリプトが `GOTOOLCHAIN` で強制します。決定論フラグもスクリプトに集約。CI の検証ゲート `.github/workflows/build-verify.yml` がこの結果との bit-identical 一致を要求します)。 ```bash ./build.sh github # github のネイティブバイナリ(linux-amd64/darwin-arm64/windows)を再ビルド。Windows は ./build.ps1 github @@ -62,5 +62,5 @@ agy plugin install https://github.com/kwrkb/agy-plugins/github ``` > **ビルドする OS によってスクリプトを使い分ける**: macOS / Linux は `./build.sh`、Windows は `./build.ps1`。 -> どちらも `CGO_ENABLED=0` のクロスコンパイルで **3 OS 分のネイティブを 1 台で一括生成**し、Go 1.26.5 固定なら +> どちらも `CGO_ENABLED=0` のクロスコンパイルで **3 OS 分のネイティブを 1 台で一括生成**し、`.go-version` のツールチェーン固定により > ホスト OS に依らず bit-identical(各 OS で実機ビルドする必要はない)。 From eac1b185454586325dd363f583d967967e07207c Mon Sep 17 00:00:00 2001 From: sasakiyugo Date: Sat, 12 Sep 2026 11:43:22 +0900 Subject: [PATCH 2/2] fix: keep .go-version free of CR so GOTOOLCHAIN stays valid build.sh supports Git Bash and WSL, but the repo had no .gitattributes, so with core.autocrlf=true `.go-version` is checked out as CRLF. Command substitution strips the LF and keeps the CR, so build.sh exported `GOTOOLCHAIN=go1.26.5\r` and Go exited with `invalid GOTOOLCHAIN "go1.26.5\r"` before building anything. Reproduced locally. Defend at both layers: - Add .gitattributes pinning .go-version and the shell scripts (build.sh, build.ps1, and the extensionless OS-dispatchers) to LF, and marking the committed native binaries as binary so nothing ever rewrites the bytes the bit-identical gate compares. - build.sh strips whitespace when reading the file, which also rescues clones that already have CRLF on disk. build.ps1 was already safe: it reads the file with .Trim(). Verified: ./build.sh still reports [go1.26.5] with the file written as CRLF and as LF, and adding .gitattributes renormalizes nothing. Co-Authored-By: Claude Opus 5 --- .gitattributes | 21 +++++++++++++++++++++ build.sh | 5 ++++- 2 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..153d5e4 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,21 @@ +# 決定論ビルドの入力とシェルスクリプトは LF 固定、コミット済みネイティブバイナリは +# 一切変換させない。CRLF で checkout されると `GOTOOLCHAIN=go1.26.5\r` になり +# go が `invalid GOTOOLCHAIN` で即死する(build.sh は Git Bash / WSL も対象)。 + +.go-version text eol=lf + +# build.sh と OS 分岐 dispatcher は sh が解釈するため LF 必須。 +*.sh text eol=lf +*.ps1 text eol=lf +ast-grep/bin/ast-grep text eol=lf +github/bin/github text eol=lf +go-lsp/bin/go-lsp text eol=lf +retro-status/bin/retro-status text eol=lf +settings-advisor/bin/settings-advisor text eol=lf +worktree-manager/bin/worktree-manager text eol=lf +agy-plugin-kit/validator/bin/validator text eol=lf + +# bit-identical な stale 検出ゲートの前提。改行変換もテキスト diff もさせない。 +*-linux-amd64 binary +*-darwin-arm64 binary +*.exe binary diff --git a/build.sh b/build.sh index b011931..7ee50ec 100755 --- a/build.sh +++ b/build.sh @@ -24,7 +24,10 @@ FLAGS="-trimpath -buildvcs=false -ldflags=-buildid=" # go.mod の `go` ディレクティブは下限でしかないため、ローカルに新しい Go があると # 黙ってそちらが使われ、正常な再ビルド出力と見分けのつかない差分が出る(stale ゲートが落ちる)。 # 指定版が無ければ Go が自動ダウンロードするので、開発者側の事前準備は不要。 -GOTOOLCHAIN="go$(cat .go-version)" +# Git Bash + core.autocrlf=true では .go-version が CRLF で checkout されうる。 +# CR が残ると go は `invalid GOTOOLCHAIN "go1.26.5\r"` で即死するため空白類を除去する +# (.gitattributes で LF 固定もしているが、既存クローンを救うため読み取り側でも守る)。 +GOTOOLCHAIN="go$(tr -d "[:space:]" < .go-version)" export GOTOOLCHAIN # build