返回 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 group: ci-${{ github.ref }}
14 cancel-in-progress: true
15
16 jobs:
17 # Cheap path gate for pull requests. PRs confined to docs/site/release-notes
18 # (or top-level Markdown) skip the heavy Go jobs, and PRs that cannot affect
19 # the desktop module skip the desktop jobs. Job-level `if` reports skipped,
20 # which satisfies the required status checks (lint, race, test) — a
21 # workflow-level paths-ignore would leave required checks pending and block
22 # merges. Gated jobs skip only on an explicit `false` output: wrapped in
23 # `always()`, a failed `changes` job (or a missing output) makes them run
24 # the full matrix instead of silently passing required checks as skipped.
25 # Pushes to main-v2 always run everything.
26 changes:
27 runs-on: ubuntu-latest
28 outputs:
29 code: ${{ steps.filter.outputs.code }}
30 desktop: ${{ steps.filter.outputs.desktop }}
31 site: ${{ steps.filter.outputs.site }}
32 sdk: ${{ steps.filter.outputs.sdk }}
33 steps:
34 - uses: actions/checkout@v7
35 with:
36 fetch-depth: 0
37 - id: filter
38 run: |
39 code=true; desktop=true; site=true; sdk=true
40 base=""
41 if [ "${{ github.event_name }}" = "pull_request" ]; then
42 base="${{ github.event.pull_request.base.sha }}"
43 elif [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then
44 base="${{ github.event.before }}"
45 fi
46 if [ -n "$base" ] && git cat-file -e "$base^{commit}" 2>/dev/null; then
47 files=$(git diff --name-only "$base" HEAD)
48 if [ -n "$files" ]; then
49 code=false; desktop=false; site=false; sdk=false
50 # Root-module CI: desktop/ is a separate module, so only the
51 # clearly unrelated paths below can skip it.
52 if echo "$files" | grep -qvE '^(docs/|site/|release-notes/|desktop/|workers/|[^/]+\.md$)'; then code=true; fi
53 # desktop/ imports the root kernel via `replace reasonix => ../`,
54 # so only this clearly-unrelated set is safe to skip.
55 if echo "$files" | grep -qvE '^(docs/|site/|release-notes/|workers/|benchmarks/|npm/|[^/]+\.md$)'; then desktop=true; fi
56 if echo "$files" | grep -q '^site/'; then site=true; fi
57 # sdk/go is a nested module invisible to root `go test ./...`;
58 # its DTOs are generated from internal/extension/protocol, so
59 # both paths must trigger the sdk job.
60 if echo "$files" | grep -qE '^(sdk/|internal/extension/)'; then sdk=true; fi
61 fi
62 fi
63 { echo "code=$code"; echo "desktop=$desktop"; echo "site=$site"; echo "sdk=$sdk"; } >> "$GITHUB_OUTPUT"
64
65 # The ruleset requires the per-OS check names (test (ubuntu-latest) etc.).
66 # A matrix job skipped at job level reports no per-leg checks at all, so
67 # those required checks would stay "Expected" and block merging. The job
68 # therefore always runs and the steps do the gating: when the changes
69 # detector reports the diff is unrelated, every step skips and each leg
70 # reports success in seconds. `always()` also keeps the legs alive when
71 # the changes job itself fails (fail-open: an empty output != 'false').
72 test:
73 needs: changes
74 if: always()
75 strategy:
76 fail-fast: false
77 matrix:
78 os: [ubuntu-latest, macos-latest, windows-latest]
79 runs-on: ${{ matrix.os }}
80 env:
81 RUN_STEPS: ${{ github.event_name != 'pull_request' || needs.changes.outputs.code != 'false' }}
82 steps:
83 - if: env.RUN_STEPS == 'true'
84 uses: actions/checkout@v7
85
86 - if: env.RUN_STEPS == 'true'
87 uses: actions/setup-go@v7
88 with:
89 go-version-file: go.mod
90 cache: true
91
92 - name: Install and verify Linux sandbox backend
93 if: env.RUN_STEPS == 'true' && runner.os == 'Linux'
94 run: |
95 sudo apt-get update
96 sudo apt-get install -y bubblewrap
97 if sysctl -n kernel.unprivileged_userns_clone >/dev/null 2>&1; then
98 sudo sysctl -w kernel.unprivileged_userns_clone=1
99 fi
100 if sysctl -n kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
101 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
102 fi
103 bwrap --ro-bind / / --dev /dev --proc /proc -- true || \
104 echo "::warning::bubblewrap is installed but this runner denies user namespaces; unavailable-backend tests will run fail-closed"
105
106 # Skipped on Windows: the runner checks out CRLF, so gofmt -l flags every
107 # file. gofmt output is OS-independent, so the Unix legs already cover it.
108 - name: gofmt
109 if: env.RUN_STEPS == 'true' && runner.os != 'Windows'
110 run: |
111 # Root module only — desktop/ is a separate module with its own tooling.
112 unformatted=$(gofmt -l . | grep -v '^desktop/' || true)
113 if [ -n "$unformatted" ]; then
114 echo "These files are not gofmt-clean:"
115 echo "$unformatted"
116 exit 1
117 fi
118
119 - name: vet
120 if: env.RUN_STEPS == 'true'
121 run: go vet ./...
122
123 - name: build
124 if: env.RUN_STEPS == 'true'
125 run: go build ./...
126
127 - name: test
128 if: env.RUN_STEPS == 'true' && runner.os != 'Windows'
129 env:
130 # Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
131 # regression there silently tanks the cache hit rate the project is
132 # built around.
133 REASONIX_RELEASE_CACHE_GUARD: "1"
134 run: go test ./...
135
136 # Pull requests run a Windows smoke suite: the packages that actually
137 # carry *_windows.go code, the startup/checkpoint packages with portable
138 # filesystem contracts, plus cmd/. The full ./... sweep stays on pushes
139 # to main-v2; the Unix legs always run the full suite. Keep the job-level
140 # budget above normal 7-9 minute runner variance; each package test binary
141 # remains independently bounded by Go's 3 minute timeout below.
142 - name: test (Windows smoke)
143 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
144 timeout-minutes: 15
145 env:
146 REASONIX_RELEASE_CACHE_GUARD: "1"
147 WINDOWS_SANDBOX_WAIT_MS: "20000"
148 run: go test -p 4 -timeout=3m ./internal/agent/... ./internal/boot/... ./internal/checkpoint/... ./internal/cli/... ./internal/control/... ./internal/desktoplauncher/... ./internal/filelock/... ./internal/fileutil/... ./internal/hook/... ./internal/instruction/... ./internal/mcplaunch/... ./internal/notify/... ./internal/proc/... ./internal/remote/... ./internal/repair/... ./internal/sandbox/... ./internal/sysproxy/... ./internal/workspacelease/... ./cmd/...
149
150 # The general Windows PR smoke list intentionally omits internal/tool.
151 # Keep the session-temp portability contract covered without widening the
152 # leg to every tool test: two real PowerShell launches must share the
153 # injected TMPDIR/TMP/TEMP directory.
154 - name: test (Windows session temp)
155 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
156 timeout-minutes: 3
157 run: go test -timeout=2m -run '^TestBashSharesSessionTempAcrossCalls$' ./internal/tool/builtin
158
159 # Shell execution contract: PowerShell identity, Chinese workspace paths,
160 # UTF-8 output, and ExitCode retention must gate PRs on native Windows.
161 # Keep this focused (not full ./internal/tool) so the smoke budget holds.
162 - name: test (Windows shell execution contract)
163 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name == 'pull_request'
164 timeout-minutes: 5
165 run: go test -timeout=3m -run '^TestBashPowerShellExecuteDetailedContract$|^TestBashPowerShell51PreflightRejectsAndAndDetailed$|^TestBashPowerShellOutputIsUTF8$|^TestBashPowerShellSurfacesNonZeroExit$|^TestBashPowerShellRejectsChaining$' ./internal/tool/builtin
166
167 - name: test (full)
168 if: env.RUN_STEPS == 'true' && runner.os == 'Windows' && github.event_name != 'pull_request'
169 timeout-minutes: 10
170 env:
171 # Run the prompt-cache prefix-stability guard (TestCacheHit*) in CI: a
172 # regression there silently tanks the cache hit rate the project is
173 # built around.
174 REASONIX_RELEASE_CACHE_GUARD: "1"
175 # Bound sandbox helper children in Windows tests so a failed OS-level
176 # launch cannot pin the Actions step after Go's package timeout fires.
177 WINDOWS_SANDBOX_WAIT_MS: "20000"
178 run: go test -p 4 -timeout=3m ./...
179
180 - name: test (Scoop desktop launch)
181 if: env.RUN_STEPS == 'true' && runner.os == 'Windows'
182 timeout-minutes: 10
183 shell: powershell
184 run: .\scripts\test-scoop-desktop-launch.ps1
185
186 race:
187 needs: changes
188 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false')
189 runs-on: ubuntu-latest
190 steps:
191 - uses: actions/checkout@v7
192
193 - uses: actions/setup-go@v7
194 with:
195 go-version-file: go.mod
196 cache: true
197
198 - name: Install and verify Linux sandbox backend
199 run: |
200 sudo apt-get update
201 sudo apt-get install -y bubblewrap
202 if sysctl -n kernel.unprivileged_userns_clone >/dev/null 2>&1; then
203 sudo sysctl -w kernel.unprivileged_userns_clone=1
204 fi
205 if sysctl -n kernel.apparmor_restrict_unprivileged_userns >/dev/null 2>&1; then
206 sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
207 fi
208 bwrap --ro-bind / / --dev /dev --proc /proc -- true || \
209 echo "::warning::bubblewrap is installed but this runner denies user namespaces; unavailable-backend tests will run fail-closed"
210
211 # The matrix never runs -race (it needs cgo); the project's concurrency
212 # (plugin fan-out, background phase B, jobs Kill/Wait) would otherwise
213 # ship without race coverage. Pull requests sweep only the
214 # concurrency-heavy packages so this required check stays fast; pushes
215 # to main-v2 keep the full ./... sweep as the safety net.
216 - name: test -race (concurrency packages)
217 if: github.event_name == 'pull_request'
218 env:
219 REASONIX_RELEASE_CACHE_GUARD: "1"
220 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/...
221
222 - name: test -race (full)
223 if: github.event_name != 'pull_request'
224 env:
225 REASONIX_RELEASE_CACHE_GUARD: "1"
226 run: go test -race ./...
227
228 # sdk/go is a nested stdlib-only module invisible to root `go test ./...`.
229 # Its DTOs are generated from internal/extension/protocol, and the
230 # host-side conformance tests spawn the SDK example, so the job runs the
231 # same three-OS matrix as the root tests.
232 sdk:
233 needs: changes
234 if: always()
235 strategy:
236 fail-fast: false
237 matrix:
238 os: [ubuntu-latest, macos-latest, windows-latest]
239 runs-on: ${{ matrix.os }}
240 env:
241 RUN_STEPS: ${{ github.event_name != 'pull_request' || needs.changes.outputs.sdk != 'false' }}
242 defaults:
243 run:
244 working-directory: sdk/go
245 steps:
246 - if: env.RUN_STEPS == 'true'
247 uses: actions/checkout@v7
248
249 - if: env.RUN_STEPS == 'true'
250 uses: actions/setup-go@v6
251 with:
252 go-version-file: sdk/go/go.mod
253
254 - name: gofmt
255 if: env.RUN_STEPS == 'true'
256 shell: bash
257 run: |
258 unformatted=$(gofmt -l .)
259 if [ -n "$unformatted" ]; then
260 echo "These files are not gofmt-clean:"
261 echo "$unformatted"
262 exit 1
263 fi
264
265 - name: vet
266 if: env.RUN_STEPS == 'true'
267 run: go vet ./...
268
269 - name: stdlib-only guard
270 if: env.RUN_STEPS == 'true'
271 shell: bash
272 run: |
273 # The SDK is a public module with a hard stdlib-only contract.
274 if go list -m all | grep -v '^github.com/esengine/DeepSeek-Reasonix/sdk/go$'; then
275 echo "sdk/go must not depend on anything outside the standard library"
276 exit 1
277 fi
278
279 - name: test
280 if: env.RUN_STEPS == 'true'
281 run: go test ./...
282
283 - name: test -race
284 if: github.event_name != 'pull_request' && env.RUN_STEPS == 'true'
285 run: go test -race ./...
286
287 desktop:
288 needs: changes
289 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false')
290 runs-on: ubuntu-22.04
291 defaults:
292 run:
293 working-directory: desktop
294 steps:
295 - uses: actions/checkout@v7
296
297 - uses: actions/setup-go@v7
298 with:
299 go-version-file: desktop/go.mod
300 cache: true
301 cache-dependency-path: desktop/go.sum
302
303 - uses: pnpm/action-setup@v6.0.9
304 with:
305 version: 10
306 run_install: false
307
308 - uses: actions/setup-node@v7
309 with:
310 node-version: "24"
311 cache: pnpm
312 cache-dependency-path: desktop/frontend/pnpm-lock.yaml
313
314 - name: Install Wails CLI
315 run: |
316 echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
317 go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
318
319 - name: gofmt
320 run: |
321 unformatted=$(gofmt -l .)
322 if [ -n "$unformatted" ]; then
323 echo "These files are not gofmt-clean:"
324 echo "$unformatted"
325 exit 1
326 fi
327
328 - name: go.mod tidy
329 run: |
330 go mod tidy
331 if ! git diff --quiet -- go.mod go.sum; then
332 echo "desktop/go.mod or go.sum is stale - run 'cd desktop && go mod tidy' and commit."
333 git diff -- go.mod go.sum
334 exit 1
335 fi
336
337 # WebKitGTK 4.0 toolchain (pinned to ubuntu-22.04; no webkit2_41 tag).
338 - name: Install Linux build deps
339 run: |
340 sudo apt-get update
341 sudo apt-get install -y gcc libgtk-3-dev libwebkit2gtk-4.0-dev
342
343 - name: Build frontend
344 run: |
345 wails generate module
346 pnpm --dir frontend install --frozen-lockfile
347 pnpm --dir frontend build
348
349 - name: Test desktop frontend
350 run: |
351 pnpm --dir frontend test:terminal
352 pnpm --dir frontend test:task-monitor
353
354 - name: Test usage statistics frontend
355 run: pnpm --dir frontend test:usage-stats
356
357 - name: vet
358 run: go vet ./...
359
360 - name: golangci-lint
361 uses: golangci/golangci-lint-action@v9
362 with:
363 version: v2.12.2
364 working-directory: desktop
365 args: --timeout=5m
366
367 - name: build
368 run: go build ./...
369
370 - name: test
371 run: go test ./...
372
373 # The extension work added new shared-state paths to the desktop
374 # runtime (tab/rebuild fences, extension UI). Race-sweep the module so
375 # regressions are CI-blocked, not just author-verified locally.
376 - name: test -race
377 run: go test -race ./...
378
379 # desktop/ is a separate module, so the root macOS matrix above does not
380 # compile or exercise the native PTY implementation.
381 desktop-macos:
382 needs: changes
383 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false')
384 runs-on: macos-latest
385 defaults:
386 run:
387 working-directory: desktop
388 steps:
389 - uses: actions/checkout@v7
390
391 - uses: actions/setup-go@v7
392 with:
393 go-version-file: desktop/go.mod
394 cache: true
395 cache-dependency-path: desktop/go.sum
396
397 - name: Test integrated terminal and PTY lifecycle
398 run: go test -race -run 'Test(ResolveTerminal|Terminal|EmptyTerminal|UnixTerminalProcess)' .
399
400 # Desktop project-root matching is case-insensitive only on Windows
401 # (sameDesktopPath folds case when os.PathSeparator is '\'), so the
402 # regression tests for that contract are named *OnWindows and can never
403 # run on the ubuntu leg above.
404 desktop-windows:
405 needs: changes
406 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.desktop != 'false')
407 runs-on: windows-latest
408 defaults:
409 run:
410 shell: bash
411 working-directory: desktop
412 steps:
413 - uses: actions/checkout@v7
414
415 - uses: actions/setup-go@v7
416 with:
417 go-version-file: desktop/go.mod
418 cache: true
419 cache-dependency-path: desktop/go.sum
420
421 - uses: pnpm/action-setup@v6.0.9
422 with:
423 version: 10
424 run_install: false
425
426 - uses: actions/setup-node@v7
427 with:
428 node-version: "24"
429 cache: pnpm
430 cache-dependency-path: desktop/frontend/pnpm-lock.yaml
431
432 - name: Install Wails CLI
433 run: |
434 echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH"
435 go install github.com/wailsapp/wails/v2/cmd/wails@v2.12.0
436
437 # go:embed of frontend/dist needs a built frontend before the package
438 # compiles, same as the ubuntu desktop leg.
439 - name: Build frontend
440 run: |
441 wails generate module
442 pnpm --dir frontend install --frozen-lockfile
443 pnpm --dir frontend build
444
445 - name: test (Windows desktop and update helper)
446 timeout-minutes: 15
447 run: go test ./...
448
449 # Installer packaging is packaging validation, not a code gate: run it
450 # on pushes to main-v2 (the release pipeline builds installers again
451 # anyway), but let pull requests stop after the test step.
452 - name: Install NSIS
453 if: github.event_name != 'pull_request'
454 run: pwsh -NoProfile -File ../scripts/install-nsis.ps1
455
456 - name: Build Windows installer and portable archive
457 if: github.event_name != 'pull_request'
458 timeout-minutes: 20
459 run: ../scripts/desktop-build.sh windows/amd64 v0.0.0-ci canary
460
461 lint:
462 needs: changes
463 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false')
464 runs-on: ubuntu-latest
465 steps:
466 - uses: actions/checkout@v7
467
468 - uses: actions/setup-go@v7
469 with:
470 go-version-file: go.mod
471 cache: true
472
473 - name: golangci-lint
474 uses: golangci/golangci-lint-action@v9
475 with:
476 version: v2.12.2
477 args: --timeout=5m
478
479 # Report-only complexity monitor for internal/agent. Does not duplicate the
480 # main lint/test/race gates; funlen+cyclop run with issues-exit-code=0 so
481 # the baseline is visible as a CI summary/artifact without blocking merges.
482 - name: agent complexity report
483 if: always()
484 run: |
485 # Reuse the golangci-lint binary installed by the previous step.
486 bash scripts/agent-complexity-report.sh
487
488 - name: upload agent complexity report
489 if: always()
490 uses: actions/upload-artifact@v7
491 with:
492 name: agent-complexity-report
493 path: agent-complexity-report.txt
494 if-no-files-found: ignore
495 retention-days: 14
496
497 - name: release workflow contracts
498 run: |
499 go run github.com/rhysd/actionlint/cmd/actionlint@v1.7.7 \
500 -ignore 'label "windows-11-arm" is unknown' \
501 .github/workflows/release-stable.yml \
502 .github/workflows/prepare-release-notes.yml \
503 .github/workflows/release-stable-trigger.yml \
504 .github/workflows/release.yml \
505 .github/workflows/release-npm.yml \
506 .github/workflows/release-desktop.yml \
507 .github/workflows/release-verify-issues.yml \
508 .github/workflows/docs-impact.yml
509 bash scripts/release-workflows.test.sh
510 node scripts/check-single-release-public-contract.mjs
511
512 # The site/ auth client has security-sensitive redirect-validation logic
513 # (safeNext) covered by node:test unit tests. Those tests use only Node
514 # builtins, so no `npm install` is needed — run them directly on every PR so
515 # a regression in redirect validation fails the build instead of shipping.
516 site:
517 needs: changes
518 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.site != 'false')
519 runs-on: ubuntu-latest
520 defaults:
521 run:
522 working-directory: site
523 steps:
524 - uses: actions/checkout@v7
525
526 - uses: actions/setup-node@v7
527 with:
528 node-version: "24"
529
530 - name: test
531 run: npm test
532
533 govulncheck:
534 needs: changes
535 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false')
536 runs-on: ubuntu-latest
537 continue-on-error: true # informational — stdlib vulns need a Go patch release
538 steps:
539 - uses: actions/checkout@v7
540
541 - uses: actions/setup-go@v7
542 with:
543 go-version-file: go.mod
544 cache: true
545
546 - name: install govulncheck
547 run: go install golang.org/x/vuln/cmd/govulncheck@latest
548
549 - name: govulncheck
550 run: govulncheck ./...
551
552 coverage:
553 needs: changes
554 if: always() && (github.event_name != 'pull_request' || needs.changes.outputs.code != 'false')
555 runs-on: ubuntu-latest
556 steps:
557 - uses: actions/checkout@v7
558
559 - uses: actions/setup-go@v7
560 with:
561 go-version-file: go.mod
562 cache: true
563
564 - name: test with coverage
565 run: go test -coverprofile=coverage.out -covermode=atomic ./...
566
567 - name: upload coverage
568 uses: actions/upload-artifact@v7
569 with:
570 name: coverage-report
571 path: coverage.out
572 retention-days: 7
573
573 lines YAML