事象: notarize スクリプトは「資格情報の無い協力者でもビルドできるように」
プロファイル疎通失敗時に警告を出して未 notarize のまま正常終了する設計
だった。Apple の開発者規約が更新されて疎通確認が失敗した日、リリースは
そのまま進み、未 notarize の zip が全チェック緑で公開された。検証器
(verify-release)は Gatekeeper 判定(spctl)を表示していたが、
spctl … | head -2 の形だったためパイプ終端の exit code しか見えず、
rejected が出ても連鎖は止まらない。さらにリリース実行側が工程出力を
tail -1 で圧縮していたため、表示されていた rejected の行も人間に
届かなかった。3 つの失敗が積み重なって初めて成立した事故である。
適用方法:
- fail-open を許した工程には、下流に決定論的なゲートを置く。 成功時に
のみマーカーファイル(例:
<zip>.notarized、status: Acceptedを確認 してから touch)を書き、検証器はその存在を要求する。オンライン照会 (spctl はチケット伝播が遅れる)ではなくローカルの決定論で判定する。 cmd | head/cmd | tailはパイプ終端の exit を返す — 判定に使う コマンドをパイプに入れるな。表示用と判定用を分ける。- リリース工程の出力を圧縮するなら、成功トークンを grep せよ。
tail -1は「最後の行がたまたま成功メッセージ」に依存しており、失敗時は ヒント行が最後に来て成功に見える。grep Acceptedは失敗時に空になる。 - ゲートは両方向を実地で試す: マーカーありで通ること、無しで落ちること。 片方向だけの確認は「発火しないゲート」を検出できない。
- zip の notarize はバイトを変えない(staple 不可の CLI 配布物)。事後に 同一 zip を再提出すれば公開済みアセットも sha もそのまま正常化できる — 再アップロード不要かの判断材料として覚えておく。
- 2026-08 に全数展開済み(GUI 15 + CLI 60): canonical の notarize スクリプト
両変種がマーカーを書き(開始時に旧マーカーを rm — 再ビルドされた成果物の隣に
古い判定を残さない)、各リポの
make verify-releaseが二重ゲートで判定する。 第二ゲートは配布形式で異なる: GUI はオフラインのstapler validate(staple 済み bundle はオンライン照会不要)、CLI は-ntの鮮度テスト (zip は staple 不可なので「マーカーが zip より新しい」ことを要求 — マーカー後に 再ビルドされた zip は再 notarize しない限り落ちる)。vendored コピーは check-org check10 が byte 比較で drift を検出する(slack-router の build-tools/ 配置も対象)。
macOS の署名・notarization、リリースアーカイブ、Homebrew tap 配布に関する知見集。 各項目は「事象 → なぜ → 適用方法」の構造で、実プロジェクトで踏んだ事例から抽出している。 規範的なルールは CONVENTIONS.md (§Release Archive Standard, §Code Signing and Notarization)を参照。本書はその背後にある 「なぜ」と、ルール化しきれない落とし穴を扱う。
事象: Apple の notary service(xcrun notarytool submit)は zip / dmg / pkg
コンテナしか受け付けず、tar.gz は直接送れない。
なぜ有効か: notary ticket はバイナリの CDHash(中身のハッシュ)をキーに Apple の サーバに保存される。最終配布フォーマットが何であっても、実行時に macOS が CDHash で 問い合わせて ticket を見つければ notarized として扱われる。
適用方法: packaging の前に「temp で zip 化 → submit → zip 破棄」を挟む。
dist-darwin: cross-build-darwin
@for arch in arm64 amd64; do \
cp dist/$(BINARY)-darwin-$$arch /tmp/$(BINARY) && \
(cd /tmp && zip -j /tmp/$(BINARY)-notary.zip $(BINARY)) && \
scripts/notarize-darwin.sh /tmp/$(BINARY)-notary.zip "$(NOTARY_PROFILE)"; \
rc=$$?; \
rm -f /tmp/$(BINARY) /tmp/$(BINARY)-notary.zip; \
test $$rc -eq 0; \
done
# この後は無修正の tar.gz / .deb / .rpm packaging でよい — バイナリは既に notarizedrm -fで notarize 失敗時も temp を掃除し、test $$rc -eq 0で失敗を伝播する。- ticket は CDHash 紐付けなので、submit 後にファイル名を変えても zip を作り直しても有効。
事象: それまで通っていた notarization が突然一律失敗し、ラッパースクリプトは 「Keychain profile not found」と報告した。
なぜ: スクリプトが事前 probe(notarytool history)の終了コードだけを見て失敗理由を
決め打ちしていた。直接 xcrun notarytool history --keychain-profile <profile> を叩くと実際は
403 A required agreement is missing or has expired — Apple Developer Program のライセンス契約が
更新され、Account Holder が未同意だっただけで、キーもプロファイルも有効だった。
適用方法:
- ラッパーの失敗表示を鵜呑みにせず、必ず
notarytool historyを直接実行して実エラーを見る。 403 ... agreement ...なら developer.apple.com/account で更新契約に同意する (App Store Connect の Agreements も確認)。数分で反映され、キー再登録は不要。- ラッパースクリプトは probe の stderr を捕捉して実エラーを必ず表示し、 403/agreement を検出したら契約再同意を案内するよう修正した(親切な誤診断は誤診断より悪い)。
notarytool historyの事前 probe は flaky(成功直後に別 arch で偽 fail することがある)。 スクリプト経由で落ちたらnotarytool submit --waitを直接叩いて迂回する。- notarytool の keychain profile はマシンローカル(保存先は data-protection
キーチェーン — 下の画面ロックの項を参照)。別マシンには同期されないので、
ビルドマシンごとに
store-credentialsが必要。
事象: 同一マシン・同一プロファイルで 8 本連続で通っていた notarize が、無人実行の
途中から一律 Error: No Keychain password item found for profile: <name> で失敗し始めた
(2026-08、GUI 10 本一括リリース中)。プロファイル消失・キーチェーン破損に見えるが、
実際は画面ロックがかかっただけだった。解除した瞬間に何の再登録もなく復旧した。
なぜ: notarytool store-credentials は資格情報を data-protection キーチェーンに
保存する(legacy な security find-generic-password ではロック解除中でも見えない —
これで消失と誤診しやすい)。data-protection キーチェーンの項目はコンソールロック中は
読み出し不可になるため、画面がロックされると notarytool は「item が無い」と報告する。
security show-keychain-info login.keychain-db が正常応答するのも誤診を誘う
(login.keychain-db 側は無関係)。
適用方法:
- このエラーを見たらまず
ioreg -n Root -d1 | grep IOConsoleLocked—Yesなら 資格情報は無事で、画面を解除するだけでよい。プロファイル再作成は不要。 - 無人リリース(並列エージェント・長時間バッチ)は notarize が終わるまで画面ロックを 保留するか、途中でロックされたら止まった分だけ後で再開する設計にする (tag 済み・release 未作成の状態からの再開は版数再利用にならない)。
- 消失と断定して
store-credentialsをやり直す前に必ずロック状態を確認する — 再登録は不要な API キー操作であり、原因も誤って学習される。
事象: Dropbox 同期フォルダ配下の ~/bin に Go バイナリを cp すると、起動即
zsh: killed(SIGKILL)。同じバイナリが dist/ 直下(同期外)では動く。
なぜ: Dropbox の FileProvider extension は、sync フォルダにファイルが landing した時点で
com.apple.provenance 等の xattr を付与する(同一マシン内のローカル cp でも付く)。
com.apple.provenance は system-protected で xattr -d でも削除不可。弱い署名
(ad-hoc / linker-signed)と provenance の組合せを Gatekeeper が SIGKILL する。
この挙動は macOS のマイナーアップデートで強化され、「以前は動いていた構成」が突然壊れた。
適用方法:
- 同期フォルダ配下に置いた後に
codesign --force --sign - <binary>で再署名すると通る (provenance は残るが、署名が「このマシンが今 sign した」ものになる)。 同期先の各マシンで再署名が必要(署名は sync では再付与されない)。 cpは宛先に新規 inode を作るので xattr が新規付与されるが、Finder の同一ボリューム内 「移動」は rename(inode 保持)なので発火しない。dittoも可。素のcpを避ける。spctl --addは最新 macOS で deprecated("This operation is no longer supported")。- これは環境/配置の問題でコードバグではない。子プロセスの即死
(
read response: EOF/broken pipe)を見たら、まず<binary> --versionの exit code(137 = SIGKILL)と配置場所(同期フォルダ配下か)を確認する。 - プロジェクトの Makefile に個人環境向けの
install:target(~/bin決め打ち cp + codesign)を入れない。プロジェクトはmake build→dist/まで。個人環境への配置は dotfiles 側のスクリプトで吸収する。
事象: CLI 用の署名/notarize スクリプトは単体 Mach-O 前提で、.app バンドルには流用できない。
要点(正規手順は CONVENTIONS.md §Code Signing → GUI app signing):
- WebView 系(Wails / Tauri)は entitlements 最小 2 つが必須:
com.apple.security.cs.allow-jit+com.apple.security.cs.allow-unsigned-executable-memory。 無いと WKWebView の JS が Hardened Runtime に殺され、フロントエンドが無言で真っ白になる。 - ネイティブ Swift / AppKit は JIT entitlements 不要。Hardened Runtime のみで動く。
localhost への HTTP も Info.plist の
NSAppTransportSecurity側で対応でき、entitlement 不要。 cp -rは禁止、dittoを使う: バンドル署名は xattr に格納されるため、cp -rは署名を 壊し「Code Signature Invalid」で起動即死する。配布 zip 化もditto -c -k --keepParent。- 署名順序: build → deep 署名(
--force --deep --options runtime --timestamp --entitlements)→ ditto → notarize(.app は temp zip 化して submit)→stapler staple。バンドル形式は staple できるのでオフライン first-launch でもダイアログが 出ない。CLI 単体 Mach-O は staple 不可(オンライン check が走る)。 - Tauri は
tauri buildが env var(APPLE_SIGNING_IDENTITY)を見て bundler 内で署名する。 Tauri 内蔵 notarize は独自の env(Apple ID + パスワード)を要求し notarytool の keychain profile を使えないため、無効のままにして自前で xcrun notarytool を回すのが楽。 DMG ファイル名は Rust triplet arch(aarch64)でuname -m(arm64)と一致しない。packagetarget をbuild依存にすると再ビルドで staple が剥がれる。 - Swift Package Manager ベースの GUI は
.appバンドル構造(Contents/MacOS/等)を Makefile で手動組み立てし、最後に codesign する。
事象: ビルドログとローカル dist が正しくても、公開資産の中身が意図通りとは限らない。 全 org 監査(52 リポ / 171 アーカイブ)で、ある zip の README/LICENSE 欠落を検出した。
適用方法:
gh release downloadで公開資産を実 DL → 展開して検査する。検査項目: canonical バイナリ名 /fileでの arch 一致 / README.md + LICENSE 同梱 / darwin の署名。.appの zip はditto -x -kで展開する。unzipは禁止 — unzip は symlink/xattr を 壊し、codesign --verify --deepが「a sealed resource is missing or invalid」と 誤失敗する(元 zip は健全なのに)。spctl -a -t execは bare CLI Mach-O を仕様上 reject する("the code is valid but does not seem to be an app")— notarize の有無と無関係。CLI の確認はcodesign --verify --strict+Authority=Developer ID Application+ Identifier 一致で行う。spctlで "accepted, source=Notarized Developer ID" と評価できるのは.appだけ。- bare CLI の notarize 有無そのものは
codesign --test-requirement="=notarized" --verifyで直接確認できる — 通ればexplicit requirement satisfiedを返す。上記の 「spctlが使えないので Authority と Identifier で代替する」より一段強い確認で、 「署名は正しいが notarize されていない」バイナリを弾ける。ローカル dist ではなくgh release downloadした公開資産に対して実行すること(2026-08-23, mcp-bridge v0.1.0 で実証)。- ただしこの検査は notarize 直後には落ちる。 bare Mach-O は staple できずチケットが
オンライン参照になるため、
notarytoolがAcceptedを返した直後でも数十秒〜数分はcode failed to satisfy specified code requirement(s)を返す。これを notarize 失敗と 読み違えないこと — 権威はnotarytoolのAcceptedであり、この検査が見ているのは チケットの配信状況にすぎない。アップロード後に公開資産へ対して実行すれば自然に待ちが入る。 それでも落ちるなら until ループで再試行する(2026-08-23, mcp-bridge v0.1.1 で遭遇)。
- ただしこの検査は notarize 直後には落ちる。 bare Mach-O は staple できずチケットが
オンライン参照になるため、
git tag(lightweight)はgit describe(annotated 限定)から見えない。Makefile の 版数導出はgit describe --tags(lightweight 込み)を使う。
事象: zip -j は渡したファイルの basename をそのままエントリ名にするため、per-arch
成果物(tool-darwin-arm64)を直接固めると zip 内のバイナリに arch サフィックスが残り、
利用者が配備時に rename を強いられる。32 ツールで一括修正になった。
適用方法: stage-and-zip パターン — per-arch 成果物を canonical 名でステージコピー してから固める。
$(eval STAGE=dist/_pkg-$(GOOS)-$(GOARCH)) \
rm -rf $(STAGE) && mkdir -p $(STAGE) ; \
cp dist/$(BINARY)-$(GOOS)-$(GOARCH)$(EXT) $(STAGE)/$(BINARY)$(EXT) ; \
zip -j $(ARCHIVE) $(STAGE)/$(BINARY)$(EXT) LICENSE README.md ; \
rm -rf $(STAGE) ; \- 単一 Mach-O の
cpは署名を保持する(.app バンドルと違い ditto 不要)。 - notarization は CDHash 紐付けでファイル名に依存しないため、rename しても通る。
事象: version サブコマンドしか持たないツールは、共有 formula テンプレートの
assert_match version.to_s, shell_output("#{bin}/<name> --version") により brew test が
"unknown flag" で失敗する。brew install は成功するので tap 掲載後に初めて露見し、
4 本まとめてパッチ版を切ることになった。
適用方法:
- scaffold 時点で
--versionを通す(cobra ならrootCmd.Version = Version)。 サブコマンドは互換のため残し、SetVersionTemplateで出力を一致させる。 - 回帰テストでは
rootCmd.Versionをテスト内で代入しない — 代入するとフラグが生えて 壊れたバイナリでも通ってしまう。本番 init での結線(rootCmd.Version != "")を検査する。
事象: VERSION ?= dev のハードコードのまま make build-all を実行し、全リリース
バイナリのバージョンが dev になった状態でアップロードしてしまった。
適用方法:
- Makefile は
VERSION ?= $(shell git describe --tags --always --dirty 2>/dev/null || echo dev)を最初から使う(?=なので手動指定も可能なまま)。 make build-all直後にdist/<binary>-darwin-arm64 --versionを実行し、期待する バージョン文字列と一致してからアップロードする。リリースチェックリストに含める。
事象: GitHub Release が存在するタグを削除して貼り直すと、リリースが Draft に戻り "Latest" 指定も失われ、手動復旧が必要になった。
なぜ: 利用者が既にダウンロードしたバイナリと同じバージョン番号の別バイナリが世に出ると、 バージョン番号が識別子として機能しなくなる。
適用方法:
- 既公開資産に影響する修正でも再リリースせず、次のパッチ/マイナーリリースで自然に直す (fix-forward)。
- タグを消す前に必ず
gh release listでリリースの有無を確認する。リリースが無いタグに 限り貼り直してよい。
事象: ソースビルドすると Developer ID 署名が剥がれる。tap は署名+notarize 済み リリース zip をそのまま入れる prebuilt 方式にする。
適用方法:
- formula(Go CLI):
depends_on arch: :arm64+depends_on :macos、bin.install "<name>"。資産名にバージョンが中間に入る形式では brew の自動 version 検出が 効かないためurlハードコード +version明示。brew styleの ComponentsOrder はurlがversionより前を要求するので、urlに#{version}補間は使えない。 - 新しい macOS を要求するアプリは cask の下限を明示的に設定する。 共有テンプレートの
既定
:big_surのままだと、動作しない OS に対応していると広告することになり、しかも 何も失敗しない——アプリが動かない環境に cask が入ってしまう。シンボルはマーケティング名 ではないので Homebrew 自身の表から読む(brew ruby -e 'puts MacOSVersion::RELEASES'またはLibrary/Homebrew/macos_version.rb。26 が:tahoe、27 が:golden_gate)。 zapがアプリの保存状態を消すべきかは、チェック項目ではなく判断である。 システム側の 設定を変更し、変更前の値の記録を保持するツールでは、その記録を zap で消すと 「変更されたまま戻す記録がない」状態を作る——zapはシステム設定自体を戻せない。 この種の記録は残し、アンインストール前にアプリ内で取り消すよう利用者に伝える。- cask(GUI .app):
depends_on macos: :big_sur(文字列形は style が symbol 形に矯正)、zap trash: [...]。 - cask が notarization の真テスト: formula は brew が quarantine xattr を剥がすが、
cask は保持する →
spctl -a -t execがフル Gatekeeper 判定を行う。 クリーンマシンで "accepted, source=Notarized Developer ID" になれば署名保持の決定的証明。 - クリーン VM が無いときの代替検証: 公開 Release から zip を DL → 自分で
com.apple.quarantinexattr を付与 →codesign --verify --strict+ quarantine 付きで実行。 quarantined な notarized バイナリが実行できれば Gatekeeper のオンライン判定を通過した証拠。 - 公開前検証は local tap で GitHub 不要: tap リポの working copy を
$(brew --repository)/Library/Taps/<org>/homebrew-tapに clone すればbrew install <org>/tap/<name>が動く(url は公開 release 資産を指すので DL 可能)。brew audit --tapは Casks/ ディレクトリが無いと cask を検査しない。 - ssh 越しの brew は PATH に無い(
/opt/homebrew/binは.zprofile経由)→eval "$(/opt/homebrew/bin/brew shellenv)"を先頭に。cask install は--appdir="$HOME/Applications"で sudo プロンプト(非対話 ssh でハング)を回避できるが、--appdirは uninstall には渡せない(receipt から解決される)。 - 後片付けは uninstall → untap の順(installed 成果物が残っていると untap が拒否される)。
事象: make build が dist/<binary> を上書き + codesign --force で再署名した結果、
そのバイナリから起動していた常駐 daemon が macOS に kill され、データ記録が 30 分以上
無言で欠落した。
なぜ: 実行中バイナリのコード署名が変わると macOS がプロセスを落とす。LaunchAgent 未登録
だと KeepAlive による自動復帰も効かない。
適用方法:
- 常駐 daemon は
dist/からではなく.app同梱パス(Contents/Resources/<binary>)から 起動させる。make buildの影響を受けず、LaunchAgent のKeepAliveで復帰する。 - ビルドを伴う作業の前後で daemon の status(最終サンプル時刻等)を確認する。 長い欠落は自分のビルドが落とした可能性を疑う。
事象: GUI アプリに CLI バイナリを同梱するリリースで、make clean 直後に
make package を実行したところ、同梱が黙ってスキップされたまま notarize まで通過した。
zip が 1.2MB(正常なら約 10MB)だったことで、アップロード前の検証で気づいた。
なぜ: 同梱処理が [ -x "$CLI_BIN" ] のような存在チェックで、無ければ警告を出して
続行する作りだった。make clean は素の名前のバイナリを消し、package が依存する
build-all はプラットフォーム接尾辞付きの名前しか作らないため、参照先が存在しなかった。
署名も notarize も「中身が欠けていること」は検出しない。
適用方法:
- アップロード前に必ずアーカイブを展開し、中身を数える。同梱物の有無、実行ファイルの
--version、署名(spctl --assess)を実物に対して確認する。 - 同梱物のサイズは最も安価な健全性指標。桁が変わったら疑う。
- 依存ステップが「無ければ警告して続行」なら、リリース経路では失敗させる方が安全。 開発時の利便性のための緩さが、そのままリリース事故になる。
事象: タグ後に Makefile の1行だけ直したところ、git describe が
v0.1.0-1-g<sha> を返し、同梱バイナリの版数表示がリリース版でない文字列になった。
なぜ: 版数を git describe --tags から取る構成では、タグ以降に何かコミットすると
(それがコンパイル対象でなくても)版数文字列が変わる。
適用方法: 同梱物をビルドするときは make build VERSION=vX.Y.Z のように明示指定する。
コンパイル対象に差分が無いことを git diff --stat <tag>..HEAD で確認したうえで、
リリース版の文字列を名乗らせる。
事象: 「プレリリース」「未リリース」と書かれた README を持つツールが 3 本見つかった。
いずれも実際にはリリース済みで、うち 1 本は 4 回リリースし、Homebrew tap にも載り、
その brew install コマンドを README 自身が 2 行下に載せていた。もう 1 本は
「未リリース。公開後は以下で入ります」と書いた直後に、既に動く brew コマンドを
置いていた。
なぜ: リリースチェックリストが見るのは CHANGELOG・バージョン・署名・アーカイブで、 scaffold 時に書いた散文は誰も見ない。しかも「まだ出ていない」は書いた瞬間は正しく、 最初のリリースで初めて誤りになる — 書いた本人がその瞬間に立ち会わない種類の腐り方をする。 同種のものとして、README の機能説明が後で実測して否定された挙動を宣伝し続けていた例も あった(別ツールの org profile 行)。
適用方法:
- 状態を散文で書かない。 リリース済みかどうかはインストール手順の有無で伝わる。
版数も書かない(
git tagが唯一の情報源) - どうしても書くなら、リリース手順が触るファイルにだけ書く(CHANGELOG 等)。 README の冒頭バナーは誰の手順にも含まれない
- 機械的に検出できる。 「リリースが 1 件以上あるのに README に
未リリース|プレリリース|not yet releasedが含まれる」は grep +gh release listで 判定でき、組織横断チェックに足せる
事象: org の 5 リポが GitHub 上で「Other」ライセンスと表示されていた。中身は 5 つとも
MIT で、LICENSE の末尾に第三者帰属が追記されていたためテンプレート一致が壊れていただけ
だった。通知を NOTICE.md に分離したところ、2 リポで別の欠陥が露見した — 同梱している
コード(Chart.js、highlight.js + Mermaid.js)の帰属が、リリースアーカイブに一度も入って
いなかった。しかも分離の前例として参照したリポ自身が同じ穴を持っていた。さらに 1 リポの
通知は他プロジェクトからの貼り付け誤りで、無関係な由来を宣言していた。
なぜ: GitHub の判定は LICENSE がテンプレートと一致するかだけで決まり、1 行の追記で 「Other」に落ちる。一方、帰属が果たされているかは「リポジトリに置いてあるか」ではなく **「配布物に入っているか」**で決まる。MIT / BSD は通知の同梱を配布の条件にしているので、 リポジトリに置いただけでは条件を満たさない。この 2 つは逆向きに効く: 分類を直すために 通知を LICENSE から出すと、配布物からも消える。
適用方法:
- LICENSE はテンプレートのまま置き、第三者帰属は
NOTICE.md等の別ファイルにする。 LICENSE から NOTICE への参照も書かない — それ自体が追記になって分類を壊す。誘導は README に置く。 - 分離と package への同梱は同じコミットで行う。順序を分けると、その間のリリースが 帰属を落とす。
- 検証を構造読みで済ませない。staging は 1 行の長いシェルループで、
cpが失敗しても 後続のtar/zipは走り、ファイルが入らないまま静かにアーカイブができる。実際に 作って中身を一覧する。 - 署名済み
.appを配る GUI は別扱い。アーカイブは.appのみで、通知を並べられない (署名バンドルを乱す)。同梱コードを持つなら.appの Resources に入れる。 - 「前例」のリポが正しいとは限らない。 参照した先が同じ穴を持っていた。前例は書式の 参考にし、正しさは条件(配布物に入るか)で判定する。
- 帰属文は貼り付けで増える。本文に他プロジェクト名が残っていないかを確認する。
事象: あるセッションが gem-agent をリリースしている最中(2026-09-13)、別のエージェントが
同じ作業コピー(と同居リポジトリ)で make build を実行し、ad-hoc 署名の dist/<name> を
書き、その後「自分の出力が公開アーカイブに混入したかもしれない」と申告した。dist/ は
共有の出力先で、数分の間に 2 つの書き手がそこにいた。
なぜ重要か: 構造上は混入しにくい — make build は dist/<name> に書き、make package は
dist/<name>-darwin-arm64 を自分でビルドして zip にし、もう一方のファイルは読まない。
リリース冒頭の make clean はそこにあった物を消す — が、「しにくい」は証拠ではなく、
最初に浮かぶ手(再ビルドして比べる)は残っている証拠を壊す。決着は公開物そのものと、
誰も編集しない記録からつける。
適用方法:
- まず凍結する。 監査が終わるまで
make clean・再ビルド・再署名はしない。以下は全て 読み取りのみ。 - 出所は公開物自身から。
gh release downloadで zip をファイルに落とし、ditto -x -kで展開し、go version -m <binary>を見る:vcs.revisionがリリースコミットでvcs.modified=falseであること。別コミットや未コミット変更のあるツリーから作った バイナリはこれを偽装できない。今回の紛れ込んだビルドはv0.77.1-2-g93b02e6を 埋め込んでおり、リリースコミットより古いコミット — それだけで、リリースのmake cleanより前と確定した。 - 同一性は Apple から。
xcrun notarytool history --output-format jsonで提出 id を得て、xcrun notarytool log <id>のsha256を読む。これが Apple が実際に審査した物の ハッシュで、公開資産のshasum -a 256(パイプせずファイルに保存してから)・ローカル zip・tap の formula と一致しなければならない。4 つのハッシュが揃えば決着。zip 内 バイナリとdist/<name>-darwin-arm64のcmpが 5 つ目。 - 時系列は誰も編集しない物から。
git reflog --date=iso(commit・amend の時刻)、ls -lT dist/(バイナリ → zip →.notarizedマーカーの順)、gh release view --json assetsのcreatedAt、紛れ込んだ成果物の mtime。1 つの表に並べる: そのビルドはmake cleanより前(消えて、パッケージされていない)か upload より後(無関係)のどちらかで、 バイナリと zip の間の数秒に入っても別のファイル名に書くだけ。 - 運用上、リリースは作業コピーを占有する。 そのリポジトリのビルドが要る別エージェントは
自分の clone で作業するか待つ。共有
dist/は 2 つの書き手が出会う場所。リリース手順 そのものが防壁(make clean && make package、verify-releaseのマーカー鮮度ゲート)で、go version -mと notary ログが事後にそれを証明する監査。