返回 DeepSeek-Reasonix
ci.yml
根目录 / .github / workflows / ci.yml
1 name: CI
2
3 on:
4 push:
5 branches: [main-v2]
6 pull_request:
7 branches: [main-v2]
8
9 permissions:
10 contents: read
11
12 concurrency:
13 # Push cancellation is handled by Supersede CI, which preserves the code
14 # ancestor still required by a release-notes-only candidate.
15 group: ci-${{ github.event_name == 'push' && github.sha || github.ref }}
16 cancel-in-progress: true
17
18 jobs:
19 # Cheap path gate for pull requests. PRs confined to docs/site/release-notes
20 # (or top-level Markdown) skip the heavy Go jobs, and PRs that cannot affect
21 # the desktop module skip the desktop jobs. Job-level `if` reports skipped,
22 # which satisfies the required status checks (lint, race, test) — a
23 # workflow-level paths-ignore would leave required checks pending and block
24 # merges. Gated jobs skip only on an explicit `false` output: wrapped in
25 # `always()`, a failed `changes` job (or a missing output) makes them run
26 # the full matrix instead of silently passing required checks as skipped.
27 # Pushes to main-v2 always run everything.
28 changes:
29 runs-on: ubuntu-latest
30 outputs:
31 code: ${{ steps.filter.outputs.code }}
32 desktop: ${{ steps.filter.outputs.desktop }}
33 desktop_go: ${{ steps.filter.outputs.desktop_go }}
34 frontend: ${{ steps.filter.outputs.frontend }}
35 browser: ${{ steps.filter.outputs.browser }}
36 memory: ${{ steps.filter.outputs.memory }}
37 electron: ${{ steps.filter.outputs.electron }}
38 native: ${{ steps.filter.outputs.native }}
39 packaging: ${{ steps.filter.outputs.packaging }}
40 site: ${{ steps.filter.outputs.site }}
41 sdk: ${{ steps.filter.outputs.sdk }}
42 windows_builtin: ${{ steps.filter.outputs.windows_builtin }}
43 release_control: ${{ steps.filter.outputs.release_control }}
44 notes_only: ${{ steps.filter.outputs.notes_only }}
45 steps:
46 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
47 with:
48 fetch-depth: 0
49 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
50 with:
51 node-version-file: .node-version
52 - id: filter
53 env:
54 EVENT_NAME: ${{ github.event_name }}
55 BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }}
56 HEAD_SHA: ${{ github.event.pull_request.head.sha || github.sha }}
57 run: |
58 args=(--head "$HEAD_SHA" --github-output "$GITHUB_OUTPUT" --summary "$GITHUB_STEP_SUMMARY")
59 if [ "$EVENT_NAME" = pull_request ]; then
60 args+=(--base "$BASE_SHA" --mode pull_request)
61 else
62 # Main pushes retain the complete qualification matrix after the
63 # existing release-notes-only exception is evaluated.
64 args+=(--base "$BASE_SHA" --mode push --full)
65 fi
66 node scripts/ci-paths.mjs "${args[@]}"
67
68 # The ruleset requires the per-OS check names (test (ubuntu-latest) etc.).
69 # A matrix job skipped at job level reports no per-leg checks at all, so
70 # those required checks would stay "Expected" and block merging. The job
71 # therefore always runs and the steps do the gating: when the changes
72 # detector reports the diff is unrelated, every step skips and each leg
73 # reports success in seconds. `always()` also keeps the legs alive when
74 # the changes job itself fails (fail-open: an empty output != 'false').
75 test:
76 needs: changes
77 if: ${{ !cancelled() }}
78 # Backstop against a wedged step holding the workflow's concurrency group:
79 # the Windows full-suite step has outlived its own timeout and kept the job
80 # alive. Normal full-suite wall time is 8-12 minutes per platform.
81 timeout-minutes: 45
82 strategy:
83 fail-fast: false
84 matrix:
85 os: [ubuntu-latest, macos-latest, windows-latest]
86 runs-on: ${{ matrix.os }}
87 env:
88 RUN_STEPS: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false' }}
89 steps:
90 - if: env.RUN_STEPS == 'true'
91 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
92
93 - if: env.RUN_STEPS == 'true'
94 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
95 with:
96 go-version-file: go.mod
97 # Windows keeps setup-go's go.sum-keyed cache: restoring the per-run
98 # cache there measured slower (17m -> 21m, then a 25-minute step
99 # timeout), so the rolling cache serves only the Unix legs.
100 cache: ${{ runner.os == 'Windows' }}
101
102 - if: env.RUN_STEPS == 'true' && runner.os != 'Windows'
103 uses: ./.github/actions/go-build-cache
104 id: gocache
105 with:
106 module: root
107
108 # Both non-Linux legs drive their package selection through a Node script.
109 - if: env.RUN_STEPS == 'true' && runner.os != 'Linux'
110 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
111 with:
112 node-version: "22"
113
114 # Defender real-time scanning on GitHub Windows runners intermittently
115 # crashes Go test binaries mid-syscall (DEP fault at PC=0 in the Windows
116 # syscall trampoline, e.g. golang/go#67139) and briefly keeps freshly
117 # written test files open, causing flaky sharing violations. Exclude the
118 # ephemeral build/test locations; fail this lane when the prerequisite
119 # cannot be established so a DEP crash is never treated as product evidence.
120 - name: Exclude build dirs from Defender scanning
121 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
122 shell: pwsh
123 run: |
124 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
125 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
126 foreach ($p in $paths) {
127 try {
128 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
129 Write-Host "Defender exclusion added: $p"
130 } catch {
131 throw "Defender exclusion failed for ${p}: $($_.Exception.Message)"
132 }
133 }
134
135 - name: Install and verify Linux sandbox backend
136 if: env.RUN_STEPS == 'true' && runner.os == 'Linux'
137 run: |
138 sudo apt-get update
139 sudo apt-get install -y bubblewrap
140 if sysctl -n kernel.unprivileged_userns_clone >/dev/null 2>&1; then
141 sudo sysctl -w kernel.unprivileged_userns_clone=1
142 fi
143 if sysctl -n kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
144 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
145 fi
146 bwrap --ro-bind / / --dev /dev --proc /proc -- true || \
147 echo "::warning::bubblewrap is installed but this runner denies user namespaces; unavailable-backend tests will run fail-closed"
148
149 # Skipped on Windows: the runner checks out CRLF, so gofmt -l flags every
150 # file. gofmt output is OS-independent, so the Unix legs already cover it.
151 - name: gofmt
152 if: env.RUN_STEPS == 'true' && runner.os != 'Windows'
153 run: |
154 # Root module only — desktop/ is a separate module with its own tooling.
155 unformatted=$(gofmt -l . | grep -v '^desktop/' || true)
156 if [ -n "$unformatted" ]; then
157 echo "These files are not gofmt-clean:"
158 echo "$unformatted"
159 exit 1
160 fi
161
162 - name: vet
163 if: env.RUN_STEPS == 'true'
164 run: go vet ./...
165
166 - name: build
167 if: env.RUN_STEPS == 'true'
168 run: go build ./...
169
170 - name: test
171 # Windows full coverage belongs to the disjoint selectors below and
172 # the isolated jobs; running ./... here would execute every test twice.
173 if: env.RUN_STEPS == 'true' && (runner.os == 'Linux' || (runner.os == 'macOS' && github.event_name != 'pull_request'))
174 env:
175 # Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
176 # regression there silently tanks the cache hit rate the project is
177 # built around.
178 REASONIX_RELEASE_CACHE_GUARD: "1"
179 run: go test ./...
180
181 # Public repositories get five concurrent macOS runners and one pull
182 # request already claims three. Linux proves the portable packages on
183 # every pull request, so this leg sweeps only the darwin-specific ones
184 # and pushes keep the full ./... run above — the same tiering the Windows
185 # legs and the race job already use.
186 - name: test (macOS platform packages)
187 if: env.RUN_STEPS == 'true' && runner.os == 'macOS' && github.event_name == 'pull_request'
188 timeout-minutes: 15
189 env:
190 REASONIX_RELEASE_CACHE_GUARD: "1"
191 run: node scripts/macos-go-tests.mjs darwin
192
193 # ACP, Agent, Bot, boot and control own isolated Windows runners on PRs
194 # and pushes. ACP and Bot previously competed in the residual -p=4 lane:
195 # that hid ACP event ordering behind scheduler delay and made native Bot
196 # crash evidence inseparable from other package processes.
197 # The shared selector excludes them here, preserving platform smoke
198 # coverage without competing durable-session I/O or duplicate execution.
199 - name: test (Windows smoke)
200 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
201 timeout-minutes: 15
202 env:
203 REASONIX_RELEASE_CACHE_GUARD: "1"
204 run: node scripts/windows-go-tests.mjs smoke
205
206 - name: test (Windows credential ACL identity)
207 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
208 timeout-minutes: 3
209 run: go test -timeout=2m -run '^TestCredentialAccessRepairsLegacyCredentialDeny|^TestRepairLegacyCredentialDenyMatchesFileAcrossPathAliases$' ./internal/config ./internal/winaclresidue
210
211 # The PR smoke selector omits session; exercise native rename, writer-lock
212 # release, and interrupted purge recovery before accepting lifecycle fixes.
213 - name: test (Windows purge recovery)
214 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
215 timeout-minutes: 5
216 run: go test -timeout=3m -run '^TestPurge' ./internal/session
217
218 # Serve and taskmonitor are outside the general Windows PR smoke list.
219 # Cover runtime identities and snapshot publication before mainline push.
220 - name: test (Windows runtime status and task snapshots)
221 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
222 timeout-minutes: 5
223 run: go test -timeout=2m -run '^TestRuntimeStateHTTP|^TestFileStoreSaveTaskWaitsForTransientSnapshotReader$' ./internal/serve ./internal/taskmonitor
224
225 # Keep the platform contract in one selector with coverage assertions:
226 # primary shell registration, timeout/schema/session temp, unsupported OS
227 # sandbox behavior, ACP ordering, and Bot stop publication all gate PRs.
228 - name: test (Windows shell and lifecycle contract)
229 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
230 timeout-minutes: 10
231 env:
232 WINDOWS_BUILTIN_FULL: ${{ needs.changes.outputs.windows_builtin }}
233 run: node scripts/windows-pr-contract-tests.mjs
234
235 - name: test (Windows persistent PowerShell 7 and 5.1)
236 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
237 timeout-minutes: 5
238 shell: pwsh
239 run: |
240 $env:REASONIX_TEST_PWSH = (Get-Command pwsh).Source
241 go test -timeout=2m -run '^TestPowerShell' ./internal/persistentshell
242 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
243 $env:REASONIX_TEST_PWSH = (Get-Command powershell.exe).Source
244 go test -timeout=2m -run '^TestPowerShell' ./internal/persistentshell
245 if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
246
247 # Enumerate the entire module and run every remaining package. The union
248 # with the isolated jobs is exhaustive and disjoint, including new packages.
249 - name: test (full)
250 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name != 'pull_request'
251 timeout-minutes: 20
252 env:
253 # Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
254 # regression there silently tanks the cache hit rate the project is
255 # built around.
256 REASONIX_RELEASE_CACHE_GUARD: "1"
257 run: node scripts/windows-go-tests.mjs full
258
259 - name: test (Scoop desktop launch)
260 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
261 timeout-minutes: 10
262 shell: powershell
263 run: .\scripts\test-scoop-desktop-launch.ps1
264
265 - name: test (Windows installer acceptance harness)
266 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
267 timeout-minutes: 2
268 shell: pwsh
269 run: |
270 ./scripts/test-windows-installer-startup.test.ps1
271 ./scripts/test-windows-upgrade-startup.test.ps1
272
273 # Only main-v2 pushes save, one job per OS and module, so the cache
274 # stays a few pushes deep instead of churning on every pull request.
275 - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
276 if: always() && env.RUN_STEPS == 'true' && runner.os != 'Windows' && github.event_name == 'push' && steps.gocache.outputs.key != ''
277 with:
278 path: ${{ steps.gocache.outputs.paths }}
279 key: ${{ steps.gocache.outputs.key }}
280
281 windows-control:
282 needs: changes
283 if: ${{ !cancelled() }}
284 runs-on: windows-latest
285 env:
286 RUN_STEPS: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false' }}
287 steps:
288 - if: env.RUN_STEPS == 'true'
289 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
290
291 - if: env.RUN_STEPS == 'true'
292 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
293 with:
294 go-version-file: go.mod
295 cache: true
296
297 - if: env.RUN_STEPS == 'true'
298 uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
299 with:
300 node-version: "22"
301
302 # Defender real-time scanning on GitHub Windows runners intermittently
303 # crashes Go test binaries mid-syscall (DEP fault at PC=0 in the Windows
304 # syscall trampoline, e.g. golang/go#67139) and briefly keeps freshly
305 # written test files open, causing flaky sharing violations. Exclude the
306 # ephemeral build/test locations; fail this lane when the prerequisite
307 # cannot be established so a DEP crash is never treated as product evidence.
308 - name: Exclude build dirs from Defender scanning
309 shell: pwsh
310 run: |
311 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
312 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
313 foreach ($p in $paths) {
314 try {
315 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
316 Write-Host "Defender exclusion added: $p"
317 } catch {
318 throw "Defender exclusion failed for ${p}: $($_.Exception.Message)"
319 }
320 }
321
322 - name: test
323 if: env.RUN_STEPS == 'true'
324 timeout-minutes: 10
325 env:
326 REASONIX_RELEASE_CACHE_GUARD: "1"
327 run: node scripts/windows-go-tests.mjs control
328
329 windows-isolated:
330 needs: changes
331 if: ${{ !cancelled() && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false') }}
332 runs-on: windows-latest
333 strategy:
334 fail-fast: false
335 matrix:
336 group: [acp, agent, boot, bot, serve, session, worktree]
337 steps:
338 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
339 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
340 with:
341 go-version-file: go.mod
342 cache: true
343 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
344 with:
345 node-version: "22"
346
347 # A hardware exception on a host whose CPU saves a large XSTATE (Intel
348 # AMX) can corrupt the Go heap (golang/go#81238). Record the processor on
349 # the same isolated runner so any remaining native crash is attributable.
350 - name: runner CPU
351 shell: pwsh
352 run: Get-CimInstance Win32_Processor | Select-Object -ExpandProperty Name
353
354 # Defender real-time scanning on GitHub Windows runners intermittently
355 # crashes Go test binaries mid-syscall (DEP fault at PC=0 in the Windows
356 # syscall trampoline, e.g. golang/go#67139) and briefly keeps freshly
357 # written test files open, causing flaky sharing violations. Exclude the
358 # ephemeral build/test locations; fail this lane when the prerequisite
359 # cannot be established so a DEP crash is never treated as product evidence.
360 - name: Exclude build dirs from Defender scanning
361 shell: pwsh
362 run: |
363 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
364 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
365 foreach ($p in $paths) {
366 try {
367 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
368 Write-Host "Defender exclusion added: $p"
369 } catch {
370 throw "Defender exclusion failed for ${p}: $($_.Exception.Message)"
371 }
372 }
373 - name: test
374 # The worktree group normally approaches ten minutes on hosted runners.
375 # Keep the script's 8m Go timeout authoritative and leave time to report it.
376 timeout-minutes: 15
377 env:
378 REASONIX_RELEASE_CACHE_GUARD: "1"
379 run: node scripts/windows-go-tests.mjs ${{ matrix.group }}
380
381 race:
382 needs: changes
383 if: ${{ !cancelled() && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false') }}
384 runs-on: ubuntu-latest
385 steps:
386 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
387
388 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
389 with:
390 go-version-file: go.mod
391 cache: false
392
393 - uses: ./.github/actions/go-build-cache
394 id: gocache
395 with:
396 module: root-race
397
398 - name: Install and verify Linux sandbox backend
399 run: |
400 sudo apt-get update
401 sudo apt-get install -y bubblewrap
402 if sysctl -n kernel.unprivileged_userns_clone >/dev/null 2>&1; then
403 sudo sysctl -w kernel.unprivileged_userns_clone=1
404 fi
405 if sysctl -n kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
406 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
407 fi
408 bwrap --ro-bind / / --dev /dev --proc /proc -- true || \
409 echo "::warning::bubblewrap is installed but this runner denies user namespaces; unavailable-backend tests will run fail-closed"
410
411 # The matrix never runs -race (it needs cgo); the project's concurrency
412 # (plugin fan-out, background phase B, jobs Kill/Wait) would otherwise
413 # ship without race coverage. Pull requests sweep only the
414 # concurrency-heavy packages so this required check stays fast; pushes
415 # to main-v2 keep the full ./... sweep as the safety net.
416 - name: test -race (concurrency packages)
417 if: github.event_name == 'pull_request'
418 env:
419 REASONIX_RELEASE_CACHE_GUARD: "1"
420 run: go test -race ./internal/agent/... ./internal/plugin/... ./internal/jobs/... ./internal/proc/... ./internal/sandbox/... ./internal/filelock/... ./internal/eventwire/... ./internal/remote/... ./internal/extension/... ./internal/boot/... ./internal/control/... ./internal/tool/... ./internal/bot/...
421
422 - name: test -race (full)
423 if: github.event_name != 'pull_request'
424 env:
425 REASONIX_RELEASE_CACHE_GUARD: "1"
426 # The session suite reached its final history tests after 580s; Go's
427 # default 10m package alarm killed a test that had run for only 20s.
428 run: go test -race -timeout=15m ./...
429
430 # Only main-v2 pushes save, one job per OS and module, so the cache
431 # stays a few pushes deep instead of churning on every pull request.
432 - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
433 if: always() && github.event_name == 'push' && steps.gocache.outputs.key != ''
434 with:
435 path: ${{ steps.gocache.outputs.paths }}
436 key: ${{ steps.gocache.outputs.key }}
437
438 # sdk/go is a nested stdlib-only module invisible to root `go test ./...`.
439 # Its DTOs are generated from internal/extension/protocol, and the
440 # host-side conformance tests spawn the SDK example, so the job runs the
441 # same three-OS matrix as the root tests.
442 sdk:
443 needs: changes
444 if: ${{ !cancelled() }}
445 strategy:
446 fail-fast: false
447 matrix:
448 os: [ubuntu-latest, macos-latest, windows-latest]
449 runs-on: ${{ matrix.os }}
450 env:
451 RUN_STEPS: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.sdk != 'false' }}
452 defaults:
453 run:
454 working-directory: sdk/go
455 steps:
456 - if: env.RUN_STEPS == 'true'
457 uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
458
459 - if: env.RUN_STEPS == 'true'
460 uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
461 with:
462 go-version-file: sdk/go/go.mod
463
464 # Defender real-time scanning on GitHub Windows runners intermittently
465 # crashes Go test binaries mid-syscall (DEP fault at PC=0 in the Windows
466 # syscall trampoline, e.g. golang/go#67139) and briefly keeps freshly
467 # written test files open, causing flaky sharing violations. Exclude the
468 # ephemeral build/test locations; failures stay non-fatal warnings.
469 - name: Exclude build dirs from Defender scanning
470 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
471 shell: pwsh
472 run: |
473 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
474 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
475 foreach ($p in $paths) {
476 try {
477 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
478 Write-Host "Defender exclusion added: $p"
479 } catch {
480 Write-Host "::warning::Defender exclusion failed for ${p}: $($_.Exception.Message)"
481 }
482 }
483
484 - name: gofmt
485 if: env.RUN_STEPS == 'true'
486 shell: bash
487 run: |
488 unformatted=$(gofmt -l .)
489 if [ -n "$unformatted" ]; then
490 echo "These files are not gofmt-clean:"
491 echo "$unformatted"
492 exit 1
493 fi
494
495 - name: vet
496 if: env.RUN_STEPS == 'true'
497 run: go vet ./...
498
499 - name: stdlib-only guard
500 if: env.RUN_STEPS == 'true'
501 shell: bash
502 run: |
503 # The SDK is a public module with a hard stdlib-only contract.
504 if go list -m all | grep -v '^github.com/esengine/DeepSeek-Reasonix/sdk/go$'; then
505 echo "sdk/go must not depend on anything outside the standard library"
506 exit 1
507 fi
508
509 - name: test
510 if: env.RUN_STEPS == 'true'
511 run: go test ./...
512
513 - name: test -race
514 if: github.event_name != 'pull_request' && env.RUN_STEPS == 'true'
515 run: go test -race ./...
516
517 # Required check. Every desktop job reaches the ruleset through this one name,
518 # so a job that is not listed here is not gated at all: that is how a failing
519 # desktop-windows-go merged. PACKAGE_REQUIRED mirrors desktop-windows-package's
520 # own `if:` verbatim rather than testing for `push`, so workflow_dispatch does
521 # not expect a skip from a job that runs.
522 # Status functions bypass implicit success(): still aggregate failed children,
523 # but release the concurrency slot when a newer head cancels this whole run.
524 desktop:
525 needs: [changes, desktop-prepare, desktop-go, desktop-go-race, desktop-frontend, desktop-browser,
526 desktop-macos, desktop-windows, desktop-windows-go, desktop-windows-package]
527 if: ${{ !cancelled() }}
528 runs-on: ubuntu-latest
529 steps:
530 - name: Verify desktop validation jobs
531 env:
532 CHANGES_RESULT: ${{ needs.changes.result }}
533 PREPARE_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.desktop != 'false' }}
534 NATIVE_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false' }}
535 FRONTEND_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.frontend != 'false' || needs.changes.outputs.electron != 'false' }}
536 PACKAGE_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.packaging != 'false' }}
537 RACE_REQUIRED: ${{ github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true' }}
538 PREPARE_RESULT: ${{ needs.desktop-prepare.result }}
539 GO_RESULT: ${{ needs.desktop-go.result }}
540 GO_RACE_RESULT: ${{ needs.desktop-go-race.result }}
541 FRONTEND_RESULT: ${{ needs.desktop-frontend.result }}
542 BROWSER_RESULT: ${{ needs.desktop-browser.result }}
543 MACOS_RESULT: ${{ needs.desktop-macos.result }}
544 WINDOWS_RESULT: ${{ needs.desktop-windows.result }}
545 WINDOWS_GO_RESULT: ${{ needs.desktop-windows-go.result }}
546 PACKAGE_RESULT: ${{ needs.desktop-windows-package.result }}
547 run: |
548 test "$CHANGES_RESULT" = success
549 expected() { if [ "$1" = true ]; then echo success; else echo skipped; fi; }
550 test "$PREPARE_RESULT" = "$(expected "$PREPARE_REQUIRED")"
551 test "$GO_RESULT" = "$(expected "$NATIVE_REQUIRED")"
552 test "$GO_RACE_RESULT" = "$(expected "$RACE_REQUIRED")"
553 test "$FRONTEND_RESULT" = "$(expected "$FRONTEND_REQUIRED")"
554 # The browser aggregate always runs and validates skipped groups itself.
555 test "$BROWSER_RESULT" = success
556 # native ⇒ desktop by construction, so these can never contradict
557 # PREPARE_REQUIRED above.
558 test "$MACOS_RESULT" = "$(expected "$NATIVE_REQUIRED")"
559 test "$WINDOWS_RESULT" = "$(expected "$NATIVE_REQUIRED")"
560 test "$WINDOWS_GO_RESULT" = success
561 test "$PACKAGE_RESULT" = "$(expected "$PACKAGE_REQUIRED")"
562
563 desktop-frontend:
564 needs: [changes, desktop-prepare]
565 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.frontend != 'false' || needs.changes.outputs.electron != 'false') }}
566 runs-on: ubuntu-22.04
567 defaults:
568 run:
569 working-directory: desktop
570 steps:
571 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
572 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
573 with:
574 version: 10
575 run_install: false
576 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
577 with:
578 node-version-file: .node-version
579 cache: pnpm
580 cache-dependency-path: desktop/pnpm-lock.yaml
581 - name: Install frontend dependencies
582 run: pnpm --dir frontend install --frozen-lockfile
583 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
584 with:
585 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
586 path: desktop/frontend
587 - name: Verify stable frontend artifact
588 run: |
589 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
590 node frontend/scripts/artifact-identity.mjs verify \
591 --shell electron --channel stable \
592 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
593 --pnpm-version "$(pnpm --version)"
594 - name: Verify CI test coverage and runner
595 run: node --test frontend/scripts/ci-test-plan.test.mjs
596 - name: Check the Electron shell
597 run: |
598 pnpm --dir electron typecheck
599 pnpm --dir electron test
600 node --test packaging/*.test.mjs
601 pnpm --dir electron build
602 - name: Test desktop frontend once per suite
603 env:
604 REASONIX_TEST_CONCURRENCY: "2"
605 run: node frontend/scripts/run-ci-tests.mjs
606
607 desktop-browser-group:
608 needs: [changes, desktop-prepare]
609 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.browser != 'false') }}
610 runs-on: ubuntu-22.04
611 strategy:
612 fail-fast: false
613 max-parallel: 2
614 matrix:
615 group: [app-settings-motion, transcript]
616 defaults:
617 run:
618 working-directory: desktop
619 steps:
620 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
621 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
622 with:
623 version: 10
624 run_install: false
625 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
626 with:
627 node-version-file: .node-version
628 cache: pnpm
629 cache-dependency-path: desktop/pnpm-lock.yaml
630 - name: Install frontend dependencies
631 run: pnpm --dir frontend install --frozen-lockfile
632 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
633 with:
634 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
635 path: desktop/frontend
636 - name: Verify stable frontend artifact
637 run: |
638 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
639 node frontend/scripts/artifact-identity.mjs verify \
640 --shell electron --channel stable \
641 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
642 --pnpm-version "$(pnpm --version)"
643 - name: Install browser runtimes
644 run: PLAYWRIGHT_BROWSERS_PATH=.pw-browsers pnpm --dir frontend exec playwright install --with-deps chromium
645 - name: Install isolated native input driver
646 if: matrix.group == 'transcript'
647 run: sudo apt-get update && sudo apt-get install -y xdotool
648 - name: Test native browser foreground runtime
649 if: matrix.group == 'transcript'
650 timeout-minutes: 3
651 env:
652 REASONIX_BROWSER_UNSETTLED_INPUT: "1"
653 run: xvfb-run -a node electron/scripts/browser-runtime-smoke.mjs
654 - name: Upload Linux browser runtime evidence
655 if: always() && matrix.group == 'transcript'
656 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
657 with:
658 name: browser-runtime-linux
659 path: desktop/electron/artifacts/browser-runtime/
660 if-no-files-found: warn
661 - name: Test submission handoff in Chromium
662 if: matrix.group == 'transcript'
663 timeout-minutes: 4
664 env:
665 REASONIX_HANDOFF_EVIDENCE: ${{ runner.temp }}/desktop-browser/transcript/submission-chromium
666 run: pnpm --dir frontend test:submission-browser
667 - name: Test submission handoff with native Electron input
668 if: matrix.group == 'transcript'
669 timeout-minutes: 4
670 env:
671 REASONIX_HANDOFF_EVIDENCE: ${{ runner.temp }}/desktop-browser/transcript/submission-electron
672 run: xvfb-run -a pnpm --dir frontend test:submission-electron --native-input
673 - name: Test desktop browser group
674 env:
675 BROWSER_GROUP: ${{ matrix.group }}
676 REASONIX_LAYOUT_ARTIFACTS: ${{ runner.temp }}/desktop-browser/${{ matrix.group }}/layout
677 run: |
678 set -o pipefail
679 evidence="$RUNNER_TEMP/desktop-browser/$BROWSER_GROUP"
680 mkdir -p "$evidence"
681 case "$BROWSER_GROUP" in
682 app-settings-motion)
683 PLAYWRIGHT_BROWSERS_PATH=.pw-browsers pnpm --dir frontend test:app-browser
684 PLAYWRIGHT_BROWSERS_PATH=.pw-browsers REASONIX_SETTINGS_BROWSERS=chromium pnpm --dir frontend test:settings-browser
685 pnpm --dir frontend test:motion-browser
686 ;;
687 transcript)
688 # The fixture resolves .pw-browsers from frontend, not this shell's desktop cwd.
689 REASONIX_MAINTENANCE_EVIDENCE="$evidence/maintenance" \
690 pnpm --dir frontend test:maintenance-browser
691 xvfb-run -a env PLAYWRIGHT_BROWSERS_PATH=.pw-browsers REASONIX_TRANSCRIPT_NATIVE_THUMB=1 \
692 REASONIX_TRANSCRIPT_MODE=native-scrollbar REASONIX_LAYOUT_ARTIFACTS="$evidence/native-scrollbar" \
693 pnpm --dir frontend test:transcript-browser
694 REASONIX_TRANSCRIPT_READER_BROWSERS=chromium PLAYWRIGHT_BROWSERS_PATH=.pw-browsers \
695 REASONIX_TRANSCRIPT_MODE=headless-reader REASONIX_LAYOUT_ARTIFACTS="$evidence/headless-reader" \
696 pnpm --dir frontend test:transcript-reader-browser
697 ;;
698 *) exit 2 ;;
699 esac 2>&1 | tee "$evidence/run.log"
700
701 - name: Upload browser group evidence
702 if: always()
703 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
704 with:
705 name: desktop-browser-${{ matrix.group }}-${{ github.run_id }}-${{ github.run_attempt }}
706 path: ${{ runner.temp }}/desktop-browser/${{ matrix.group }}
707 if-no-files-found: ignore
708 retention-days: 7
709
710 desktop-browser:
711 needs: [changes, desktop-prepare, desktop-browser-group]
712 if: ${{ !cancelled() }}
713 runs-on: ubuntu-latest
714 steps:
715 - name: Verify desktop browser groups
716 env:
717 CHANGES_RESULT: ${{ needs.changes.result }}
718 SHOULD_RUN: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.browser != 'false' }}
719 PREPARE_RESULT: ${{ needs.desktop-prepare.result }}
720 GROUP_RESULT: ${{ needs.desktop-browser-group.result }}
721 run: |
722 test "$CHANGES_RESULT" = success
723 if [ "$SHOULD_RUN" = true ]; then
724 test "$PREPARE_RESULT" = success
725 test "$GROUP_RESULT" = success
726 else
727 test "$GROUP_RESULT" = skipped
728 fi
729
730 desktop-prepare:
731 needs: changes
732 if: ${{ !cancelled() && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.desktop != 'false') }}
733 env:
734 REASONIX_COMMIT: ${{ github.sha }}
735 outputs:
736 producer_attempt: ${{ steps.artifact-identity.outputs.attempt }}
737 stable_artifact_name: desktop-frontend-stable-${{ github.run_id }}-${{ steps.artifact-identity.outputs.attempt }}
738 canary_artifact_name: desktop-frontend-canary-${{ github.run_id }}-${{ steps.artifact-identity.outputs.attempt }}
739 runs-on: ubuntu-22.04
740 defaults:
741 run:
742 working-directory: desktop
743 steps:
744 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
745 - name: Capture frontend artifact producer identity
746 id: artifact-identity
747 run: echo "attempt=$GITHUB_RUN_ATTEMPT" >> "$GITHUB_OUTPUT"
748
749 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
750 with:
751 go-version-file: desktop/go.mod
752 cache: false
753
754 - uses: ./.github/actions/go-build-cache
755 id: gocache
756 with:
757 module: desktop
758
759 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
760 with:
761 version: 10
762 run_install: false
763
764 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
765 with:
766 node-version-file: .node-version
767 cache: pnpm
768 cache-dependency-path: desktop/pnpm-lock.yaml
769
770 - name: gofmt
771 run: |
772 unformatted=$(gofmt -l .)
773 if [ -n "$unformatted" ]; then
774 echo "These files are not gofmt-clean:"
775 echo "$unformatted"
776 exit 1
777 fi
778
779 - name: go.mod tidy
780 run: |
781 go mod tidy
782 if ! git diff --quiet -- go.mod go.sum; then
783 echo "desktop/go.mod or go.sum is stale - run 'cd desktop && go mod tidy' and commit."
784 git diff -- go.mod go.sum
785 exit 1
786 fi
787
788 # The packaged Electron shell embeds desktopContract.json; a stale
789 # frontend/src/generated would ship a shell/service protocol mismatch.
790 - name: Check desktop host contract drift
791 run: |
792 go run . -emit-contract frontend/src/generated
793 if ! git diff --exit-code -- frontend/src/generated; then
794 echo "desktop contract is stale - run 'cd desktop && go run . -emit-contract frontend/src/generated' and commit."
795 exit 1
796 fi
797
798 - name: Install frontend dependencies
799 run: pnpm --dir frontend install --frozen-lockfile
800
801 - name: Build stable frontend
802 run: pnpm --dir frontend build:electron
803
804 - name: Record stable frontend artifact identity
805 run: |
806 node frontend/scripts/artifact-identity.mjs create \
807 --shell electron \
808 --channel stable \
809 --source-sha "$(git rev-parse HEAD)" \
810 --run-id "$GITHUB_RUN_ID" \
811 --attempt "$GITHUB_RUN_ATTEMPT" \
812 --pnpm-version "$(pnpm --version)"
813
814 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
815 with:
816 name: desktop-frontend-stable-${{ github.run_id }}-${{ github.run_attempt }}
817 path: |
818 desktop/frontend/dist
819 desktop/frontend/sourcemaps/${{ github.sha }}
820 desktop/frontend/.reasonix-frontend-artifact.json
821 include-hidden-files: true
822 if-no-files-found: error
823 retention-days: 3
824
825 # Static checks ran in the stable build. The canary variant still gets a
826 # fresh Vite build and bundle-budget validation because its embedded
827 # channel is part of the shipped bytes.
828 - name: Build canary frontend
829 env:
830 REASONIX_CHANNEL: canary
831 run: node frontend/scripts/build-for-shell.mjs electron --bundle-only
832
833 - name: Record canary frontend artifact identity
834 run: |
835 node frontend/scripts/artifact-identity.mjs create \
836 --shell electron \
837 --channel canary \
838 --source-sha "$(git rev-parse HEAD)" \
839 --run-id "$GITHUB_RUN_ID" \
840 --attempt "$GITHUB_RUN_ATTEMPT" \
841 --pnpm-version "$(pnpm --version)"
842
843 - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
844 with:
845 name: desktop-frontend-canary-${{ github.run_id }}-${{ github.run_attempt }}
846 path: |
847 desktop/frontend/dist
848 desktop/frontend/sourcemaps/${{ github.sha }}
849 desktop/frontend/.reasonix-frontend-artifact.json
850 include-hidden-files: true
851 if-no-files-found: error
852 retention-days: 3
853
854 desktop-go:
855 needs: [changes, desktop-prepare]
856 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false') }}
857 runs-on: ubuntu-22.04
858 defaults:
859 run:
860 working-directory: desktop
861 steps:
862 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
863
864 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
865 with:
866 go-version-file: desktop/go.mod
867 cache: false
868
869 - uses: ./.github/actions/go-build-cache
870 id: gocache
871 with:
872 module: desktop
873
874 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
875 with:
876 version: 10
877 run_install: false
878
879 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
880 with:
881 node-version-file: .node-version
882 cache: pnpm
883 cache-dependency-path: desktop/pnpm-lock.yaml
884
885 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
886 with:
887 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
888 path: desktop/frontend
889 - name: Verify stable frontend artifact
890 run: |
891 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
892 node frontend/scripts/artifact-identity.mjs verify \
893 --shell electron --channel stable \
894 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
895 --pnpm-version "$(pnpm --version)"
896 - name: Install frontend dependencies
897 run: pnpm --dir frontend install --frozen-lockfile
898
899 - name: vet
900 run: go vet ./...
901
902 - name: golangci-lint
903 uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
904 with:
905 version: v2.12.2
906 working-directory: desktop
907 args: --timeout=5m
908
909 - name: build
910 run: go build ./...
911
912 - name: test
913 # Keep every test, with independent package alarms for history versions.
914 run: node ../scripts/desktop-windows-go-tests.mjs --all
915
916 # Only main-v2 pushes save, one job per OS and module, so the cache
917 # stays a few pushes deep instead of churning on every pull request.
918 - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
919 if: always() && github.event_name == 'push' && steps.gocache.outputs.key != ''
920 with:
921 path: ${{ steps.gocache.outputs.paths }}
922 key: ${{ steps.gocache.outputs.key }}
923
924 # The full desktop race package exceeded Go's ten-minute package alarm.
925 # Reuse the verified test partition to keep complete coverage in parallel.
926 # Pull requests run these partitions without -race (desktop-go and the Windows
927 # groups); the race sweep runs once per main-v2 push instead of per PR head.
928 desktop-go-race:
929 needs: [changes, desktop-prepare]
930 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true' }}
931 runs-on: ubuntu-22.04
932 timeout-minutes: 30
933 strategy:
934 fail-fast: false
935 matrix:
936 group: [A-B, C, D, E-H, I-M, N-P, Q-S, T-Z, history-3-5, history-6-7, history-8-9, history-10-11]
937 defaults:
938 run:
939 working-directory: desktop
940 steps:
941 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
942
943 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
944 with:
945 go-version-file: desktop/go.mod
946 cache: false
947
948 - uses: ./.github/actions/go-build-cache
949 id: gocache
950 with:
951 module: desktop-race
952
953 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
954 with:
955 version: 10
956 run_install: false
957
958 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
959 with:
960 node-version-file: .node-version
961 cache: pnpm
962 cache-dependency-path: desktop/pnpm-lock.yaml
963
964 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
965 with:
966 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
967 path: desktop/frontend
968 - name: Verify stable frontend artifact
969 run: |
970 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
971 node frontend/scripts/artifact-identity.mjs verify \
972 --shell electron --channel stable \
973 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
974 --pnpm-version "$(pnpm --version)"
975 - name: Install frontend dependencies
976 run: pnpm --dir frontend install --frozen-lockfile
977
978 # The extension work added new shared-state paths to the desktop
979 # runtime (tab/rebuild fences, extension UI). Race-sweep the module so
980 # regressions are CI-blocked, not just author-verified locally.
981 - name: test -race
982 run: node ../scripts/desktop-windows-go-tests.mjs ${{ matrix.group }} --race
983
984 # One shard saves the shared compiler cache; all shards restore it.
985 - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
986 if: ${{ !cancelled() && github.event_name == 'push' && matrix.group == 'A-B' && steps.gocache.outputs.key != '' }}
987 with:
988 path: ${{ steps.gocache.outputs.paths }}
989 key: ${{ steps.gocache.outputs.key }}
990
991 # desktop/ is a separate module, so the root macOS matrix above does not
992 # compile or exercise the native PTY and FSEvents implementations.
993 desktop-macos:
994 needs: [changes, desktop-prepare]
995 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false') }}
996 runs-on: macos-latest
997 defaults:
998 run:
999 working-directory: desktop
1000 steps:
1001 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1002
1003 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1004 with:
1005 go-version-file: desktop/go.mod
1006 cache: false
1007
1008 - uses: ./.github/actions/go-build-cache
1009 id: gocache
1010 with:
1011 module: desktop
1012
1013 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
1014 with:
1015 version: 10
1016 run_install: false
1017
1018 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
1019 with:
1020 node-version-file: .node-version
1021 cache: pnpm
1022 cache-dependency-path: desktop/pnpm-lock.yaml
1023
1024 - name: Install frontend dependencies
1025 run: pnpm --dir frontend install --frozen-lockfile
1026 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
1027 with:
1028 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
1029 path: desktop/frontend
1030 - name: Verify stable frontend artifact
1031 run: |
1032 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
1033 node frontend/scripts/artifact-identity.mjs verify \
1034 --shell electron --channel stable \
1035 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
1036 --pnpm-version "$(pnpm --version)"
1037
1038 - name: Test integrated terminal, PTY, and FSEvents lifecycle
1039 run: go test -race -run 'Test(DarwinWorkspaceWatcher|WorkspaceChangeHub|ResolveTerminal|Terminal|EmptyTerminal|UnixTerminalProcess)' .
1040
1041 - name: Test native macOS signing coverage
1042 run: node --test packaging/sign-macos.test.mjs
1043
1044 - name: Test native graphics recovery
1045 timeout-minutes: 3
1046 run: node electron/scripts/graphics-recovery-smoke.mjs
1047
1048 - name: Test native browser runtime and 100 capture lifecycles
1049 timeout-minutes: 5
1050 env:
1051 REASONIX_BROWSER_CYCLES: "100"
1052 run: node electron/scripts/browser-runtime-smoke.mjs
1053
1054 - name: Upload macOS browser runtime evidence
1055 if: always()
1056 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
1057 with:
1058 name: browser-runtime-macos
1059 path: desktop/electron/artifacts/browser-runtime/
1060 if-no-files-found: warn
1061
1062 - name: Test bounded Electron diagnostics
1063 timeout-minutes: 3
1064 run: |
1065 # @electron/get downloads the platform binary lazily. GitHub's
1066 # release endpoint occasionally returns a transient 504; retry only
1067 # that dependency fetch and keep all product/test failures fatal.
1068 set +e
1069 for attempt in 1 2 3; do
1070 output="$(node electron/scripts/performance-smoke.mjs 2>&1)"
1071 status=$?
1072 printf '%s\n' "$output"
1073 if [ "$status" -eq 0 ]; then
1074 exit 0
1075 fi
1076 if ! printf '%s\n' "$output" | grep -Eq 'HTTPError: Response code (429|5[0-9][0-9])|Electron failed to install correctly'; then
1077 exit "$status"
1078 fi
1079 if [ "$attempt" -lt 3 ]; then
1080 echo "Electron binary fetch failed transiently; retrying ($attempt/3)"
1081 sleep 5
1082 fi
1083 done
1084 exit "$status"
1085
1086 # The production desktop build uses CGO. Keep the explicit unavailable
1087 # backend buildable as a fail-closed portability contract instead of
1088 # silently falling back to kqueue's per-file descriptor usage.
1089 - name: Test macOS build without CGO
1090 env:
1091 CGO_ENABLED: "0"
1092 run: go test -run '^TestDarwinWorkspaceWatcherWithoutCGOIsUnavailable$' .
1093
1094 # desktop/*_darwin.go is the one build-tag set the lint job cannot reach:
1095 # the main-thread watchdog is cgo, so it only type-checks with a real
1096 # macOS toolchain.
1097 - name: golangci-lint
1098 uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
1099 with:
1100 version: v2.12.2
1101 working-directory: desktop
1102 args: --timeout=5m
1103
1104 # Packaging validation, not a code gate: run it on pushes to main-v2 (the
1105 # release pipeline packages again anyway), and let pull requests stop
1106 # after the lint step. Ad-hoc signs (no Apple secrets in CI) and skips
1107 # the DMG.
1108 - name: Remove stable frontend before canary packaging
1109 if: github.event_name != 'pull_request'
1110 run: rm -rf frontend/dist frontend/.reasonix-frontend-artifact.json
1111
1112 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
1113 if: github.event_name != 'pull_request'
1114 with:
1115 name: ${{ needs.desktop-prepare.outputs.canary_artifact_name }}
1116 path: desktop/frontend
1117
1118 - name: Verify canary frontend artifact
1119 if: github.event_name != 'pull_request'
1120 run: |
1121 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
1122 node frontend/scripts/artifact-identity.mjs verify \
1123 --shell electron --channel canary \
1124 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
1125 --pnpm-version "$(pnpm --version)"
1126
1127 - name: Package Electron app and verify bundle members
1128 if: github.event_name != 'pull_request'
1129 timeout-minutes: 25
1130 env:
1131 DESKTOP_BUILD_SKIP_DMG: "1"
1132 REASONIX_PACKAGE_REUSE_FRONTEND: "1"
1133 REASONIX_FRONTEND_PRODUCER_ATTEMPT: ${{ needs.desktop-prepare.outputs.producer_attempt }}
1134 run: |
1135 REASONIX_FRONTEND_PNPM_VERSION="$(pnpm --version)"
1136 export REASONIX_FRONTEND_PNPM_VERSION
1137 ../scripts/desktop-build.sh darwin/arm64 v0.0.0-ci canary
1138 node packaging/verify.mjs ../dist/Reasonix-darwin-arm64.zip
1139
1140 # Signing and bundle members do not prove the app starts. Until now the
1141 # packaged macOS renderer path ran only in the release build, so a
1142 # regression there surfaced at publication time instead of on main-v2.
1143 - name: Smoke-test the packaged macOS startup
1144 if: github.event_name != 'pull_request'
1145 timeout-minutes: 5
1146 run: |
1147 ditto -xk ../dist/Reasonix-darwin-arm64.zip "$RUNNER_TEMP/desktop-startup"
1148 node packaging/smoke.mjs "$RUNNER_TEMP/desktop-startup/Reasonix.app"
1149
1150 # Only main-v2 pushes save, one job per OS and module, so the cache
1151 # stays a few pushes deep instead of churning on every pull request.
1152 - uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4
1153 if: always() && github.event_name == 'push' && steps.gocache.outputs.key != ''
1154 with:
1155 path: ${{ steps.gocache.outputs.paths }}
1156 key: ${{ steps.gocache.outputs.key }}
1157
1158 # Desktop project-root matching is case-insensitive only on Windows
1159 # (sameDesktopPath folds case when os.PathSeparator is '\'), so the
1160 # regression tests for that contract are named *OnWindows and can never
1161 # run on the ubuntu leg above.
1162 desktop-windows:
1163 needs: [changes, desktop-prepare]
1164 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false') }}
1165 runs-on: windows-latest
1166 # A hung native step must not hold the workflow's concurrency group for the
1167 # six-hour default. This bounds hangs, not duration: the leg's own spread on
1168 # 2026-09-11 was 34-47 minutes, so a cap near the mean cancels healthy runs.
1169 # Every step keeps its own tighter budget.
1170 timeout-minutes: 60
1171 defaults:
1172 run:
1173 shell: bash
1174 working-directory: desktop
1175 steps:
1176 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1177
1178 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1179 with:
1180 go-version-file: desktop/go.mod
1181 cache: true
1182 cache-dependency-path: desktop/go.sum
1183
1184 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
1185 with:
1186 version: 10
1187 run_install: false
1188
1189 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
1190 with:
1191 node-version-file: .node-version
1192 cache: pnpm
1193 cache-dependency-path: desktop/pnpm-lock.yaml
1194
1195 # Defender real-time scanning on GitHub Windows runners intermittently
1196 # crashes Go test binaries mid-syscall (DEP fault at PC=0 in the Windows
1197 # syscall trampoline, e.g. golang/go#67139) and briefly keeps freshly
1198 # written test files open, causing flaky sharing violations. Exclude the
1199 # ephemeral build/test locations; failures stay non-fatal warnings.
1200 - name: Exclude build dirs from Defender scanning
1201 shell: pwsh
1202 run: |
1203 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
1204 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
1205 foreach ($p in $paths) {
1206 try {
1207 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
1208 Write-Host "Defender exclusion added: $p"
1209 } catch {
1210 Write-Host "::warning::Defender exclusion failed for ${p}: $($_.Exception.Message)"
1211 }
1212 }
1213
1214 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
1215 with:
1216 name: ${{ needs.desktop-prepare.outputs.canary_artifact_name }}
1217 path: desktop/frontend
1218 - name: Verify canary frontend artifact
1219 run: |
1220 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
1221 node frontend/scripts/artifact-identity.mjs verify \
1222 --shell electron --channel canary \
1223 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
1224 --pnpm-version "$(pnpm --version)"
1225
1226 # test:motion, test:transcript-browser and test:settings-browser run on
1227 # ubuntu (desktop-frontend and desktop-browser). They were repeated here
1228 # when each OS shipped its own engine; Electron ships one Chromium, so the
1229 # Windows-specific renderer evidence is the native Electron step below.
1230
1231 # Package the real Electron shell on every desktop PR and exercise the
1232 # production startup path (shell -> Go service handshake) in the packaged
1233 # layout. package.mjs builds the frontend itself (build:electron flavor).
1234 - name: Package Electron shell for native startup smoke
1235 timeout-minutes: 15
1236 env:
1237 REASONIX_PACKAGE_REUSE_FRONTEND: "1"
1238 REASONIX_FRONTEND_PRODUCER_ATTEMPT: ${{ needs.desktop-prepare.outputs.producer_attempt }}
1239 run: |
1240 REASONIX_FRONTEND_PNPM_VERSION="$(pnpm --version)"
1241 export REASONIX_FRONTEND_PNPM_VERSION
1242 pnpm install --frozen-lockfile
1243 go build -trimpath -ldflags "-s -w -H windowsgui -X main.version=v0.0.0-ci -X main.channel=canary" -o build/bin/reasonix-desktop.exe .
1244 node ../scripts/verify-windows-gui-subsystem.mjs build/bin/reasonix-desktop.exe
1245 node packaging/package.mjs windows/amd64 v0.0.0-ci canary
1246
1247 - name: Smoke-test Electron native startup
1248 timeout-minutes: 3
1249 run: node packaging/smoke.mjs build/electron/windows-amd64/app --service build/bin/reasonix-desktop.exe
1250
1251 - name: Test native window geometry on Windows
1252 timeout-minutes: 2
1253 run: node electron/scripts/window-geometry-smoke.mjs
1254
1255 - name: Test native graphics recovery
1256 timeout-minutes: 3
1257 run: node electron/scripts/graphics-recovery-smoke.mjs
1258
1259 - name: Test native browser foreground runtime
1260 timeout-minutes: 3
1261 run: node electron/scripts/browser-runtime-smoke.mjs
1262
1263 - name: Upload Windows browser runtime evidence
1264 if: always()
1265 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
1266 with:
1267 name: browser-runtime-windows
1268 path: desktop/electron/artifacts/browser-runtime/
1269 if-no-files-found: warn
1270
1271 - name: Test transcript content bounds in native Electron
1272 timeout-minutes: 5
1273 env:
1274 REASONIX_LAYOUT_ARTIFACTS: ${{ runner.temp }}/transcript-layout-electron
1275 run: pnpm --dir frontend test:transcript-electron
1276
1277 - name: Upload native transcript layout evidence
1278 if: always()
1279 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
1280 with:
1281 name: transcript-layout-electron-windows
1282 if-no-files-found: ignore
1283 retention-days: 7
1284 path: ${{ runner.temp }}/transcript-layout-electron
1285
1286 - name: Test bounded Electron diagnostics
1287 timeout-minutes: 3
1288 run: node electron/scripts/performance-smoke.mjs
1289
1290 # The desktop Go suite shares nothing with the Electron and browser steps
1291 # beyond a built frontend for go:embed, and it is the leg's longest step by
1292 # far: the module that tests in 1.8 minutes on ubuntu takes ~17 on Windows,
1293 # ~11 of them compiling and linking the cgo-heavy root package before a
1294 # single test runs (GOCACHE restored, Defender excluded). Running it beside
1295 # the Electron steps makes the leg's wall time the longer half rather than
1296 # the sum, and a Go flake reruns only this job.
1297 desktop-windows-go-group:
1298 needs: [changes, desktop-prepare]
1299 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false') }}
1300 runs-on: windows-latest
1301 # Independent runners prevent valid groups from consuming each other's
1302 # step deadline. Each group runs with go -timeout=25m (driver testArgs).
1303 timeout-minutes: 45
1304 strategy:
1305 fail-fast: false
1306 matrix:
1307 group: [A-B, C, D, E-H, I-M, N-P, Q-S, T-Z, history-3-5, history-6-7, history-8-9, history-10-11]
1308 defaults:
1309 run:
1310 shell: bash
1311 working-directory: desktop
1312 steps:
1313 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1314
1315 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1316 with:
1317 go-version-file: desktop/go.mod
1318 cache: true
1319 cache-dependency-path: desktop/go.sum
1320
1321 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
1322 with:
1323 version: 10
1324 run_install: false
1325
1326 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
1327 with:
1328 node-version-file: .node-version
1329
1330 # Same Defender hazard as the Electron leg: exclusions keep Go test
1331 # binaries from crashing mid-syscall and freshly written files readable.
1332 - name: Exclude build dirs from Defender scanning
1333 shell: pwsh
1334 run: |
1335 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
1336 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
1337 foreach ($p in $paths) {
1338 try {
1339 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
1340 Write-Host "Defender exclusion added: $p"
1341 } catch {
1342 Write-Host "::warning::Defender exclusion failed for ${p}: $($_.Exception.Message)"
1343 }
1344 }
1345
1346 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
1347 with:
1348 name: ${{ needs.desktop-prepare.outputs.stable_artifact_name }}
1349 path: desktop/frontend
1350 - name: Verify stable frontend artifact
1351 run: |
1352 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
1353 node frontend/scripts/artifact-identity.mjs verify \
1354 --shell electron --channel stable \
1355 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
1356 --pnpm-version "$(pnpm --version)"
1357
1358 - name: Validate Windows test partition
1359 run: node --test ../scripts/desktop-windows-go-tests.test.mjs
1360
1361 - name: test (Windows desktop and update helper)
1362 # Keep native Go output here. JSON mode forces verbose output into the
1363 # test cache and spent 8-10 minutes finalizing it on Windows runners.
1364 timeout-minutes: 25
1365 # The driver checks the current native inventory for unique ownership.
1366 run: node ../scripts/desktop-windows-go-tests.mjs ${{ matrix.group }}
1367
1368 # ConPTY startup and teardown depend on the hosted runner's interactive
1369 # Windows environment. Keep the end-to-end probe visible without making
1370 # a single host scheduling stall equivalent to a Go correctness failure.
1371 - name: probe (Windows ConPTY host integration)
1372 if: matrix.group == 'T-Z'
1373 id: conpty-smoke
1374 continue-on-error: true
1375 timeout-minutes: 2
1376 run: go test -run '^TestWindowsTerminalProcessConPTYSmoke$' .
1377
1378 - name: report ConPTY probe failure
1379 if: always() && matrix.group == 'T-Z' && steps.conpty-smoke.outcome == 'failure'
1380 shell: bash
1381 run: echo "::warning::The hosted Windows ConPTY integration probe failed; deterministic Windows Go tests still passed."
1382
1383 - name: test (vendored systray identity)
1384 if: matrix.group == 'T-Z'
1385 timeout-minutes: 2
1386 run: go test -timeout=60s fyne.io/systray
1387
1388 desktop-windows-go:
1389 needs: [changes, desktop-prepare, desktop-windows-go-group]
1390 if: ${{ !cancelled() }}
1391 runs-on: ubuntu-latest
1392 steps:
1393 - name: Verify Windows desktop Go groups
1394 env:
1395 CHANGES_RESULT: ${{ needs.changes.result }}
1396 SHOULD_RUN: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.native != 'false' }}
1397 PREPARE_RESULT: ${{ needs.desktop-prepare.result }}
1398 GROUP_RESULT: ${{ needs.desktop-windows-go-group.result }}
1399 run: |
1400 test "$CHANGES_RESULT" = success
1401 if [ "$SHOULD_RUN" = true ]; then
1402 test "$PREPARE_RESULT" = success
1403 test "$GROUP_RESULT" = success
1404 else
1405 test "$GROUP_RESULT" = skipped
1406 fi
1407
1408 # Installer packaging is packaging validation, not a code gate, and it shares
1409 # no state with the test steps above. It runs beside them instead of after
1410 # them so the Windows leg's wall time is the longer half rather than the sum,
1411 # and so a packaging failure costs a short rerun instead of the whole leg.
1412 # Packaging changes also qualify before merge, so installer/upgrade failures
1413 # cannot first appear on the release candidate's push.
1414 desktop-windows-package:
1415 needs: [changes, desktop-prepare]
1416 if: ${{ !cancelled() && needs.desktop-prepare.result == 'success' && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.packaging != 'false') }}
1417 runs-on: windows-latest
1418 # Bound packaging hangs; diagnostic measurement has its own workflow.
1419 timeout-minutes: 45
1420 defaults:
1421 run:
1422 shell: bash
1423 working-directory: desktop
1424 steps:
1425 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1426
1427 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1428 with:
1429 go-version-file: desktop/go.mod
1430 cache: true
1431 cache-dependency-path: desktop/go.sum
1432
1433 - uses: pnpm/action-setup@ea17c68df8912ef543352723c149a84f56e3d413 # v6.1.0
1434 with:
1435 version: 10
1436 run_install: false
1437
1438 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
1439 with:
1440 node-version-file: .node-version
1441 cache: pnpm
1442 cache-dependency-path: desktop/pnpm-lock.yaml
1443
1444 # Same Defender hazard as the test leg: exclusions keep packaging from
1445 # tripping over freshly written files that are still held open.
1446 - name: Exclude build dirs from Defender scanning
1447 shell: pwsh
1448 run: |
1449 $paths = @($env:GITHUB_WORKSPACE, $env:RUNNER_TEMP, $env:TEMP, (go env GOCACHE)) |
1450 Where-Object { $_ -and (Test-Path $_) } | Select-Object -Unique
1451 foreach ($p in $paths) {
1452 try {
1453 Add-MpPreference -ExclusionPath $p -ErrorAction Stop
1454 Write-Host "Defender exclusion added: $p"
1455 } catch {
1456 Write-Host "::warning::Defender exclusion failed for ${p}: $($_.Exception.Message)"
1457 }
1458 }
1459
1460 - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
1461 with:
1462 name: ${{ needs.desktop-prepare.outputs.canary_artifact_name }}
1463 path: desktop/frontend
1464 - name: Verify canary frontend artifact
1465 run: |
1466 test -n "${{ needs.desktop-prepare.outputs.producer_attempt }}"
1467 node frontend/scripts/artifact-identity.mjs verify \
1468 --shell electron --channel canary \
1469 --source-sha "$(git rev-parse HEAD)" --run-id "$GITHUB_RUN_ID" --attempt "${{ needs.desktop-prepare.outputs.producer_attempt }}" \
1470 --pnpm-version "$(pnpm --version)"
1471
1472 - name: Install NSIS
1473 run: pwsh -NoProfile -File ../scripts/install-nsis.ps1
1474
1475 - name: Verify uninstaller-only compilation preserves the shared uninstaller
1476 run: node ../scripts/check-windows-uninstaller.mjs
1477
1478 - name: Build Windows installer and portable archive
1479 timeout-minutes: 20
1480 env:
1481 REASONIX_PACKAGE_REUSE_FRONTEND: "1"
1482 REASONIX_FRONTEND_PRODUCER_ATTEMPT: ${{ needs.desktop-prepare.outputs.producer_attempt }}
1483 run: |
1484 REASONIX_FRONTEND_PNPM_VERSION="$(pnpm --version)"
1485 export REASONIX_FRONTEND_PNPM_VERSION
1486 ../scripts/desktop-build.sh windows/amd64 v0.0.0-ci canary
1487
1488 - name: Verify packaged Windows artifacts
1489 run: |
1490 node packaging/verify.mjs ../dist/Reasonix-windows-amd64.zip
1491 node packaging/signing-files.mjs build/windows/signing-payload --check
1492
1493 - name: Install and smoke-test Windows installer identity
1494 shell: pwsh
1495 run: |
1496 go build -o "$env:RUNNER_TEMP/windows-upgrade-fixture.exe" ./cmd/windows-upgrade-fixture
1497 ../scripts/test-windows-installer-startup.ps1 `
1498 -InstallerPath ../dist/Reasonix-windows-amd64-installer.exe `
1499 -ExpectedVersion v0.0.0-ci `
1500 -FixtureBuilderPath "$env:RUNNER_TEMP/windows-upgrade-fixture.exe" `
1501 -EvidenceDirectory "$env:RUNNER_TEMP/reasonix-installer-acceptance"
1502
1503 - name: Upload Windows installer acceptance evidence
1504 if: always()
1505 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
1506 with:
1507 name: windows-installer-acceptance-${{ github.run_id }}-${{ github.run_attempt }}
1508 path: |
1509 ${{ runner.temp }}/reasonix-installer-acceptance/**/*.json
1510 ${{ runner.temp }}/reasonix-installer-acceptance/**/*.png
1511 ${{ runner.temp }}/reasonix-installer-acceptance/**/*.log
1512 !${{ runner.temp }}/reasonix-installer-acceptance/installed/**
1513 !${{ runner.temp }}/reasonix-installer-acceptance/**/cache/**
1514 if-no-files-found: ignore
1515 retention-days: 7
1516
1517 ci-metrics:
1518 needs: [desktop, desktop-macos, desktop-windows, desktop-windows-go, desktop-windows-package]
1519 if: ${{ !cancelled() }}
1520 runs-on: ubuntu-latest
1521 permissions:
1522 actions: read
1523 contents: read
1524 steps:
1525 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1526 - name: Summarize queue, execution and stage timing
1527 env:
1528 GH_TOKEN: ${{ github.token }}
1529 run: |
1530 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" > "$RUNNER_TEMP/ci-run.json"
1531 gh api "repos/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/ci-jobs.json"
1532 node scripts/ci-timings.mjs \
1533 --run "$RUNNER_TEMP/ci-run.json" \
1534 --jobs "$RUNNER_TEMP/ci-jobs.json" \
1535 --summary "$GITHUB_STEP_SUMMARY" \
1536 --title "Frontend CI timing"
1537
1538 # Every main-v2 push saves one Go cache per Unix OS and module (~2.6 GiB
1539 # measured) against a 10 GiB repository limit shared with pnpm, CodeQL and
1540 # setup-go caches; two pushes filled it. Keep only the newest cache per
1541 # prefix once the savers are done; restore-keys pick the newest anyway.
1542 prune-go-cache:
1543 needs: [test, race, desktop-go, desktop-go-race, desktop-macos]
1544 if: ${{ !cancelled() && github.event_name == 'push' }}
1545 runs-on: ubuntu-latest
1546 timeout-minutes: 5
1547 permissions:
1548 actions: write
1549 contents: read
1550 steps:
1551 - name: Keep the newest Go cache per OS and module
1552 env:
1553 GH_TOKEN: ${{ github.token }}
1554 run: |
1555 set -euo pipefail
1556 declare -A seen
1557 while read -r id key; do
1558 [ -n "$id" ] || continue
1559 prefix="${key%-*-*-*}"
1560 if [ -n "${seen[$prefix]:-}" ]; then
1561 echo "deleting older cache $key"
1562 gh api -X DELETE "repos/$GITHUB_REPOSITORY/actions/caches/$id" >/dev/null
1563 else
1564 seen[$prefix]=1
1565 echo "keeping newest cache $key"
1566 fi
1567 done < <(gh api "repos/$GITHUB_REPOSITORY/actions/caches?per_page=100&sort=created_at&direction=desc" --paginate \
1568 --jq '.actions_caches[] | select(.key | startswith("go-")) | "\(.id) \(.key)"')
1569
1570 # repolint scans desktop/ and sdk/ too, so this job also runs for diffs the
1571 # `code` filter would otherwise skip.
1572 lint-code:
1573 needs: changes
1574 if: ${{ !cancelled() && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false' || needs.changes.outputs.desktop != 'false' || needs.changes.outputs.sdk != 'false') }}
1575 runs-on: ubuntu-latest
1576 steps:
1577 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1578
1579 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1580 with:
1581 go-version-file: go.mod
1582 cache: false
1583
1584 - uses: ./.github/actions/go-build-cache
1585 id: gocache
1586 with:
1587 module: root
1588
1589 - name: repo standards
1590 run: go run ./tools/repolint
1591
1592 # `make lint-install` reads the same file, so a local run and this job
1593 # cannot drift onto different linter versions.
1594 - id: golangci
1595 run: echo "version=$(cat .golangci-version)" >> "$GITHUB_OUTPUT"
1596
1597 - name: golangci-lint
1598 uses: golangci/golangci-lint-action@ba0d7d2ec06a0ea1cb5fa41b2e4a3ab91d21278a # v9
1599 with:
1600 version: ${{ steps.golangci.outputs.version }}
1601 args: --timeout=5m
1602
1603 # The step above only type-checks the linux/amd64 build, so every
1604 # //go:build windows and //go:build darwin file in the tree went unlinted.
1605 # Both modules cross-check without a toolchain; desktop under darwin does
1606 # not, because its bundle icon repair is cgo, so that leg lives in
1607 # desktop-macos.
1608 - name: golangci-lint (cross-platform build tags)
1609 run: |
1610 set -euo pipefail
1611 command -v golangci-lint
1612 for target in "darwin ." "windows ." "windows desktop"; do
1613 read -r target_os target_dir <<< "$target"
1614 echo "::group::golangci-lint GOOS=$target_os ($target_dir)"
1615 (cd "$target_dir" && GOOS="$target_os" golangci-lint run --timeout=5m ./...)
1616 echo "::endgroup::"
1617 done
1618
1619 release-control:
1620 needs: changes
1621 if: ${{ !cancelled() && ((github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.release_control != 'false') }}
1622 runs-on: ubuntu-latest
1623 timeout-minutes: 10
1624 steps:
1625 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1626 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1627 with:
1628 go-version-file: go.mod
1629 cache: false
1630 - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7
1631 with:
1632 node-version-file: .node-version
1633 - name: Validate release workflows and executable contracts
1634 run: |
1635 go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 \
1636 -ignore 'label "windows-11-arm" is unknown' \
1637 -ignore 'label "macos-15-intel" is unknown' \
1638 .github/workflows/release-stable.yml \
1639 .github/workflows/release-candidate.yml \
1640 .github/workflows/release-candidate-verify.yml \
1641 .github/workflows/release-promote.yml \
1642 .github/workflows/release-verify.yml \
1643 .github/workflows/release.yml \
1644 .github/workflows/release-npm.yml \
1645 .github/workflows/release-desktop.yml \
1646 .github/workflows/apple-notary-log.yml \
1647 .github/workflows/macos-signing-check.yml \
1648 .github/workflows/release-verify-issues.yml \
1649 .github/workflows/docs-impact.yml \
1650 .github/workflows/ci.yml
1651 node --test scripts/release-candidate.test.mjs scripts/resolve-release-candidate.test.mjs \
1652 scripts/verify-release-artifact-archive.test.mjs \
1653 scripts/build-release-cli-candidate.test.mjs scripts/publish-homebrew-cask.test.mjs \
1654 scripts/desktop-release-artifacts.test.mjs scripts/release-publication-ledger.test.mjs \
1655 scripts/release-cli-freeze.test.mjs \
1656 scripts/ci-paths.test.mjs scripts/ci-workflow.test.mjs scripts/macos-go-tests.test.mjs \
1657 scripts/desktop-windows-go-tests.test.mjs \
1658 npm/publish.test.mjs
1659 bash scripts/validate-release-candidate-source.test.sh
1660 bash scripts/validate-release-control-plane.test.sh
1661 bash scripts/verify-release-push-ci.test.sh
1662 bash scripts/release-stable.test.sh
1663 bash scripts/release-candidate-tags.test.sh
1664 node --test scripts/verify-release-tag-identity.test.mjs
1665 bash scripts/verify-release-authorization.test.sh
1666 bash scripts/release-workflows.test.sh
1667 node scripts/check-single-release-public-contract.mjs
1668
1669 # `lint` is a protected check name. Keep it as the fail-closed aggregate so
1670 # required checks for root-module jobs the per-OS `test` legs do
1671 # not cover. windows-control and sdk gate internally through RUN_STEPS and so
1672 # always report a result: `success` here means "did not fail", not "validated
1673 # this diff" — the same trade-off the required `test` legs already make. Do
1674 # not add govulncheck: it sets continue-on-error, so needs.*.result is
1675 # `success` even when its steps fail and the assertion would be a tautology.
1676 root:
1677 needs: [changes, windows-control, windows-isolated, sdk, coverage]
1678 if: ${{ !cancelled() }}
1679 runs-on: ubuntu-latest
1680 steps:
1681 - name: Verify root validation jobs
1682 env:
1683 CHANGES_RESULT: ${{ needs.changes.result }}
1684 CODE_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false' }}
1685 COVERAGE_REQUIRED: ${{ github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true' }}
1686 CONTROL_RESULT: ${{ needs.windows-control.result }}
1687 ISOLATED_RESULT: ${{ needs.windows-isolated.result }}
1688 SDK_RESULT: ${{ needs.sdk.result }}
1689 COVERAGE_RESULT: ${{ needs.coverage.result }}
1690 run: |
1691 test "$CHANGES_RESULT" = success
1692 expected() { if [ "$1" = true ]; then echo success; else echo skipped; fi; }
1693 test "$CONTROL_RESULT" = success
1694 test "$SDK_RESULT" = success
1695 test "$ISOLATED_RESULT" = "$(expected "$CODE_REQUIRED")"
1696 test "$COVERAGE_RESULT" = "$(expected "$COVERAGE_REQUIRED")"
1697
1698 # frontend unit coverage can run once in desktop-frontend without weakening
1699 # the branch gate.
1700 lint:
1701 needs: [changes, lint-code, release-control, desktop-prepare, desktop-frontend]
1702 if: ${{ !cancelled() }}
1703 runs-on: ubuntu-latest
1704 steps:
1705 - name: Verify lint and frontend validation jobs
1706 env:
1707 CHANGES_RESULT: ${{ needs.changes.result }}
1708 LINT_CODE_RESULT: ${{ needs.lint-code.result }}
1709 LINT_CODE_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.code != 'false' || needs.changes.outputs.desktop != 'false' || needs.changes.outputs.sdk != 'false' }}
1710 RELEASE_CONTROL_RESULT: ${{ needs.release-control.result }}
1711 RELEASE_CONTROL_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.release_control != 'false' }}
1712 FRONTEND_RESULT: ${{ needs.desktop-frontend.result }}
1713 PREPARE_RESULT: ${{ needs.desktop-prepare.result }}
1714 FRONTEND_REQUIRED: ${{ (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') || needs.changes.outputs.frontend != 'false' || needs.changes.outputs.electron != 'false' }}
1715 run: |
1716 test "$CHANGES_RESULT" = success
1717 expected() { if [ "$1" = true ]; then echo success; else echo skipped; fi; }
1718 lint_expected=skipped
1719 if [ "$LINT_CODE_REQUIRED" = true ]; then lint_expected=success; fi
1720 test "$LINT_CODE_RESULT" = "$lint_expected"
1721 test "$RELEASE_CONTROL_RESULT" = "$(expected "$RELEASE_CONTROL_REQUIRED")"
1722 if [ "$FRONTEND_REQUIRED" = true ]; then
1723 test "$PREPARE_RESULT" = success
1724 test "$FRONTEND_RESULT" = success
1725 else
1726 test "$FRONTEND_RESULT" = skipped
1727 fi
1728
1729 govulncheck:
1730 needs: changes
1731 if: (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true')
1732 runs-on: ubuntu-latest
1733 continue-on-error: true # informational — stdlib vulns need a Go patch release
1734 steps:
1735 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1736
1737 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1738 with:
1739 go-version-file: go.mod
1740 cache: false
1741
1742 - uses: ./.github/actions/go-build-cache
1743 id: gocache
1744 with:
1745 module: root
1746
1747 - name: install govulncheck
1748 run: go install golang.org/x/vuln/cmd/govulncheck@latest
1749
1750 - name: govulncheck
1751 run: govulncheck ./...
1752
1753 # `always()` matches every other gated job: a failed changes job makes this
1754 # run the full sweep instead of silently reporting skipped to the aggregate.
1755 coverage:
1756 needs: changes
1757 if: ${{ !cancelled() && (github.event_name != 'pull_request' && needs.changes.outputs.notes_only != 'true') }}
1758 runs-on: ubuntu-latest
1759 steps:
1760 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
1761
1762 - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7
1763 with:
1764 go-version-file: go.mod
1765 cache: false
1766
1767 - uses: ./.github/actions/go-build-cache
1768 id: gocache
1769 with:
1770 module: root
1771
1772 - name: Install and verify Linux sandbox backend
1773 run: |
1774 sudo apt-get update
1775 sudo apt-get install -y bubblewrap
1776 if sysctl -n kernel.unprivileged_userns_clone >/dev/null 2>&1; then
1777 sudo sysctl -w kernel.unprivileged_userns_clone=1
1778 fi
1779 if sysctl -n kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
1780 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
1781 fi
1782 bwrap --ro-bind / / --dev /dev --proc /proc -- true
1783
1784 - name: test with coverage
1785 run: go test -coverprofile=coverage.out -covermode=atomic ./...
1786
1787 - name: upload coverage
1788 uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
1789 with:
1790 name: coverage-report
1791 path: coverage.out
1792 retention-days: 7
1793
1793 lines YAML