-
Notifications
You must be signed in to change notification settings - Fork 0
363 lines (341 loc) · 16.5 KB
/
Copy pathrelease.yml
File metadata and controls
363 lines (341 loc) · 16.5 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
name: release
on:
push:
tags: ['v*']
permissions:
contents: write
concurrency:
# One release run per tag, and never cancel one mid-flight: a canceled job could
# leave a half-populated draft that a later run would then publish.
group: release-${{ github.ref_name }}
cancel-in-progress: false
env:
CARGO_TERM_COLOR: always
jobs:
# Open a DRAFT release for the tag. The build jobs attach every asset to it, and the
# `publish` job flips it live last — so a release is only published once all assets are
# present. This is what makes the release immutable-safe: once a release is published its
# tag and assets freeze, so nothing can be added afterward. A draft keeps the tag unlocked
# until publish; a failed build simply leaves an unpublished draft the tag can be reused on.
create-release:
runs-on: ubuntu-latest
steps:
- name: Create draft release (idempotent)
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: |
set -euo pipefail
if gh release view "$TAG" >/dev/null 2>&1; then
# A published release is frozen under immutability — refuse to clobber assets
# onto it (e.g. a manual re-run of an already-succeeded workflow). Only an
# unpublished draft from a failed attempt is safe to reuse.
if [ "$(gh release view "$TAG" --json isDraft --jq .isDraft)" != "true" ]; then
echo "::error::release $TAG is already published — refusing to modify it. Cut a new tag for a new release."
exit 1
fi
echo "release $TAG already exists as a draft (re-run) — reusing it."
else
# A semver pre-release tag carries a hyphen (v1.0.0-rc1, v0.9.0-beta). Mark it
# --prerelease so GitHub never surfaces it as the repo's "Latest" release; a final
# tag (v1.0.0) has no hyphen and publishes as a normal release.
PRERELEASE=""
case "$TAG" in *-*) PRERELEASE="--prerelease" ;; esac
gh release create "$TAG" --draft $PRERELEASE --generate-notes --title "$TAG"
fi
linux:
needs: create-release
name: linux artifacts (gnu + musl)
runs-on: ubuntu-latest
permissions:
contents: write
id-token: write # OIDC token for build-provenance signing
attestations: write # write the provenance to the attestation API
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pinned toolchain
run: rustup show
- name: Add musl target
run: rustup target add x86_64-unknown-linux-musl
- name: Install musl tools
run: sudo apt-get update && sudo apt-get install -y musl-tools
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: .
- name: Build (gnu)
run: cargo build --release -p glass-mcp --locked
- name: Build (musl, static)
run: cargo build --release -p glass-mcp --target x86_64-unknown-linux-musl --locked
- name: Package
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
mkdir -p dist stage
pkg() { # <built-binary> <readme> <suffix>
local name="glass-mcp-${tag}-$3" dir
dir="stage/$name"
mkdir -p "$dir"
install -m0755 "$1" "$dir/glass-mcp"
strip "$dir/glass-mcp"
cp "packaging/$2" "$dir/README.md"
tar -C stage -czf "dist/$name.tar.gz" "$name"
# The bare, uncompressed binary `glass-mcp update` downloads. Same stripped file the
# archive carries, published flat so the updater needs no archive-extraction code.
cp "$dir/glass-mcp" "dist/$name"
}
pkg target/release/glass-mcp README-gnu.md x86_64-linux-gnu
pkg target/x86_64-unknown-linux-musl/release/glass-mcp README-musl.md x86_64-linux-musl
( cd dist && for f in glass-mcp-*; do case "$f" in *.sha256) ;; *) sha256sum "$f" > "$f.sha256";; esac; done )
- name: Attest build provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
dist/*.tar.gz
dist/glass-mcp-*-x86_64-linux-gnu
dist/glass-mcp-*-x86_64-linux-musl
- name: Upload to draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" dist/glass-mcp-* --clobber
windows:
needs: create-release
name: windows artifact
# The shipped glass-mcp.exe statically links the MSVC CRT (.cargo/config.toml,
# +crt-static). Cut Windows releases ONLY here (not on a dev box) for consistent build
# provenance. Toolchain/CRT redistribution licensing has open questions to confirm
# before commercial GA — see /LICENSING.md.
runs-on: windows-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
- name: Install pinned toolchain
run: rustup show
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
workspaces: .
- name: Build
run: cargo build --release -p glass-mcp --locked
# Windows Authenticode signing (Azure Trusted Signing). Until the Trusted Signing
# account is provisioned and its secrets are set, this SKIPS signing (exit 0) and
# still uploads the (unsigned) artifact — same as before. Secrets can't be read in a
# job-level `if:`, so we probe one here and gate the sign step on its output.
# Required secrets (all org-level, WINDOWS_TRUSTED_SIGNING_* prefix):
# WINDOWS_TRUSTED_SIGNING_ENDPOINT, WINDOWS_TRUSTED_SIGNING_ACCOUNT,
# WINDOWS_TRUSTED_SIGNING_CERT_PROFILE, WINDOWS_TRUSTED_SIGNING_AZURE_TENANT_ID,
# WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_ID, WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET.
# Provisioning runbook: internal.
- name: Check signing credentials
id: creds
shell: pwsh
env:
SECRET: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET }}
run: |
if ($env:SECRET) {
"present=true" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
} else {
"present=false" | Out-File -FilePath $env:GITHUB_OUTPUT -Append
Write-Output "::notice::Windows signing secrets not configured — shipping unsigned artifact."
}
- name: Sign glass-mcp.exe (Authenticode)
if: steps.creds.outputs.present == 'true'
uses: Azure/artifact-signing-action@c7ab2a863ab5f9a846ddb8265964877ef296ee82 # v2.0.0
with:
azure-tenant-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_TENANT_ID }}
azure-client-id: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_ID }}
azure-client-secret: ${{ secrets.WINDOWS_TRUSTED_SIGNING_AZURE_CLIENT_SECRET }}
endpoint: ${{ secrets.WINDOWS_TRUSTED_SIGNING_ENDPOINT }}
signing-account-name: ${{ secrets.WINDOWS_TRUSTED_SIGNING_ACCOUNT }}
certificate-profile-name: ${{ secrets.WINDOWS_TRUSTED_SIGNING_CERT_PROFILE }}
files-folder: target/release
files-folder-filter: exe
file-digest: SHA256
timestamp-rfc3161: http://timestamp.acs.microsoft.com
timestamp-digest: SHA256
- name: Package
shell: pwsh
run: |
$tag = $env:GITHUB_REF_NAME
$name = "glass-mcp-$tag-x86_64-windows"
New-Item -ItemType Directory -Force -Path "dist/$name" | Out-Null
Copy-Item target/release/glass-mcp.exe "dist/$name/glass-mcp.exe"
Copy-Item packaging/README-windows.md "dist/$name/README.md"
Compress-Archive -Path "dist/$name/*" -DestinationPath "dist/$name.zip" -Force
# The bare .exe `glass-mcp update` downloads, as for Linux above. Copied from
# target/release after the sign step, so it is Authenticode-signed exactly when the
# archived copy is: only when the signing secrets are configured (see `creds` above).
Copy-Item target/release/glass-mcp.exe "dist/$name.exe"
foreach ($f in Get-ChildItem dist/*.zip, dist/*.exe) {
$hash = (Get-FileHash $f.FullName -Algorithm SHA256).Hash.ToLower()
"$hash $($f.Name)" | Out-File "$($f.FullName).sha256" -Encoding ascii
}
- name: Attest build provenance
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
dist/*.zip
dist/*.exe
- name: Upload to draft release
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
run: |
$files = (Get-ChildItem dist/*.zip, dist/*.zip.sha256, dist/*.exe, dist/*.exe.sha256).FullName
gh release upload $env:GITHUB_REF_NAME $files --clobber
macos:
needs: create-release
name: macos artifact (universal, notarized)
runs-on: macos-latest
permissions:
contents: write
id-token: write
attestations: write
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
# The macOS release artifact needs Developer ID signing + notarization secrets.
# Until the Apple Developer enrollment is approved and these are configured, this
# job SKIPS every real step (exit 0) so tagging a release still publishes the
# Linux/Windows artifacts. Secrets can't be read in a job-level `if:`, so we probe
# one here and gate the rest on its output.
- name: Check signing credentials
id: creds
env:
CERT: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12 }}
run: |
if [ -n "$CERT" ]; then
echo "present=true" >> "$GITHUB_OUTPUT"
else
echo "present=false" >> "$GITHUB_OUTPUT"
echo "::notice::macOS signing secrets not configured — skipping notarized macOS artifact (Apple Developer enrollment pending)."
fi
- name: Install pinned toolchain
if: steps.creds.outputs.present == 'true'
run: |
rustup show
rustup target add x86_64-apple-darwin
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
if: steps.creds.outputs.present == 'true'
with:
workspaces: .
- name: Import Developer ID certificate
if: steps.creds.outputs.present == 'true'
env:
CERT_P12: ${{ secrets.MACOS_DEVELOPER_ID_CERT_P12 }}
CERT_PASSWORD: ${{ secrets.MACOS_DEVELOPER_ID_CERT_PASSWORD }}
run: |
set -euo pipefail
keychain="$RUNNER_TEMP/build.keychain-db"
kc_pw="$(openssl rand -base64 24)"
echo "SIGN_KEYCHAIN=$keychain" >> "$GITHUB_ENV"
security create-keychain -p "$kc_pw" "$keychain"
security set-keychain-settings -lut 21600 "$keychain"
security unlock-keychain -p "$kc_pw" "$keychain"
echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security import "$RUNNER_TEMP/cert.p12" -k "$keychain" -P "$CERT_PASSWORD" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$kc_pw" "$keychain" >/dev/null
# Prepend the build keychain to the search list so codesign resolves the identity.
security list-keychains -d user -s "$keychain" $(security list-keychains -d user | sed 's/[",]//g')
rm -f "$RUNNER_TEMP/cert.p12"
- name: Build + sign universal app
if: steps.creds.outputs.present == 'true'
env:
SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
./packaging/macos/build-app.sh --universal --timestamp \
--identity "$SIGN_IDENTITY" --keychain "$SIGN_KEYCHAIN" \
--version "${tag#v}" --build "${GITHUB_RUN_NUMBER}"
- name: Notarize + staple
if: steps.creds.outputs.present == 'true'
env:
NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_API_KEY_P8 }}
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }}
run: |
set -euo pipefail
echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8"
./packaging/macos/notarize.sh \
--app target/macos-app/GlassMcp.app \
--key "$RUNNER_TEMP/AuthKey.p8" \
--key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER"
rm -f "$RUNNER_TEMP/AuthKey.p8"
- name: Build + notarize .dmg
if: steps.creds.outputs.present == 'true'
env:
SIGN_IDENTITY: ${{ secrets.MACOS_SIGN_IDENTITY }}
NOTARY_KEY_P8: ${{ secrets.MACOS_NOTARY_API_KEY_P8 }}
NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_API_KEY_ID }}
NOTARY_ISSUER: ${{ secrets.MACOS_NOTARY_API_ISSUER_ID }}
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
echo "$NOTARY_KEY_P8" | base64 --decode > "$RUNNER_TEMP/AuthKey.p8"
./packaging/macos/make-dmg.sh --app target/macos-app/GlassMcp.app --out dist --version "${tag}"
./packaging/macos/notarize.sh \
--dmg "dist/glass-mcp-${tag}-universal-apple-darwin.dmg" \
--identity "$SIGN_IDENTITY" --keychain "$SIGN_KEYCHAIN" \
--key "$RUNNER_TEMP/AuthKey.p8" --key-id "$NOTARY_KEY_ID" --issuer "$NOTARY_ISSUER"
rm -f "$RUNNER_TEMP/AuthKey.p8"
- name: Package
if: steps.creds.outputs.present == 'true'
run: |
set -euo pipefail
tag="${GITHUB_REF_NAME}"
name="glass-mcp-${tag}-universal-apple-darwin"
mkdir -p dist
# The README sits beside the bundle, never inside it: a file added within
# GlassMcp.app would invalidate its signature and its notarization.
stage="$(mktemp -d)"
trap 'rm -rf "$stage"' EXIT
# ditto (not cp -R) is the guaranteed-faithful copy for a signed bundle — the same
# copy make-dmg.sh already makes of the stapled app, so the ticket survives it too.
ditto target/macos-app/GlassMcp.app "$stage/GlassMcp.app"
cp packaging/README-macos.md "$stage/README.md"
# Archive the staging directory's *contents* (no --keepParent), so the app stays at
# the root of the zip next to the README rather than nested under a folder.
ditto -c -k "$stage" "dist/${name}.zip"
( cd dist && for f in *.zip *.dmg; do shasum -a 256 "$f" > "$f.sha256"; done )
- name: Attest build provenance
if: steps.creds.outputs.present == 'true'
uses: actions/attest-build-provenance@0f67c3f4856b2e3261c31976d6725780e5e4c373 # v4.1.1
with:
subject-path: |
dist/*.zip
dist/*.dmg
- name: Upload to draft release
if: steps.creds.outputs.present == 'true'
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
run: gh release upload "$TAG" dist/*.zip dist/*.zip.sha256 dist/*.dmg dist/*.dmg.sha256 --clobber
# Flip the draft live only after every build+upload job succeeded, so the published
# release carries all its assets atomically. If any upload job failed, this job is skipped
# and the draft stays unpublished (re-run the workflow to retry). The macOS skip-gate
# composes cleanly: when its secrets are absent the macos job still *succeeds* (its real
# steps are skipped), so publish proceeds with the Linux/Windows assets.
publish:
needs: [linux, windows, macos]
runs-on: ubuntu-latest
steps:
- name: Publish the draft release
env:
GH_TOKEN: ${{ github.token }}
GH_REPO: ${{ github.repository }}
TAG: ${{ github.ref_name }}
# Reaffirm the prerelease flag at publish (idempotent) so a hyphenated tag stays a
# pre-release even if its draft was created before this flag existed. --latest=false on
# a pre-release keeps it from taking the "Latest" badge; a final tag flips both live.
run: |
set -euo pipefail
case "$TAG" in
*-*) gh release edit "$TAG" --draft=false --prerelease --latest=false ;;
*) gh release edit "$TAG" --draft=false --latest ;;
esac