返回 CodeWhale
ci.yml
根目录 / .github / workflows / ci.yml
1 name: CI
2
3 on:
4 push:
5 branches: [master, main]
6 pull_request:
7 branches: [master, main]
8 schedule:
9 - cron: '31 6 * * 1'
10 workflow_dispatch:
11 inputs:
12 expected_sha:
13 description: Exact 40-character commit selected by --ref (manual runs always force full CI)
14 required: true
15 type: string
16
17 permissions:
18 contents: read
19
20 concurrency:
21 group: ci-${{ github.event.pull_request.number || github.ref }}
22 cancel-in-progress: true
23
24 env:
25 CARGO_TERM_COLOR: always
26 CARGO_INCREMENTAL: 0
27 RUSTFLAGS: -Dwarnings
28
29 jobs:
30 changes:
31 name: Change detection
32 runs-on: ubuntu-latest
33 outputs:
34 heavy: ${{ steps.detect.outputs.heavy }}
35 workflow: ${{ steps.detect.outputs.workflow }}
36 mobile: ${{ steps.detect.outputs.mobile }}
37 actions: ${{ steps.detect.outputs.actions }}
38 steps:
39 - uses: actions/checkout@v7
40 with:
41 fetch-depth: 0
42 - name: Detect executable changes
43 id: detect
44 shell: bash
45 env:
46 EVENT_NAME: ${{ github.event_name }}
47 BASE_REF: ${{ github.base_ref }}
48 BEFORE_SHA: ${{ github.event.before }}
49 EXPECTED_SHA: ${{ inputs.expected_sha }}
50 run: |
51 set -euo pipefail
52
53 if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
54 if [[ "${#EXPECTED_SHA}" -ne 40 || "${EXPECTED_SHA}" =~ [^0-9a-fA-F] ]]; then
55 echo "::error::expected_sha must be a full 40-character commit SHA." >&2
56 exit 1
57 fi
58 actual="$(git rev-parse HEAD)"
59 expected_normalized="$(printf '%s' "${EXPECTED_SHA}" | tr '[:upper:]' '[:lower:]')"
60 if [[ "${actual}" != "${expected_normalized}" ]]; then
61 echo "::error::Dispatch resolved to ${actual}, not requested ${EXPECTED_SHA}." >&2
62 exit 1
63 fi
64 echo "Manual exact-head dispatch: forcing heavy, workflow, mobile, and action gates."
65 echo "heavy=true" >> "${GITHUB_OUTPUT}"
66 echo "workflow=true" >> "${GITHUB_OUTPUT}"
67 echo "mobile=true" >> "${GITHUB_OUTPUT}"
68 echo "actions=true" >> "${GITHUB_OUTPUT}"
69 exit 0
70 fi
71
72 if [[ "${EVENT_NAME}" == "schedule" ]]; then
73 echo "heavy=true" >> "${GITHUB_OUTPUT}"
74 echo "workflow=true" >> "${GITHUB_OUTPUT}"
75 echo "mobile=true" >> "${GITHUB_OUTPUT}"
76 echo "actions=true" >> "${GITHUB_OUTPUT}"
77 exit 0
78 fi
79
80 base=""
81 if [[ "${EVENT_NAME}" == "pull_request" && -n "${BASE_REF}" ]]; then
82 git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}" --depth=1
83 base="origin/${BASE_REF}"
84 elif [[ -n "${BEFORE_SHA}" && "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then
85 base="${BEFORE_SHA}"
86 fi
87
88 if [[ -z "${base}" ]]; then
89 echo "heavy=true" >> "${GITHUB_OUTPUT}"
90 echo "workflow=true" >> "${GITHUB_OUTPUT}"
91 echo "mobile=true" >> "${GITHUB_OUTPUT}"
92 echo "actions=true" >> "${GITHUB_OUTPUT}"
93 exit 0
94 fi
95
96 mapfile -t changed < <(git diff --name-only "${base}" "${GITHUB_SHA}" | sort)
97 heavy=false
98 workflow=false
99 mobile=false
100 actions=false
101 for path in "${changed[@]}"; do
102 # Heavy classification. ORDER MATTERS: must-stay-heavy inputs are
103 # matched BEFORE any light entry so a script that only a
104 # heavy-gated job exercises can never be misclassified as light.
105 # Anything unrecognized falls through to the default-heavy `*)`
106 # arm (fail-safe default-heavy). Light-classified scripts below
107 # are exercised by ALWAYS-on jobs/steps that run regardless of
108 # `heavy` (check-versions.sh / check-ohos-deps.sh via Version
109 # drift, check-coauthor-trailers.py via Lint), so no coverage is
110 # lost.
111 case "${path}" in
112 scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py)
113 heavy=true
114 ;;
115 docs/*|*.md|.github/PULL_REQUEST_TEMPLATE.md|.github/ISSUE_TEMPLATE/*|.github/scripts/agent-task-metadata.test.sh|.github/workflows/agent-task-labels.yml|.github/workflows/auto-tag.yml|.github/workflows/stale.yml|.github/workflows/triage.yml|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/install-dogfood.sh|scripts/release/install-dogfood.test.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py)
116 ;;
117 *)
118 heavy=true
119 ;;
120 esac
121 case "${path}" in
122 crates/workflow/*|workflows/rlm_cache_change.star|.github/workflows/ci.yml)
123 workflow=true
124 ;;
125 esac
126 # Mobile runtime surface: the `codewhale-tui serve --mobile`
127 # HTTP/SSE stack that scripts/mobile-smoke.sh exercises. Pull
128 # requests run the smoke only when one of these changes; every
129 # push to main still runs it unconditionally as the pre-release
130 # safety net for anything this filter misses.
131 case "${path}" in
132 crates/app-server/*|crates/tui/src/runtime_api*|crates/tui/src/runtime_mobile.html|crates/tui/src/runtime_threads*|crates/tui/src/main.rs|scripts/mobile-smoke.sh|.github/workflows/ci.yml|Cargo.lock|Cargo.toml)
133 mobile=true
134 ;;
135 esac
136 case "${path}" in
137 .github/workflows/*|.github/actionlint.yml)
138 actions=true
139 ;;
140 esac
141 done
142
143 echo "heavy=${heavy}" >> "${GITHUB_OUTPUT}"
144 echo "workflow=${workflow}" >> "${GITHUB_OUTPUT}"
145 echo "mobile=${mobile}" >> "${GITHUB_OUTPUT}"
146 echo "actions=${actions}" >> "${GITHUB_OUTPUT}"
147
148 echo "Heavy Rust CI required: ${heavy}"
149 echo "Workflow RLM cache CI required: ${workflow}"
150 echo "Mobile runtime smoke required (PRs): ${mobile}"
151 echo "Workflow lint required: ${actions}"
152 printf 'Changed files:\n'
153 printf ' %s\n' "${changed[@]}"
154
155 versions:
156 name: Version drift
157 runs-on: ubuntu-latest
158 steps:
159 - uses: actions/checkout@v7
160 - uses: dtolnay/rust-toolchain@stable
161 - uses: actions/setup-node@v7
162 with:
163 node-version: 20
164 - name: Check version drift
165 run: ./scripts/release/check-versions.sh
166 - name: Check OHOS dependency graph
167 run: ./scripts/release/check-ohos-deps.sh
168 - name: Check release helper contracts
169 run: |
170 bash .github/scripts/agent-task-metadata.test.sh
171 bash scripts/release/generate-release-body.test.sh
172 bash scripts/release/install-dogfood.test.sh
173 bash scripts/release/prepare-release.test.sh
174 bash scripts/release/require-release-tag-checkout.test.sh
175 bash scripts/release/verify-remote-tag.test.sh
176 bash .github/scripts/update-homebrew-tap.test.sh
177 node .github/scripts/release-workflows.test.js
178 node --test scripts/release/assemble-release-assets.test.js
179 node --test scripts/release/ensure-release-assets-absent.test.js
180 - name: Run runtime web client tests
181 # crates/tui/tests/runtime_web_client.test.mjs exercises the embedded
182 # web client's event/snapshot state machine; it ran nowhere before.
183 run: node --test crates/tui/tests/runtime_web_client.test.mjs
184
185 integrations:
186 name: Integrations
187 runs-on: ubuntu-latest
188 steps:
189 - uses: actions/checkout@v7
190 - uses: actions/setup-node@v7
191 with:
192 node-version: 22
193 - name: Run chat-bridge suites
194 # All four bridges + bridge-core ship dependency-free node --test
195 # suites that no workflow ran. weixin has no lockfile by design
196 # (zero deps); npm test works without npm ci everywhere here.
197 run: |
198 set -euo pipefail
199 for bridge in bridge-core feishu-bridge telegram-bridge wecom-bridge weixin-bridge; do
200 echo "== ${bridge}"
201 (cd "integrations/${bridge}" && npm test)
202 done
203
204 lint:
205 name: Lint
206 needs: changes
207 runs-on: ubuntu-latest
208 steps:
209 - uses: actions/checkout@v7
210 with:
211 fetch-depth: 0
212 - uses: dtolnay/rust-toolchain@master
213 if: needs.changes.outputs.heavy == 'true'
214 with:
215 toolchain: stable
216 components: rustfmt, clippy
217 - uses: mozilla-actions/sccache-action@v0.0.10
218 id: sccache
219 # Cache bootstrap failures (e.g. GitHub 504s fetching the sccache
220 # binary) degrade to an uncached build instead of failing product CI.
221 continue-on-error: true
222 if: needs.changes.outputs.heavy == 'true'
223 - name: Enable sccache
224 if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
225 shell: bash
226 run: |
227 echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
228 echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
229 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
230 - name: Install Linux system dependencies
231 if: needs.changes.outputs.heavy == 'true'
232 run: |
233 for i in 1 2 3 4 5; do
234 sudo apt-get update && break
235 echo "apt-get update failed (attempt $i); retrying in 15s"
236 sleep 15
237 done
238 sudo apt-get install -y libdbus-1-dev pkg-config
239 - uses: Swatinem/rust-cache@v2
240 if: needs.changes.outputs.heavy == 'true'
241 with:
242 cache-bin: false
243 # PRs restore the cache seeded by main but skip the expensive
244 # post-job save; sccache covers PR-specific compilation deltas.
245 save-if: ${{ github.ref == 'refs/heads/main' }}
246 - name: Check formatting
247 if: needs.changes.outputs.heavy == 'true'
248 run: cargo fmt --all -- --check
249 - name: Run clippy
250 if: needs.changes.outputs.heavy == 'true'
251 run: |
252 cargo clippy --workspace --all-features --locked -- \
253 -D warnings \
254 -A clippy::uninlined_format_args \
255 -A clippy::too_many_arguments \
256 -A clippy::unnecessary_map_or \
257 -A clippy::collapsible_if \
258 -A clippy::assertions_on_constants
259 - name: sccache stats
260 if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success'
261 continue-on-error: true
262 shell: bash
263 run: sccache --show-stats
264 - name: Check provider registry drift
265 if: needs.changes.outputs.heavy == 'true'
266 run: python3 scripts/check-provider-registry.py
267 # Clippy above runs without `--all-targets`, so it cannot see dead code
268 # that only tests keep alive. This ratchet covers that blind spot by
269 # refusing to let the `#[allow(dead_code)]` total rise (#4785).
270 - name: Check dead-code budget
271 if: needs.changes.outputs.heavy == 'true'
272 run: python3 scripts/check-dead-code-budget.py
273 # The offline runtime-contract measurement needs the full locked graph,
274 # dev-dependencies included (e.g. wiremock -> assert-json-diff), but
275 # clippy above builds no test targets and the rust-cache registry key
276 # derives from Cargo.lock, so any lock-changing PR (every dependabot
277 # bump) restores an empty cache and the hermetic `cargo test --offline`
278 # dies with "failed to download ... --offline was specified" before a
279 # single budget is measured. Fetch the locked graph once here so the
280 # measurement below is deterministic on every branch.
281 - name: Fetch locked dependency graph for offline measurement
282 if: needs.changes.outputs.heavy == 'true'
283 run: cargo fetch --locked
284 # Provider-free local measurement. The checker forces Cargo offline and
285 # the measurement script runs only locked, ignored Rust metric tests.
286 - name: Check runtime-contract budget
287 if: needs.changes.outputs.heavy == 'true'
288 run: python3 scripts/check-runtime-contract-budget.py
289 # Provider-free paused-consumer measurement of the production
290 # persistence request channel. RSS is sampled only on macOS; every host
291 # enforces the accepted/retained request and payload contract.
292 - name: Test persistence-backlog checker
293 if: needs.changes.outputs.heavy == 'true'
294 run: python3 scripts/test_check_persistence_backlog_budget.py
295 - name: Check persistence-backlog budget
296 if: needs.changes.outputs.heavy == 'true'
297 run: python3 scripts/check-persistence-backlog-budget.py
298 # Source-only ownership ratchet. Deletion and line-neutral consolidation
299 # pass; new packages/binaries/thousand-line module paths, a larger maximum
300 # module, or aggregate owned Rust growth require reviewed updates.
301 - name: Check source-structure budget
302 if: needs.changes.outputs.heavy == 'true'
303 run: python3 scripts/check-source-structure-budget.py
304 - name: Check README translations stay in sync
305 if: github.event_name != 'schedule'
306 run: python3 scripts/check-readme-translations.py
307 - name: Check README locale link symmetry
308 if: github.event_name != 'schedule'
309 run: bash scripts/check-readme-locales.sh
310 - name: Check TUI locale pack parity
311 if: github.event_name != 'schedule'
312 run: python3 scripts/check-tui-locale-parity.py
313 - name: Check website locale dictionary parity
314 if: github.event_name != 'schedule'
315 run: node web/scripts/check-locales.mjs
316 - name: Check harvested contributor credit
317 if: github.event_name != 'schedule'
318 shell: bash
319 run: |
320 if [[ "${{ github.event_name }}" == "pull_request" ]]; then
321 git fetch --no-tags origin "${{ github.base_ref }}"
322 RANGE="origin/${{ github.base_ref }}..HEAD"
323 elif [[ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then
324 RANGE="${{ github.event.before }}..${{ github.sha }}"
325 else
326 RANGE="HEAD~1..HEAD"
327 fi
328 python3 scripts/check-coauthor-trailers.py \
329 --author-map .github/AUTHOR_MAP \
330 --range "$RANGE" \
331 --check-authors
332 - name: Skip Rust lint for light change
333 if: needs.changes.outputs.heavy != 'true'
334 run: echo "No executable Rust changes detected; preserving required Lint context."
335 - name: Linux clippy location
336 if: needs.changes.outputs.heavy == 'true'
337 run: echo "Linux clippy/test gates run on CNB for mirrored fix/*, rebrand/*, work/v*, and main branches."
338
339 workflow-rlm-cache:
340 name: Workflow RLM cache
341 needs: changes
342 if: needs.changes.outputs.workflow == 'true'
343 runs-on: ubuntu-latest
344 steps:
345 - uses: actions/checkout@v7
346 - uses: dtolnay/rust-toolchain@stable
347 - uses: mozilla-actions/sccache-action@v0.0.10
348 id: sccache
349 continue-on-error: true
350 - name: Enable sccache
351 if: steps.sccache.outcome == 'success'
352 shell: bash
353 run: |
354 echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
355 echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
356 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
357 - uses: Swatinem/rust-cache@v2
358 with:
359 cache-bin: false
360 save-if: ${{ github.ref == 'refs/heads/main' }}
361 - name: Run RLM cache workflow mock/replay tests
362 run: cargo test -p codewhale-workflow --locked rlm_cache_change
363
364 test:
365 name: Test
366 needs: changes
367 # Required contexts "Test (ubuntu-latest)" / "Test (macos-latest)" /
368 # "Test (windows-latest)" derive from job name + matrix.os and are
369 # independent of runs-on. For light changes the macOS/Windows legs only
370 # echo a skip line, so run them on ubuntu instead of queueing for scarce
371 # macOS/Windows runners. Heavy changes use the real matrix OS as before.
372 # The ternary is safe: matrix.os is always a non-empty literal, so
373 # runs-on can never evaluate to empty.
374 runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
375 strategy:
376 # A failure on one desktop platform must not erase evidence from the
377 # other one. We need both conclusions to diagnose and release safely.
378 fail-fast: false
379 matrix:
380 # Linux workspace tests moved to CNB; GitHub keeps the platform
381 # coverage CNB cannot provide.
382 os: [ubuntu-latest, macos-latest, windows-latest]
383 steps:
384 - name: Skip tests for light change
385 if: needs.changes.outputs.heavy != 'true'
386 run: echo "No executable Rust changes detected; preserving required Test context."
387 - uses: actions/checkout@v7
388 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
389 - name: Test Windows installer PATH helper
390 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
391 shell: pwsh
392 run: ./scripts/installer/update-user-path.tests.ps1
393 - name: Install NSIS for Windows installer regression
394 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
395 shell: pwsh
396 run: choco install nsis -y --no-progress
397 - name: Test Windows installer PATH regression
398 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest'
399 shell: pwsh
400 run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation
401 - uses: dtolnay/rust-toolchain@stable
402 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
403 - uses: mozilla-actions/sccache-action@v0.0.10
404 id: sccache
405 continue-on-error: true
406 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
407 - name: Enable sccache
408 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch') && steps.sccache.outcome == 'success'
409 shell: bash
410 run: |
411 echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
412 echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
413 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
414 - name: Install Linux system dependencies
415 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name == 'workflow_dispatch'
416 run: |
417 for i in 1 2 3 4 5; do
418 sudo apt-get update && break
419 echo "apt-get update failed (attempt $i); retrying in 15s"
420 sleep 15
421 done
422 sudo apt-get install -y libdbus-1-dev pkg-config
423 - uses: Swatinem/rust-cache@v2
424 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
425 with:
426 cache-bin: false
427 save-if: ${{ github.ref == 'refs/heads/main' }}
428 - name: Run tests
429 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
430 run: cargo test --workspace --all-features --locked
431 env:
432 # Give test threads the stack the product gives itself. main.rs runs
433 # the owner thread and every tokio worker at
434 # CODEWHALE_MAIN_STACK_BYTES (16 MiB) because the engine and
435 # runtime-thread futures are genuinely deep. `#[tokio::test]` builds
436 # its own runtime and never sees that, so tests ran the same code on
437 # ~2 MiB (~1 MiB on Windows) — a configuration that never ships.
438 # That gap is what aborted the whole Windows test binary with
439 # STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_
440 # environment_id, masking every other Windows result (78afd8d3d4
441 # Box::pin'd that one frame; the mismatch itself remained). std reads
442 # this for any thread spawned without an explicit size, which covers
443 # both libtest's per-test threads and tokio's workers.
444 RUST_MIN_STACK: '16777216'
445 # The Ubuntu lint lane validates non-RSS backlog fields. Run the same
446 # source-bound measurement on macOS so loss or growth of RSS evidence
447 # fails closed instead of becoming an unsupported-field skip.
448 - name: Check persistence-backlog RSS budget
449 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
450 run: python3 scripts/check-persistence-backlog-budget.py
451 - name: Run isolated Skills Manager PTY acceptance
452 # This real-PTY scenario is deterministic in a fresh process (10/10
453 # locally) but can inherit event starvation after the full qa_pty
454 # suite on loaded Linux runners. Keep the assertion intact and run it
455 # separately on Unix after the workspace suite has released its PTYs.
456 if: needs.changes.outputs.heavy == 'true' && matrix.os != 'windows-latest' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
457 run: cargo test -p codewhale-tui --test qa_pty skills_opens_manager_owned_then_compatible -- --ignored --exact
458 - name: Lockfile drift guard
459 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
460 run: git diff --exit-code -- Cargo.lock
461 - name: Run Offline Eval Harness
462 # The eval harness is OS-independent prompt/composition checking;
463 # running it once (on the faster macOS leg, warm from the test build)
464 # instead of once per desktop OS keeps the coverage while taking
465 # ~2min off the Windows critical path.
466 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest'
467 run: cargo run -p codewhale-tui --all-features -- eval
468 - name: sccache stats
469 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch') && steps.sccache.outcome == 'success'
470 continue-on-error: true
471 shell: bash
472 run: sccache --show-stats
473 - name: Linux test location
474 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch'
475 run: echo "Linux workspace tests run on CNB for mirrored first-party branches."
476
477 npm-wrapper-smoke:
478 name: npm wrapper smoke
479 needs: changes
480 if: github.event_name != 'schedule'
481 # Same ternary rationale as the Test job: light legs only echo, so keep
482 # them off macOS/Windows runners. On pull_request the matrix is
483 # ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)"
484 # context is unaffected.
485 runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }}
486 strategy:
487 matrix:
488 os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }}
489 steps:
490 - name: Skip npm wrapper smoke for light change
491 if: needs.changes.outputs.heavy != 'true'
492 run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context."
493 - uses: actions/checkout@v7
494 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
495 - uses: dtolnay/rust-toolchain@stable
496 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
497 - uses: mozilla-actions/sccache-action@v0.0.10
498 id: sccache
499 continue-on-error: true
500 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
501 - name: Enable sccache
502 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch') && steps.sccache.outcome == 'success'
503 shell: bash
504 run: |
505 echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
506 echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
507 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
508 - uses: actions/setup-node@v7
509 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
510 with:
511 node-version: 20
512 - name: Install Linux system dependencies
513 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name == 'workflow_dispatch'
514 run: |
515 for i in 1 2 3 4 5; do
516 sudo apt-get update && break
517 echo "apt-get update failed (attempt $i); retrying in 15s"
518 sleep 15
519 done
520 sudo apt-get install -y libdbus-1-dev pkg-config
521 - uses: Swatinem/rust-cache@v2
522 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
523 with:
524 cache-bin: false
525 save-if: ${{ github.ref == 'refs/heads/main' }}
526 - name: Build wrapper binaries
527 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
528 # The smoke validates wrapper install/delegation plumbing, not
529 # codegen quality, so skip fat LTO + codegen-units=1 for a much
530 # cheaper release build. Shipped binaries keep the real profile via
531 # the Release workflow.
532 env:
533 CARGO_PROFILE_RELEASE_LTO: 'off'
534 CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
535 run: cargo build --release --locked -p codewhale-cli -p codewhale-tui
536 - name: Smoke wrapper install and delegated entrypoints
537 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch')
538 run: node scripts/release/npm-wrapper-smoke.js
539 - name: sccache stats
540 if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch') && steps.sccache.outcome == 'success'
541 continue-on-error: true
542 shell: bash
543 run: sccache --show-stats
544 - name: Linux smoke location
545 if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch'
546 run: echo "Linux npm wrapper smoke runs on CNB for mirrored first-party branches."
547
548 mobile-smoke:
549 name: Mobile runtime smoke
550 needs: changes
551 # Not a required PR context. Pull requests run it only when the mobile
552 # runtime surface changed (see the `mobile` filter above); every push to
553 # main runs it unconditionally as the pre-release safety net.
554 if: >-
555 github.event_name != 'schedule' &&
556 needs.changes.outputs.heavy == 'true' &&
557 (github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true')
558 runs-on: ubuntu-latest
559 steps:
560 - uses: actions/checkout@v7
561 - uses: dtolnay/rust-toolchain@stable
562 - uses: mozilla-actions/sccache-action@v0.0.10
563 id: sccache
564 continue-on-error: true
565 - name: Enable sccache
566 if: steps.sccache.outcome == 'success'
567 shell: bash
568 run: |
569 echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}"
570 echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}"
571 echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}"
572 - name: Install Linux system dependencies
573 run: |
574 for i in 1 2 3 4 5; do
575 sudo apt-get update && break
576 echo "apt-get update failed (attempt $i); retrying in 15s"
577 sleep 15
578 done
579 sudo apt-get install -y libdbus-1-dev pkg-config
580 - uses: Swatinem/rust-cache@v2
581 with:
582 cache-bin: false
583 save-if: ${{ github.ref == 'refs/heads/main' }}
584 - name: Run mobile smoke tests
585 # The smoke exercises HTTP/SSE runtime behaviour, not codegen
586 # quality; skipping fat LTO + codegen-units=1 cuts the in-script
587 # release build from ~12min to a fraction of that.
588 env:
589 CARGO_PROFILE_RELEASE_LTO: 'off'
590 CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16'
591 run: ./scripts/mobile-smoke.sh
592 - name: sccache stats
593 if: steps.sccache.outcome == 'success'
594 continue-on-error: true
595 shell: bash
596 run: sccache --show-stats
597
598 actionlint:
599 name: Workflow lint
600 needs: changes
601 if: needs.changes.outputs.actions == 'true'
602 runs-on: ubuntu-latest
603 steps:
604 - uses: actions/checkout@v7
605 - name: Run actionlint
606 uses: docker://rhysd/actionlint:1.7.12
607 with:
608 # SC2129 (grouped redirects) is style-only and endemic to the
609 # existing GITHUB_ENV/GITHUB_OUTPUT append pattern; SC2221/SC2222
610 # flag the long-standing `*.md` glob shadowing the PR-template
611 # entry in change detection, which is intentional.
612 args: -color -ignore SC2129 -ignore SC2221 -ignore SC2222
613
614 # Check documentation builds without warnings
615 docs:
616 name: Documentation
617 if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
618 runs-on: ubuntu-latest
619 steps:
620 - uses: actions/checkout@v7
621 - uses: dtolnay/rust-toolchain@stable
622 - name: Install Linux system dependencies
623 if: runner.os == 'Linux'
624 run: |
625 for i in 1 2 3 4 5; do
626 sudo apt-get update && break
627 echo "apt-get update failed (attempt $i); retrying in 15s"
628 sleep 15
629 done
630 sudo apt-get install -y libdbus-1-dev pkg-config
631 - uses: Swatinem/rust-cache@v2
632 with:
633 cache-bin: false
634 - name: Build docs
635 run: cargo doc --workspace --no-deps
636 env:
637 RUSTDOCFLAGS: -Dwarnings
638
638 lines YAML