Repository navigation
409 lines (375 loc) · 21 KB
/
Copy pathrelease.yml
File metadata and controls
409 lines (375 loc) · 21 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
name: Release
# リリース公開 (gh release create) をトリガに 5 ターゲットをビルドし、
# ビルド済みバイナリ + SHA256 チェックサム + インストーラをそのリリースに添付する。
# 手動再実行: gh workflow run release.yml -f tag=v0.1.0
# (タグ指定は Resolve tag ステップで inputs.tag に落ちる。checkout も
# そのタグを ref にするので、後からの再実行でも当時のソースが再現される。
# アップロードは --clobber なので既存アセットを安全に上書きできる。
# 前提: そのタグの GitHub Release が既に存在すること。無いと gh release upload が失敗する。)
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag (e.g. v0.1.0)"
required: true
# ■ 既定はゼロ。要る権限は**ジョブ単位で足す**。
# このワークフローは `contents: write` を持つ = リリース資産を差し替えられる。
# ワークフロー全体に配ると、侵害された 1 つのアクションが全ジョブでその力を持つ。
permissions: {}
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: ${{ matrix.label }}
runs-on: ${{ matrix.runner }}
permissions:
contents: write # gh release upload
id-token: write # 来歴 (provenance) の署名を Sigstore から受け取る
attestations: write # 来歴を GitHub の attestation ストアへ書く
strategy:
fail-fast: false
matrix:
include:
- { label: linux-x86_64, runner: ubuntu-latest, target: x86_64-unknown-linux-gnu, archive: tar }
- { label: linux-arm64, runner: ubuntu-24.04-arm, target: aarch64-unknown-linux-gnu, archive: tar }
- { label: macos-arm64, runner: macos-latest, target: aarch64-apple-darwin, archive: tar }
- { label: macos-x86_64, runner: macos-latest, target: x86_64-apple-darwin, archive: tar }
- { label: windows-x86_64, runner: windows-latest, target: x86_64-pc-windows-msvc, archive: zip }
steps:
- name: Resolve tag
id: tag
shell: bash
run: echo "tag=${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.tag.outputs.tag }}
# SHA 固定すると github.action_ref が SHA になり、ref 名から toolchain を
# 拾えなくなる。**toolchain: を必ず明示する**。
- uses: dtolnay/rust-toolchain@4360b52568e2003a75bf9bc1d59f33a8e3fc893c # stable @2026-08-05
with:
toolchain: stable
targets: ${{ matrix.target }}
# 依存クレートのビルド結果をタグ間で使い回す。これが無いと毎リリース
# フルビルドになり、待ち時間が数倍になる (キーはリリースタグに依存しない)。
- uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2
with:
key: ${{ matrix.target }}
cache-on-failure: true
- name: Install Linux build deps
if: startsWith(matrix.label, 'linux')
# 時限と再試行は道具の側に 1 本だけ (`--self-test` で毎回証明する)。
run: sh tools/ci-linux-deps.sh
# アーカイブ名・実行ファイル名を 1 か所で決める。
# 以降のステップ (package / checksum / size / upload) はこの出力だけを見るので、
# 命名規則を変えるときの修正箇所が 1 つに収まる。
# install.sh / install.ps1 が組み立てる URL と同じ規則であること:
# install.sh -> zai-$tag-$os-$arch.tar.gz
# install.ps1 -> zai-$tag-windows-x86_64.zip
- name: Artifact name
id: art
shell: bash
run: |
set -euo pipefail
name="zai-${{ steps.tag.outputs.tag }}-${{ matrix.label }}"
if [ '${{ matrix.archive }}' = 'zip' ]; then
echo "file=$name.zip" >> "$GITHUB_OUTPUT"
echo "bin=target/${{ matrix.target }}/release/zai.exe" >> "$GITHUB_OUTPUT"
else
echo "file=$name.tar.gz" >> "$GITHUB_OUTPUT"
echo "bin=target/${{ matrix.target }}/release/zai" >> "$GITHUB_OUTPUT"
fi
echo "name=$name" >> "$GITHUB_OUTPUT"
- name: Build
run: cargo build --release --locked --target ${{ matrix.target }}
# ── コード署名 ────────────────────────────────────────────────────
# 署名鍵はリポジトリに置けないので、**secrets が設定された環境でだけ**動く。
# 未設定でもリリースは通る (今日の配布を壊さない) が、**署名の有無は
# 必ずサマリに出す** — 「たぶん署名されている」を作らないため。
#
# ⚠ このリポジトリにはまだ証明書 secrets が無いため、下の 2 ステップは
# 一度も実行されたことがない (常に skip)。有効化の手順と必要な secrets は
# docs/signing.md にまとめてある。
- name: Sign + notarize (macOS)
if: runner.os == 'macOS' && env.MACOS_CERT_P12 != ''
env:
MACOS_CERT_P12: ${{ secrets.MACOS_CERT_P12 }}
MACOS_CERT_PASSWORD: ${{ secrets.MACOS_CERT_PASSWORD }}
MACOS_SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
MACOS_NOTARY_APPLE_ID: ${{ secrets.MACOS_NOTARY_APPLE_ID }}
MACOS_NOTARY_TEAM_ID: ${{ secrets.MACOS_NOTARY_TEAM_ID }}
MACOS_NOTARY_PASSWORD: ${{ secrets.MACOS_NOTARY_PASSWORD }}
run: |
set -euo pipefail
bin='${{ steps.art.outputs.bin }}'
# 専用の一時キーチェーンを作る (ランナーの login キーチェーンを汚さない)。
kc="$RUNNER_TEMP/zai-signing.keychain-db"
kcpw=$(uuidgen)
security create-keychain -p "$kcpw" "$kc"
security set-keychain-settings -lut 900 "$kc"
security unlock-keychain -p "$kcpw" "$kc"
printf '%s' "$MACOS_CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$kc" -P "$MACOS_CERT_PASSWORD" \
-T /usr/bin/codesign -T /usr/bin/security
rm -f "$RUNNER_TEMP/cert.p12"
# UI プロンプト無しで鍵を使わせる (これが無いと codesign が固まる)。
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kcpw" "$kc" >/dev/null
security list-keychains -d user -s "$kc"
# --options runtime (Hardened Runtime) と --timestamp は公証の必須条件。
codesign --force --timestamp --options runtime --sign "$MACOS_SIGN_IDENTITY" "$bin"
codesign --verify --strict --verbose=2 "$bin"
# notarytool が受け取るのは .zip / .pkg / .dmg のみ。tar.gz は通らないので
# 単体バイナリを zip に包んで出す。
ditto -c -k --keepParent "$bin" "$RUNNER_TEMP/notarize.zip"
xcrun notarytool submit "$RUNNER_TEMP/notarize.zip" \
--apple-id "$MACOS_NOTARY_APPLE_ID" \
--team-id "$MACOS_NOTARY_TEAM_ID" \
--password "$MACOS_NOTARY_PASSWORD" --wait
# 単体の実行ファイルには stapler が使えない (.app / .dmg / .pkg のみ)。
# 公証チケットはオンライン照会されるので staple はしない。
security delete-keychain "$kc"
echo "MACOS_SIGNED=1" >> "$GITHUB_ENV"
- name: Sign (Windows Authenticode)
if: runner.os == 'Windows' && env.WINDOWS_CERT_PFX != ''
shell: pwsh
env:
WINDOWS_CERT_PFX: ${{ secrets.WINDOWS_CERT_PFX }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
run: |
$ErrorActionPreference = 'Stop'
$pfx = Join-Path $env:RUNNER_TEMP 'zai-cert.pfx'
[IO.File]::WriteAllBytes($pfx, [Convert]::FromBase64String($env:WINDOWS_CERT_PFX))
# signtool.exe は Windows SDK 同梱。SDK の版はランナー更新で変わるので探す
# (パスを直書きすると、ランナーが更新された日に静かに壊れる)。
$tool = Get-ChildItem "${env:ProgramFiles(x86)}\Windows Kits\10\bin" -Recurse -Filter signtool.exe -ErrorAction SilentlyContinue |
Where-Object { $_.FullName -like '*\x64\*' } |
Sort-Object FullName -Descending | Select-Object -First 1
if (-not $tool) { throw 'signtool.exe が見つかりません (Windows SDK 未導入)' }
& $tool.FullName sign /fd SHA256 /td SHA256 /tr http://timestamp.digicert.com `
/f $pfx /p $env:WINDOWS_CERT_PASSWORD '${{ steps.art.outputs.bin }}'
if ($LASTEXITCODE -ne 0) { throw "signtool sign が失敗しました ($LASTEXITCODE)" }
Remove-Item $pfx -Force
& $tool.FullName verify /pa '${{ steps.art.outputs.bin }}'
if ($LASTEXITCODE -ne 0) { throw "signtool verify が失敗しました ($LASTEXITCODE)" }
"WINDOWS_SIGNED=1" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
- name: Package (tar.gz)
if: matrix.archive == 'tar'
shell: bash
run: |
name='${{ steps.art.outputs.name }}'
mkdir "$name"
cp "target/${{ matrix.target }}/release/zai" LICENSE README.md "$name/"
tar czf "$name.tar.gz" "$name"
- name: Package (zip)
if: matrix.archive == 'zip'
shell: pwsh
run: |
$name = '${{ steps.art.outputs.name }}'
New-Item -ItemType Directory -Path $name | Out-Null
Copy-Item "target/${{ matrix.target }}/release/zai.exe", "LICENSE", "README.md" $name
Compress-Archive -Path $name -DestinationPath "$name.zip"
# ■ チェックサムは「そのアーカイブを作ったランナー上で」計算する。
# 後からどこかで再ダウンロードして計算し直すと、経路上の改変を検出できなくなる。
# 出力は sha256sum(1) の標準形 "<64桁hex><SP><SP><filename>" に統一する
# (install.sh 側で `sha256sum -c` / `shasum -a 256 -c` がそのまま使える形)。
- name: SHA256 (Linux / macOS)
if: runner.os != 'Windows'
shell: bash
run: |
set -euo pipefail
f='${{ steps.art.outputs.file }}'
out='sha256-${{ matrix.label }}.txt'
# Linux は coreutils の sha256sum、macOS は sha256sum が無いので shasum -a 256。
# どちらも既定の出力書式が標準形なので整形不要。
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$f" > "$out"
else
shasum -a 256 "$f" > "$out"
fi
cat "$out"
- name: SHA256 (Windows)
if: runner.os == 'Windows'
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
$f = '${{ steps.art.outputs.file }}'
$out = 'sha256-${{ matrix.label }}.txt'
# Get-FileHash は大文字 hex を返す。sha256sum は小文字なので揃える
# (揃えないと Linux 上の `sha256sum -c` が不一致扱いにする)。
$h = (Get-FileHash -Algorithm SHA256 -LiteralPath $f).Hash.ToLower()
# BOM 無し・LF 固定で書く。PowerShell の既定 (CRLF / 場合により BOM) のままだと
# 結合後の checksums.txt でファイル名末尾に \r が付き、
# Linux 側の検証が「そんなファイルは無い」で必ず失敗する。
[System.IO.File]::WriteAllText(
(Join-Path $PWD $out),
"$h $f`n",
(New-Object System.Text.UTF8Encoding($false)))
Get-Content $out
# 単一バイナリ配布はこの製品の価値そのもの (競合は Electron で 190MB)。
# サイズを毎リリース可視化して、太ったことに後から気付く事故を防ぐ。
- name: Size report
shell: bash
run: |
set -euo pipefail
{
echo "### ${{ matrix.label }}"
echo ""
echo "| item | bytes | MB |"
echo "| --- | ---: | ---: |"
for p in '${{ steps.art.outputs.bin }}' '${{ steps.art.outputs.file }}'; do
b=$(wc -c < "$p" | tr -d ' ')
# awk / basename に依存しない (Windows ランナーの Git Bash でも確実に動く)。
# 小数第1位までを整数演算で作る: 10 倍して割り、商と余りで組み立てる。
t=$(( b * 10 / 1048576 ))
printf '| %s | %s | %d.%d |\n' "${p##*/}" "$b" "$(( t / 10 ))" "$(( t % 10 ))"
done
} >> "$GITHUB_STEP_SUMMARY"
# 断片を集約ジョブへ渡す。ファイル名がターゲットごとに一意なので
# download-artifact の merge-multiple で 1 ディレクトリに平坦展開できる。
- name: Upload checksum fragment
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: sha256-${{ matrix.label }}
path: sha256-${{ matrix.label }}.txt
if-no-files-found: error
retention-days: 7
# 署名されたか否かを**必ず**出す。出さないと「署名されているつもり」の
# リリースが黙って出る (secrets を消した日に静かに無署名へ戻る)。
- name: Signing report
if: always()
shell: bash
run: |
set -euo pipefail
case '${{ runner.os }}' in
macOS) state="${MACOS_SIGNED:-}" ; kind="Developer ID + notarization" ;;
Windows) state="${WINDOWS_SIGNED:-}"; kind="Authenticode" ;;
*) state="n/a" ; kind="(Linux は OS 署名の仕組みを持たない)" ;;
esac
if [ "$state" = "1" ]; then
echo "- **${{ matrix.label }}**: 署名済み ($kind)" >> "$GITHUB_STEP_SUMMARY"
elif [ "$state" = "n/a" ]; then
echo "- **${{ matrix.label }}**: $kind — 来歴 (attestation) で担保" >> "$GITHUB_STEP_SUMMARY"
else
echo "- **${{ matrix.label }}**: ⚠ **未署名** ($kind の secrets が未設定 / docs/signing.md)" >> "$GITHUB_STEP_SUMMARY"
fi
# ■ 来歴 (build provenance)。証明書を 1 円も買わずに今日から効く担保。
# 「このアーカイブは、この repo の、このワークフローの、このコミットから
# GitHub のランナー上で作られた」を Sigstore が署名した SLSA の証明として残す。
# 利用者の検証: gh attestation verify <file> --repo ${{ github.repository }}
#
# コード署名 (Gatekeeper / SmartScreen) の代わりにはならない。
# **別の質問に答える**もので、両方あって初めて配布物の身元が揃う。
- name: Attest build provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ steps.art.outputs.file }}
- name: Upload to release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: gh release upload "${{ steps.tag.outputs.tag }}" '${{ steps.art.outputs.file }}' --clobber
# 5 ターゲット分の断片を 1 枚の checksums.txt にまとめてリリースへ添付する。
# needs: build なので、1 ターゲットでもビルドが落ちれば このジョブは skip され、
# 「一部しか載っていない checksums.txt」が世に出ない (fail-closed)。
checksums:
name: checksums.txt + SBOM + installers
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: write # gh release upload
id-token: write # SBOM の来歴署名
attestations: write
steps:
- name: Resolve tag
id: tag
shell: bash
run: echo "tag=${{ github.event_name == 'release' && github.event.release.tag_name || inputs.tag }}" >> "$GITHUB_OUTPUT"
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.tag.outputs.tag }}
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: sha256-*
path: parts
merge-multiple: true
# ■ SBOM (CycloneDX)。この版が何を取り込んでいるかの一覧を配布物と一緒に配る。
# 新しい脆弱性が出たときに「自分の入れた zai は影響あるか」を、
# 利用者がこちらの発表を待たずに判定できる。
# Cargo.lock を読むだけなのでビルド不要 = 数秒で終わる。
- uses: taiki-e/install-action@4cef1412cce204788f482e778a0b9187f9626a29 # v2.87.21
with:
tool: cargo-cyclonedx
- name: Generate SBOM
id: sbom
shell: bash
run: |
set -euo pipefail
# --override-filename は拡張子 .json を自動で足すので、ここでは
# 「.cdx」までを渡して zai-<tag>-sbom.cdx.json を作らせる。
# --target all: 配布 5 ターゲット分の依存を全部入れる (host だけだと
# Windows / Linux 専用の依存が SBOM から丸ごと落ちる)。
# --all: 直接依存だけでなく推移依存も列挙する。
stem="zai-${{ steps.tag.outputs.tag }}-sbom.cdx"
cargo cyclonedx --format json --spec-version 1.5 --all --target all \
--override-filename "$stem"
out="$stem.json"
echo "file=$out" >> "$GITHUB_OUTPUT"
# 中身が空でないことをここで確かめる (空の SBOM は嘘の安心を配る)。
# 手元の実測は 546 components / 548 purl なので 100 は十分に低い床。
n=$(grep -c '"purl"' "$out" || true)
[ "$n" -gt 100 ] || { echo "::error::SBOM の中身が少なすぎる ($n purl)"; exit 1; }
echo "SBOM: $out ($n purl)"
- name: Attest SBOM provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-path: ${{ steps.sbom.outputs.file }}
- name: Assemble checksums.txt
shell: bash
run: |
set -euo pipefail
# インストーラ自身のハッシュも載せる。install.sh を curl | sh する利用者が
# 「実行する前にスクリプトを検証する」導線を持てるようにするため。
cat parts/sha256-*.txt > merged.txt
# SBOM も検証できる資産にする。載せないと「リリースに居るのに
# 誰も確かめられないファイル」が 1 つ増える。
sha256sum install.sh install.ps1 '${{ steps.sbom.outputs.file }}' >> merged.txt
# CR を除去 (Windows 断片の保険) し、ファイル名でソートして決定的にする。
tr -d '\r' < merged.txt | LC_ALL=C sort -k2 > checksums.txt
# 形式検査: 1 行でも標準形を外れたら公開しない。
# [^ ]+ ではなく [^[:space:]]+ を使う: 前者は末尾 CR を「非スペース」として
# 受理してしまい、CRLF 混入を素通しする (ローカルで実証済み)。
# ここを緩めると install.sh 側の検証が静かに素通りするようになる。
if grep -qvE '^[0-9a-f]{64} [^[:space:]]+$' checksums.txt; then
echo "::error::checksums.txt に標準形でない行がある"
grep -nvE '^[0-9a-f]{64} [^[:space:]]+$' checksums.txt
exit 1
fi
# 5 ターゲット + install.sh + install.ps1 + SBOM = 8 行。
# 資産を増やすときはこの数字も必ず一緒に直す
# (直さないと「載せたつもりの資産」が黙って欠ける)。
n=$(wc -l < checksums.txt)
if [ "$n" -ne 8 ]; then
echo "::error::checksums.txt は 8 行のはずが $n 行だった (ターゲット欠落の疑い)"
cat checksums.txt
exit 1
fi
cat checksums.txt
{
echo "### checksums.txt"
echo ""
echo '```'
cat checksums.txt
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
# install.sh / install.ps1 をリリースアセットとしても配る。
# 理由: main の HEAD ではなくタグに固定されたインストーラを指せるようになり、
# 「v0.5.1 を入れた手順」を後から完全に再現できる。ファイルは数 KB なのでコストは無い。
- name: Upload checksums + SBOM + installers
shell: bash
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${{ steps.tag.outputs.tag }}" \
checksums.txt '${{ steps.sbom.outputs.file }}' install.sh install.ps1 --clobber