-
Notifications
You must be signed in to change notification settings - Fork 1
353 lines (346 loc) · 14.6 KB
/
Copy pathci.yml
File metadata and controls
353 lines (346 loc) · 14.6 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
name: CI
on:
push:
pull_request:
workflow_dispatch:
inputs:
build_unsigned_windows:
description: "Build SHA/run-labelled unsigned Windows test artifacts. Never publishes a release."
required: true
type: boolean
default: false
build_unsigned_linux:
description: "Build SHA/run-labelled unsigned Linux test artifacts. Never publishes a release."
required: true
type: boolean
default: false
jobs:
test:
name: Test (${{ matrix.name }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- name: Linux
os: ubuntu-latest
- name: macOS Apple Silicon
os: macos-15
- name: macOS Intel
os: macos-15-intel
- name: Windows
os: windows-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
fetch-depth: 0
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run ci
# The native macOS bridge daemon gates Screen Watch / creative-app /
# editor actions, so its Swift unit tests must pass before changes land.
# The bridge is macOS-only and needs the Xcode Swift toolchain
# (preinstalled on GitHub macOS runners), so this step is gated to the
# macOS legs of the matrix and skipped on Linux/Windows. `swift test`
# exits non-zero on failure, which fails this job.
- name: Swift bridge tests (macOS only)
if: runner.os == 'macOS'
run: |
swift --version
npm run test:swift:bridge
# iOS companion gate — the Electron matrix can't see iOS regressions, and the
# phone is the monetized launch surface. macOS 26 runner for the iOS 26 SDK
# (TaskWraithUI uses `.glassEffect`), with Xcode PINNED to 26.0 (Swift 6.2) to
# MATCH the shipping toolchain: the runner's default Xcode 26.5 (Swift 6.3.2)
# enforces region-isolation `sending` diagnostics as errors that local
# Swift 6.2.4 does not, so an unpinned lane would fail on latent concurrency
# debt rather than real regressions. (TODO: clear that Swift-6.3 region-
# isolation debt in RemoteSessionModel before bumping the pin.)
ios:
name: iOS (Kit tests)
runs-on: macos-26
steps:
- uses: actions/checkout@v4
- name: Available Xcodes (pre-pin diagnostic)
run: ls -d /Applications/Xcode*.app
- name: Select Xcode 26.0 (Swift 6.2 — matches ship toolchain)
uses: maxim-lobanov/setup-xcode@v1
with:
xcode-version: "26.0"
- name: Toolchain versions
run: xcodebuild -version && swift --version
# `swift test` builds TaskWraithKit + TaskWraithUI (all the SwiftUI/Glass
# code) for the host and runs the 73 unit tests — the real regression
# gate. An app-target `xcodebuild` is intentionally NOT run here: the
# pinned Xcode 26.0 lacks the iOS simulator PLATFORM (GitHub pre-installs
# it only for the runner's default Xcode 26.5), and `-downloadPlatform iOS`
# would add minutes + GBs per run just to compile the thin app wrapper.
- name: Swift package tests (TaskWraithKit)
run: swift test --package-path ios/TaskWraithKit
notarized-macos-release:
name: Notarized macOS Release
runs-on: macos-15
needs: [test, ios]
if: startsWith(github.ref, 'refs/tags/v') && vars.ENABLE_MACOS_NOTARIZED_RELEASE == 'true'
permissions:
actions: read
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run security:deps
- name: Prepare macOS artifacts without signing credentials
run: |
npm run clean:dist
npm run test:swift:bridge
TASKWRAITH_BRIDGE_ARCH=universal npm run prebuild:bridge-daemon
node scripts/smoke-bridge-daemon-roundtrip.cjs
npm run prepare:mac-universal-deps
npm run build
- name: Sign, package, and notarize macOS artifacts
env:
CSC_LINK: ${{ secrets.MACOS_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.MACOS_CSC_KEY_PASSWORD }}
CSC_NAME: ${{ secrets.MACOS_CSC_NAME }}
APPLE_KEYCHAIN_PROFILE: ${{ secrets.APPLE_KEYCHAIN_PROFILE }}
APPLE_ID: ${{ secrets.APPLE_ID }}
APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: >-
node scripts/run-electron-builder.cjs --mac --universal --publish never
-c.npmRebuild=false -c.mac.notarize=true
- name: Run post-package macOS checks without signing credentials
run: |
node scripts/smoke-packaged-electron.cjs dist
npm run validate:mac-update-feed
npm run security:sbom
- name: Verify notarized macOS artifacts
run: |
set -euo pipefail
test -d "dist/mac-universal/TaskWraith.app"
codesign --verify --deep --strict --verbose=2 "dist/mac-universal/TaskWraith.app"
spctl --assess --type execute --verbose=4 "dist/mac-universal/TaskWraith.app"
xcrun stapler validate "dist/mac-universal/TaskWraith.app"
for artifact in dist/*.dmg; do
test -f "$artifact"
xcrun stapler validate "$artifact"
done
- name: Upload verified macOS release artifacts
env:
GH_TOKEN: ${{ github.token }}
run: |
set -euo pipefail
shopt -s nullglob
assets=(dist/*.dmg dist/*.zip dist/*.blockmap dist/latest-mac.yml dist/sbom.cdx.json)
if [ "${#assets[@]}" -lt 3 ]; then
echo "Expected notarized macOS release artifacts in dist/, found ${#assets[@]}."
exit 1
fi
release_tag="$GITHUB_REF_NAME"
candidate_sha="$(git rev-parse HEAD)"
verify_remote_release_tag() {
local tag_ref="refs/tags/$release_tag"
local tag_type
local remote_tag_sha
git fetch --force --no-tags origin "+$tag_ref:$tag_ref"
tag_type="$(git cat-file -t "$tag_ref")"
if [ "$tag_type" = "tag" ]; then
remote_tag_sha="$(git rev-parse "$tag_ref^{}")"
elif [ "$tag_type" = "commit" ]; then
remote_tag_sha="$(git rev-parse "$tag_ref")"
else
echo "Release tag $tag_ref has unsupported object type $tag_type."
return 1
fi
if [ "$remote_tag_sha" != "$candidate_sha" ]; then
echo "Remote release tag resolves to $remote_tag_sha; expected $candidate_sha."
return 1
fi
}
verify_remote_release_tag
gh release view "$release_tag" >/dev/null 2>&1 || \
gh release create "$release_tag" --verify-tag --title "$release_tag" --notes "Verified TaskWraith release artifacts for $release_tag." || \
gh release view "$release_tag" >/dev/null
# Re-resolve after release creation and immediately before mutation.
verify_remote_release_tag
gh release upload "$release_tag" "${assets[@]}" --clobber
signed-windows-release:
name: Signed Windows Release
runs-on: windows-latest
needs: [test, ios]
if: startsWith(github.ref, 'refs/tags/v') && vars.ENABLE_WINDOWS_SIGNED_RELEASE == 'true'
permissions:
actions: read
contents: write
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
with:
persist-credentials: false
- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run security:deps
- name: Compile Windows artifacts without signing credentials
run: npm run build:win:compile
- name: Sign and package Windows artifacts
env:
CSC_LINK: ${{ secrets.WINDOWS_CSC_LINK }}
CSC_KEY_PASSWORD: ${{ secrets.WINDOWS_CSC_KEY_PASSWORD }}
shell: pwsh
run: |
node scripts/require-windows-signing-env.cjs
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
& .\node_modules\.bin\electron-builder.cmd --win --x64 --arm64 --publish never
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
- name: Run post-package Windows checks without signing credentials
run: npm run build:win:checks
- name: Verify signed Windows artifacts
shell: pwsh
run: |
$ErrorActionPreference = "Stop"
$installers = Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe"
if ($installers.Count -lt 2) {
throw "Expected signed x64 and arm64 Windows installers in dist/."
}
foreach ($installer in $installers) {
$signature = Get-AuthenticodeSignature -FilePath $installer.FullName
if ($signature.Status -ne "Valid") {
throw "Invalid Authenticode signature for $($installer.Name): $($signature.Status)"
}
}
$version = (Get-Content package.json | ConvertFrom-Json).version
$feedPrefix = if ($version -match "-") { "beta" } else { "latest" }
foreach ($arch in @("x64", "arm64")) {
$feed = "dist/$feedPrefix-win-$arch.yml"
if (!(Test-Path $feed)) {
throw "Missing $feed"
}
}
- name: Upload verified Windows release artifacts
shell: pwsh
env:
GH_TOKEN: ${{ github.token }}
run: |
$ErrorActionPreference = "Stop"
$assets = @(
Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe"
Get-ChildItem -Path dist -Filter "TaskWraith-*-win-*-setup.exe.blockmap"
Get-ChildItem -Path dist -Filter "*-win-*.yml"
) | ForEach-Object { $_.FullName }
if ($assets.Count -lt 4) {
throw "Expected signed Windows installers, blockmaps, and update feeds in dist/, found $($assets.Count)."
}
$releaseTag = $env:GITHUB_REF_NAME
$candidateSha = (git rev-parse HEAD).Trim()
if ($LASTEXITCODE -ne 0) {
throw "Could not resolve the checked-out release commit."
}
function Assert-RemoteReleaseTag {
$tagRef = "refs/tags/$releaseTag"
$refspec = "+{0}:{0}" -f $tagRef
git fetch --force --no-tags origin $refspec
if ($LASTEXITCODE -ne 0) {
throw "Could not fetch exact remote release tag $tagRef."
}
$tagType = (git cat-file -t $tagRef).Trim()
if ($LASTEXITCODE -ne 0) {
throw "Could not inspect remote release tag $tagRef."
}
if ($tagType -eq "tag") {
$remoteTagSha = (git rev-parse "$tagRef^{}").Trim()
} elseif ($tagType -eq "commit") {
$remoteTagSha = (git rev-parse $tagRef).Trim()
} else {
throw "Release tag $tagRef has unsupported object type $tagType."
}
if ($LASTEXITCODE -ne 0) {
throw "Could not resolve remote release tag $tagRef."
}
if ($remoteTagSha -ne $candidateSha) {
throw "Remote release tag resolves to $remoteTagSha; expected $candidateSha."
}
}
Assert-RemoteReleaseTag
gh release view "$releaseTag" *> $null
if ($LASTEXITCODE -ne 0) {
gh release create "$releaseTag" --verify-tag --title "$releaseTag" --notes "Verified TaskWraith release artifacts for $releaseTag."
if ($LASTEXITCODE -ne 0) {
gh release view "$releaseTag" *> $null
if ($LASTEXITCODE -ne 0) {
throw "Could not create or verify GitHub release $releaseTag."
}
}
}
# Re-resolve after release creation and immediately before mutation.
Assert-RemoteReleaseTag
gh release upload "$releaseTag" @assets --clobber
unsigned-windows-build:
name: Unsigned Windows Build (testing)
runs-on: windows-latest
needs: [test, ios]
if: github.event_name == 'workflow_dispatch' && inputs.build_unsigned_windows == true
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run security:deps
# `build:win:nopublish` shares build:win's recipe (compile + electron-builder
# + packaged-app smoke + update-feed write/validate) but forces
# `--publish never`. The packaged-app launch smoke skips the arm64 binary on
# this x64 runner because it cannot execute it natively.
- run: npm run build:win:nopublish
- name: Upload SHA/run-labelled unsigned Windows test artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: unsigned-windows-testing-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
dist/TaskWraith-*-win-*-setup.exe
dist/TaskWraith-*-win-*-setup.exe.blockmap
dist/*-win-*.yml
if-no-files-found: error
retention-days: 14
unsigned-linux-build:
name: Unsigned Linux Build (testing)
runs-on: ubuntu-latest
needs: [test, ios]
if: github.event_name == 'workflow_dispatch' && inputs.build_unsigned_linux == true
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run security:deps
# AppImage + deb only — snap needs snapcraft (absent on the default
# runner). `--publish never` keeps this manual testing lane outside the
# GitHub Release publication surface.
- run: npm run build:linux:nopublish
- name: Upload SHA/run-labelled unsigned Linux test artifacts
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: unsigned-linux-testing-${{ github.sha }}-${{ github.run_id }}-${{ github.run_attempt }}
path: |
dist/*.AppImage
dist/*.deb
dist/latest-linux*.yml
if-no-files-found: error
retention-days: 14