-
Notifications
You must be signed in to change notification settings - Fork 6
348 lines (326 loc) · 16.4 KB
/
Copy pathpublish.yml
File metadata and controls
348 lines (326 loc) · 16.4 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
name: Publish (release fanout)
# Fans a published GitHub Release out to the package channels. Every channel
# is a thin pointer at the same attested artifact set release.yml produced —
# nothing here rebuilds binaries.
#
# All jobs run in the `release-publish` environment: add a required-reviewer
# protection rule on that environment (Settings → Environments) and every
# fanout pauses for a one-click approval before any credential is touched.
#
# One-time setup per channel (see packaging/README.md):
# crates.io — first publish by hand, then Trusted Publishing (OIDC, no
# stored secret) configured per crate for this workflow.
# AUR — secret AUR_SSH_PRIVATE_KEY (push key for the keyroost-bin repo)
# Homebrew — secret TAP_PUSH_TOKEN (fine-grained PAT, contents:write on
# the homebrew-keyroost tap repo only)
# winget — secret WINGET_TOKEN (PAT with public_repo, used by Microsoft's
# wingetcreate to PR microsoft/winget-pkgs)
# Jobs whose secret isn't configured yet skip with a notice instead of failing.
on:
release:
types: [published]
workflow_dispatch:
inputs:
tag:
description: "Release tag to fan out (e.g. vX.Y.Z)"
required: true
permissions:
contents: read
env:
TAG: ${{ github.event.release.tag_name || inputs.tag }}
jobs:
crates-io:
name: crates.io (workspace, in dependency order)
runs-on: ubuntu-latest
environment: release-publish
permissions:
contents: read
id-token: write # OIDC exchange for a temporary crates.io token
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
# `cargo publish` verifies each crate builds, so the build deps from CI
# are needed (pcsc for transport, the GL/wayland set for the GUI).
- name: Install system dependencies
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
libpcsclite-dev \
libxkbcommon-dev libwayland-dev \
libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev \
libgl1-mesa-dev libssl-dev
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable branch, 2026-06
with:
toolchain: stable
# Until Trusted Publishing is configured on crates.io this exchange
# fails — treat that like the other channels' missing secrets and skip
# with a notice instead of failing the run.
- uses: rust-lang/crates-io-auth-action@c6f97d42243bad5fab37ca0427f495c86d5b1a18 # v1.0.5
id: auth
continue-on-error: true
- name: Skip notice (trusted publishing not configured)
if: steps.auth.outcome != 'success'
run: |
echo "::notice::crates.io Trusted Publishing is not configured for this repo; skipping. See packaging/README.md for the one-time setup."
# Tiered by dependency; `cargo publish` itself waits for each crate to
# appear in the index before dependents resolve. Already-published
# versions are skipped so a partial run can be re-dispatched safely.
- name: Publish workspace crates
if: steps.auth.outcome == 'success'
env:
CARGO_REGISTRY_TOKEN: ${{ steps.auth.outputs.token }}
run: |
set -euo pipefail
version="${TAG#v}"
# Dependency order matters: keyroost-token2prog must precede
# keyroost-transport/-resolve/-keyroostctl/keyroost (they depend on
# it), and it itself only needs keyroost-proto, so it sits right
# after keyroost-token2otp. (Its very first publish was by hand — a
# brand-new crate can't be created over OIDC — after which Trusted
# Publishing handles it like the rest.) keyroost-winwebauthn is new in
# 0.7.3 and only keyroost depends on it (it has no in-tree deps of its
# own), so it sits just before the binaries; like token2prog, its FIRST
# publish must be done by hand before Trusted Publishing can take over.
# keyroost-screengrab is the same shape: a new, dep-free crate only
# keyroost uses, so it also needs a one-time hand publish first.
for crate in keyroost-proto keyroost-hid keyroost-keyring keyroost-rsakey \
keyroost-ctap keyroost-oath keyroost-openpgp keyroost-piv \
keyroost-token2otp keyroost-token2prog keyroost-import \
keyroost-transport keyroost-resolve keyroost-qr \
keyroost-winwebauthn keyroost-screengrab \
keyroostctl keyroost; do
# crates.io rejects API requests without a User-Agent (HTTP 403),
# which would make this "already published?" probe always fail and
# then try to republish an existing version (a hard error). Send a
# descriptive UA so the 200/404 distinction is real.
if curl -fsSL -H "User-Agent: keyroost-release (https://github.com/framefilter/keyroost)" \
"https://crates.io/api/v1/crates/${crate}/${version}" >/dev/null 2>&1; then
echo "${crate} ${version} already on crates.io — skipping"
continue
fi
cargo publish -p "${crate}" --locked
done
aur:
name: AUR (keyroost-bin)
runs-on: ubuntu-latest
environment: release-publish
steps:
- name: Check secret configured
id: guard
env:
KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
if [ -z "$KEY" ]; then
echo "AUR_SSH_PRIVATE_KEY not configured — skipping AUR publish"
echo "ready=false" >> "$GITHUB_OUTPUT"
else
echo "ready=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.guard.outputs.ready == 'true'
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
- name: Render and push PKGBUILD
if: steps.guard.outputs.ready == 'true'
env:
AUR_SSH_PRIVATE_KEY: ${{ secrets.AUR_SSH_PRIVATE_KEY }}
run: |
set -euo pipefail
version="${TAG#v}"
# The version is interpolated into a sed script that renders the
# PKGBUILD (which AUR users execute) — accept plain x.y.z only.
case "$version" in
*[!0-9.]*|'') echo "refusing: tag '$TAG' is not a plain x.y.z version" >&2; exit 1;;
esac
curl -fsSL --retry 4 -o SHA256SUMS \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/SHA256SUMS"
sha_linux="$(grep 'linux-x86_64' SHA256SUMS | cut -d' ' -f1)"
sha_udev="$(sha256sum udev/70-keyroost-fido.rules | cut -d' ' -f1)"
mkdir -p ~/.ssh
printf '%s\n' "$AUR_SSH_PRIVATE_KEY" > ~/.ssh/aur
chmod 600 ~/.ssh/aur
ssh-keyscan -t ed25519 aur.archlinux.org >> ~/.ssh/known_hosts 2>/dev/null
export GIT_SSH_COMMAND="ssh -i ~/.ssh/aur"
git clone ssh://aur@aur.archlinux.org/keyroost-bin.git aur-repo
for t in PKGBUILD .SRCINFO; do
sed -e "s/@VERSION@/${version}/g" \
-e "s/@SHA_LINUX@/${sha_linux}/g" \
-e "s/@SHA_UDEV@/${sha_udev}/g" \
"packaging/aur/${t}.template" > "aur-repo/${t}"
done
cd aur-repo
git config user.name "keyroost release bot"
git config user.email "release@invalid.local"
git add PKGBUILD .SRCINFO
git diff --cached --quiet && { echo "AUR already current"; exit 0; }
git commit -m "Update to ${version}"
git push origin master
homebrew:
name: Homebrew tap
runs-on: ubuntu-latest
environment: release-publish
steps:
- name: Check secret configured
id: guard
env:
KEY: ${{ secrets.TAP_PUSH_TOKEN }}
run: |
if [ -z "$KEY" ]; then
echo "TAP_PUSH_TOKEN not configured — skipping Homebrew publish"
echo "ready=false" >> "$GITHUB_OUTPUT"
else
echo "ready=true" >> "$GITHUB_OUTPUT"
fi
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
if: steps.guard.outputs.ready == 'true'
with:
ref: ${{ github.event.release.tag_name || inputs.tag }}
- name: Render and push formula
if: steps.guard.outputs.ready == 'true'
env:
TAP_PUSH_TOKEN: ${{ secrets.TAP_PUSH_TOKEN }}
run: |
set -euo pipefail
version="${TAG#v}"
# Interpolated into the sed that renders the formula — x.y.z only.
case "$version" in
*[!0-9.]*|'') echo "refusing: tag '$TAG' is not a plain x.y.z version" >&2; exit 1;;
esac
curl -fsSL --retry 4 -o SHA256SUMS \
"https://github.com/${GITHUB_REPOSITORY}/releases/download/${TAG}/SHA256SUMS"
sha_mac="$(grep 'macos-universal2' SHA256SUMS | cut -d' ' -f1)"
sha_linux="$(grep 'linux-x86_64' SHA256SUMS | cut -d' ' -f1)"
owner="${GITHUB_REPOSITORY%%/*}"
git clone "https://x-access-token:${TAP_PUSH_TOKEN}@github.com/${owner}/homebrew-keyroost.git" tap
mkdir -p tap/Formula
sed -e "s/@VERSION@/${version}/g" \
-e "s/@SHA_MAC@/${sha_mac}/g" \
-e "s/@SHA_LINUX@/${sha_linux}/g" \
packaging/homebrew/keyroost.rb.template > tap/Formula/keyroost.rb
cd tap
git config user.name "keyroost release bot"
git config user.email "release@invalid.local"
git add Formula/keyroost.rb
git diff --cached --quiet && { echo "tap already current"; exit 0; }
git commit -m "keyroost ${version}"
git push origin HEAD
winget:
name: winget (Framefilter.Keyroost, Token2-signed asset)
runs-on: windows-latest
environment: release-publish
steps:
# Decide whether this run submits, in one place. Ordered checks:
# 1. WINGET_TOKEN absent -> skip (setup not done) — notice
# 2. WINGET_TOKEN present but dead -> FAIL loudly (the expired-PAT
# silent-skip trap from the v0.7.5 postmortem)
# 3. version already in winget-pkgs -> skip (idempotent re-dispatch)
# 4. signed asset not attached yet -> skip — winget WAITS for the
# Token2-signed zip (policy, 2026-07-17): signed bytes carry
# SmartScreen cert reputation across releases, and submitting
# off the release critical path decouples us from Defender
# validation false positives on brand-new binaries (which are
# prevalence-based, self-heal by re-run, and are NOT caused by
# the signed variant circulating — that theory didn't survive
# research; see docs/superpowers/plans/2026-07-17-winget-signed-asset-flow.md).
# New rhythm: tag -> fanout (this job notices + skips) -> Token2 signs
# -> maintainer attaches keyroost-vX.Y.Z-windows-x86_64-signed.zip
# (+ .sha256) as NEW assets -> `gh workflow run publish.yml -f tag=vX.Y.Z`
# -> every other job no-ops, this one submits.
- name: Resolve signed asset and submission state
id: resolve
shell: pwsh
env:
KEY: ${{ secrets.WINGET_TOKEN }}
GH_TOKEN: ${{ github.token }}
run: |
"submit=false" | Out-File -Append $Env:GITHUB_OUTPUT
if (-not $Env:KEY) {
Write-Host "::notice::WINGET_TOKEN not configured - skipping winget publish"
exit 0
}
# 2. A present-but-expired PAT must fail loudly, not skip silently.
$headers = @{ Authorization = "token $Env:KEY" }
try {
Invoke-RestMethod -Uri "https://api.github.com/user" -Headers $headers | Out-Null
} catch {
Write-Host "::error::WINGET_TOKEN is set but rejected by the GitHub API - the PAT has likely expired. Renew it (classic PAT, public_repo scope) or the winget channel silently stops shipping."
exit 1
}
$version = $Env:TAG.TrimStart('v')
# 3. Already live in winget-pkgs? (manifests/f/Framefilter/Keyroost/<version>)
$manifestPath = "repos/microsoft/winget-pkgs/contents/manifests/f/Framefilter/Keyroost/$version"
$null = & gh api $manifestPath 2>$null
if ($LASTEXITCODE -eq 0) {
Write-Host "::notice::Framefilter.Keyroost $version already in winget-pkgs - nothing to submit"
exit 0
}
# 4. Is the Token2-signed zip attached to the release?
$signedName = "keyroost-$Env:TAG-windows-x86_64-signed.zip"
$assets = & gh release view $Env:TAG --repo $Env:GITHUB_REPOSITORY --json assets --jq '.assets[].name'
if ($LASTEXITCODE -ne 0) {
Write-Host "::error::could not read release $Env:TAG asset list"
exit 1
}
if ($assets -notcontains $signedName) {
Write-Host "::notice::winget is HOLDING for the Token2-signed build: attach $signedName (+ .sha256) to release $Env:TAG as NEW assets (never replace the CI zips - that invalidates SHA256SUMS/provenance), then re-dispatch publish.yml with tag=$Env:TAG. Every other channel job no-ops on re-dispatch."
exit 0
}
$url = "https://github.com/$Env:GITHUB_REPOSITORY/releases/download/$Env:TAG/$signedName"
"url=$url" | Out-File -Append $Env:GITHUB_OUTPUT
"submit=true" | Out-File -Append $Env:GITHUB_OUTPUT
Write-Host "signed asset present - will verify and submit $url"
# The whole point of waiting was signed bytes: prove they ARE signed
# before pointing the manifest at them. Requires a Valid Authenticode
# signature on every PE in the zip; logs the signer so a wrong-cert
# upload is visible in the run log.
- name: Verify Authenticode signature of the signed zip
if: steps.resolve.outputs.submit == 'true'
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
& gh release download $Env:TAG --repo $Env:GITHUB_REPOSITORY --pattern "keyroost-$Env:TAG-windows-x86_64-signed.zip" --output signed.zip
if ($LASTEXITCODE -ne 0) { Write-Error "download failed"; exit 1 }
Expand-Archive signed.zip -DestinationPath signed
$exes = Get-ChildItem signed -Recurse -Include *.exe,*.dll
if (-not $exes) { Write-Error "no PE files found in the signed zip"; exit 1 }
$bad = 0
foreach ($f in $exes) {
$sig = Get-AuthenticodeSignature $f.FullName
Write-Host "$($f.Name): $($sig.Status) - $($sig.SignerCertificate.Subject)"
if ($sig.Status -ne 'Valid') { $bad++ }
}
if ($bad -gt 0) {
Write-Host "::error::$bad file(s) in the '-signed' zip lack a Valid Authenticode signature - refusing to submit unsigned bytes under the signed name"
exit 1
}
# wingetcreate is Microsoft's official manifest tool; it updates the
# existing Framefilter.Keyroost manifests and opens the PR against
# microsoft/winget-pkgs. The FIRST submission must be done by hand
# (wingetcreate new, or a manual PR from packaging/winget/) — this job
# only handles version bumps.
- name: Update winget manifest
if: steps.resolve.outputs.submit == 'true'
shell: pwsh
env:
WINGET_TOKEN: ${{ secrets.WINGET_TOKEN }}
run: |
$version = $Env:TAG.TrimStart('v')
$url = "${{ steps.resolve.outputs.url }}"
# Pinned + hash-checked for the same reason the actions above are
# SHA-pinned: this executable runs with the winget PAT in env.
# Hash is from wingetcreate.exe.txt on the same release.
$wcVersion = "v1.12.8.0"
$wcSha256 = "8bd738851b524885410112678e3771b341c5c716de60fbbecb88ab0a363ed85d"
curl.exe -fsSL -o wingetcreate.exe "https://github.com/microsoft/winget-create/releases/download/$wcVersion/wingetcreate.exe"
$actual = (Get-FileHash wingetcreate.exe -Algorithm SHA256).Hash.ToLower()
if ($actual -ne $wcSha256) {
Write-Error "wingetcreate.exe hash mismatch: got $actual, expected $wcSha256"
exit 1
}
.\wingetcreate.exe update Framefilter.Keyroost `
--version $version `
--urls $url `
--submit `
--token $Env:WINGET_TOKEN