| 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 | # PRs still share one group so a new push cancels the superseded head. |
| 22 | # Push/schedule/dispatch on main must be keyed by SHA: with cancel-in-progress |
| 23 | # false, GitHub still cancels a *pending* run in the same group when a new |
| 24 | # one queues. That is how 31 of the last 40 main CI runs vanished without a |
| 25 | # verdict (test bankruptcy, 2026-08-19). Each SHA gets its own group so |
| 26 | # every commit on main actually finishes. |
| 27 | group: ${{ github.event_name == 'pull_request' && format('ci-pr-{0}', github.event.pull_request.number) || format('ci-{0}-{1}', github.workflow, github.sha) }} |
| 28 | cancel-in-progress: ${{ github.event_name == 'pull_request' }} |
| 29 | |
| 30 | env: |
| 31 | CARGO_TERM_COLOR: always |
| 32 | CARGO_INCREMENTAL: 0 |
| 33 | RUSTFLAGS: -Dwarnings |
| 34 | # Test threads share a process and tokio/async frames run deep; the default |
| 35 | # 2 MiB stack overflowed sporadically in runtime_api::tests::start_turn_* |
| 36 | # under load and aborted the whole lib suite (signal 6). 8 MiB is the |
| 37 | # measured-safe floor; nextest's per-process runs are unaffected either way. |
| 38 | RUST_MIN_STACK: 8388608 |
| 39 | |
| 40 | jobs: |
| 41 | changes: |
| 42 | name: Change detection |
| 43 | timeout-minutes: 10 |
| 44 | runs-on: ubuntu-latest |
| 45 | outputs: |
| 46 | heavy: ${{ steps.detect.outputs.heavy }} |
| 47 | workflow: ${{ steps.detect.outputs.workflow }} |
| 48 | mobile: ${{ steps.detect.outputs.mobile }} |
| 49 | actions: ${{ steps.detect.outputs.actions }} |
| 50 | trusted: ${{ steps.trust.outputs.trusted }} |
| 51 | steps: |
| 52 | - name: Classify event trust |
| 53 | id: trust |
| 54 | shell: bash |
| 55 | env: |
| 56 | EVENT_NAME: ${{ github.event_name }} |
| 57 | HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }} |
| 58 | THIS_REPO: ${{ github.repository }} |
| 59 | run: | |
| 60 | set -euo pipefail |
| 61 | # "trusted" means the code came from this repository, not a fork. |
| 62 | # Only trusted events may run on the self-hosted macOS runner: this |
| 63 | # repo is public with thousands of forks, and a fork PR on a |
| 64 | # self-hosted runner is arbitrary code execution on that machine. |
| 65 | if [ "${EVENT_NAME}" != "pull_request" ] || [ "${HEAD_REPO}" = "${THIS_REPO}" ]; then |
| 66 | echo "trusted=true" >> "$GITHUB_OUTPUT" |
| 67 | else |
| 68 | echo "trusted=false" >> "$GITHUB_OUTPUT" |
| 69 | fi |
| 70 | - uses: actions/checkout@v7 |
| 71 | with: |
| 72 | fetch-depth: 0 |
| 73 | - name: Detect executable changes |
| 74 | id: detect |
| 75 | shell: bash |
| 76 | env: |
| 77 | EVENT_NAME: ${{ github.event_name }} |
| 78 | BASE_REF: ${{ github.base_ref }} |
| 79 | BEFORE_SHA: ${{ github.event.before }} |
| 80 | EXPECTED_SHA: ${{ inputs.expected_sha }} |
| 81 | run: | |
| 82 | set -euo pipefail |
| 83 | |
| 84 | if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then |
| 85 | if [[ "${#EXPECTED_SHA}" -ne 40 || "${EXPECTED_SHA}" =~ [^0-9a-fA-F] ]]; then |
| 86 | echo "::error::expected_sha must be a full 40-character commit SHA." >&2 |
| 87 | exit 1 |
| 88 | fi |
| 89 | actual="$(git rev-parse HEAD)" |
| 90 | expected_normalized="$(printf '%s' "${EXPECTED_SHA}" | tr '[:upper:]' '[:lower:]')" |
| 91 | if [[ "${actual}" != "${expected_normalized}" ]]; then |
| 92 | echo "::error::Dispatch resolved to ${actual}, not requested ${EXPECTED_SHA}." >&2 |
| 93 | exit 1 |
| 94 | fi |
| 95 | echo "Manual exact-head dispatch: forcing heavy, workflow, mobile, and action gates." |
| 96 | echo "heavy=true" >> "${GITHUB_OUTPUT}" |
| 97 | echo "workflow=true" >> "${GITHUB_OUTPUT}" |
| 98 | echo "mobile=true" >> "${GITHUB_OUTPUT}" |
| 99 | echo "actions=true" >> "${GITHUB_OUTPUT}" |
| 100 | exit 0 |
| 101 | fi |
| 102 | |
| 103 | if [[ "${EVENT_NAME}" == "schedule" ]]; then |
| 104 | echo "heavy=true" >> "${GITHUB_OUTPUT}" |
| 105 | echo "workflow=true" >> "${GITHUB_OUTPUT}" |
| 106 | echo "mobile=true" >> "${GITHUB_OUTPUT}" |
| 107 | echo "actions=true" >> "${GITHUB_OUTPUT}" |
| 108 | exit 0 |
| 109 | fi |
| 110 | |
| 111 | base="" |
| 112 | if [[ "${EVENT_NAME}" == "pull_request" && -n "${BASE_REF}" ]]; then |
| 113 | git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}" --depth=1 |
| 114 | base="origin/${BASE_REF}" |
| 115 | elif [[ -n "${BEFORE_SHA}" && "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then |
| 116 | base="${BEFORE_SHA}" |
| 117 | fi |
| 118 | |
| 119 | if [[ -z "${base}" ]]; then |
| 120 | echo "heavy=true" >> "${GITHUB_OUTPUT}" |
| 121 | echo "workflow=true" >> "${GITHUB_OUTPUT}" |
| 122 | echo "mobile=true" >> "${GITHUB_OUTPUT}" |
| 123 | echo "actions=true" >> "${GITHUB_OUTPUT}" |
| 124 | exit 0 |
| 125 | fi |
| 126 | |
| 127 | mapfile -t changed < <(git diff --name-only "${base}" "${GITHUB_SHA}" | sort) |
| 128 | heavy=false |
| 129 | workflow=false |
| 130 | mobile=false |
| 131 | actions=false |
| 132 | for path in "${changed[@]}"; do |
| 133 | # Heavy classification. ORDER MATTERS: must-stay-heavy inputs are |
| 134 | # matched BEFORE any light entry so a script that only a |
| 135 | # heavy-gated job exercises can never be misclassified as light. |
| 136 | # Anything unrecognized falls through to the default-heavy `*)` |
| 137 | # arm (fail-safe default-heavy). Light-classified scripts below |
| 138 | # are exercised by ALWAYS-on jobs/steps that run regardless of |
| 139 | # `heavy` (check-versions.sh / check-ohos-deps.sh via Version |
| 140 | # drift, dev-cache/dev-test |
| 141 | # self-checks via Version drift), so no coverage is lost. |
| 142 | case "${path}" in |
| 143 | scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py) |
| 144 | heavy=true |
| 145 | ;; |
| 146 | docs/*|*.md|packaging/aur/*|.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/dev-cache.sh|scripts/dev-cache.test.sh|scripts/dev-cargo.sh|scripts/dev-test.sh) |
| 147 | ;; |
| 148 | *) |
| 149 | heavy=true |
| 150 | ;; |
| 151 | esac |
| 152 | case "${path}" in |
| 153 | crates/workflow/*|.github/workflows/ci.yml) |
| 154 | workflow=true |
| 155 | ;; |
| 156 | esac |
| 157 | # Mobile runtime surface: the `codewhale serve --mobile` |
| 158 | # HTTP/SSE stack that scripts/mobile-smoke.sh exercises. Pull |
| 159 | # requests run the smoke only when one of these changes; every |
| 160 | # push to main still runs it unconditionally as the pre-release |
| 161 | # safety net for anything this filter misses. |
| 162 | case "${path}" in |
| 163 | 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) |
| 164 | mobile=true |
| 165 | ;; |
| 166 | esac |
| 167 | case "${path}" in |
| 168 | .github/workflows/*|.github/actionlint.yml) |
| 169 | actions=true |
| 170 | ;; |
| 171 | esac |
| 172 | done |
| 173 | |
| 174 | echo "heavy=${heavy}" >> "${GITHUB_OUTPUT}" |
| 175 | echo "workflow=${workflow}" >> "${GITHUB_OUTPUT}" |
| 176 | echo "mobile=${mobile}" >> "${GITHUB_OUTPUT}" |
| 177 | echo "actions=${actions}" >> "${GITHUB_OUTPUT}" |
| 178 | |
| 179 | echo "Heavy Rust CI required: ${heavy}" |
| 180 | echo "Workflow RLM cache CI required: ${workflow}" |
| 181 | echo "Mobile runtime smoke required (PRs): ${mobile}" |
| 182 | echo "Workflow lint required: ${actions}" |
| 183 | printf 'Changed files:\n' |
| 184 | printf ' %s\n' "${changed[@]}" |
| 185 | |
| 186 | versions: |
| 187 | name: Version drift |
| 188 | timeout-minutes: 15 |
| 189 | runs-on: ubuntu-latest |
| 190 | steps: |
| 191 | - uses: actions/checkout@v7 |
| 192 | with: |
| 193 | fetch-depth: 0 |
| 194 | - uses: dtolnay/rust-toolchain@stable |
| 195 | - uses: actions/setup-node@v7 |
| 196 | with: |
| 197 | node-version: 20 |
| 198 | - name: Check version drift |
| 199 | # Checks 7 and 12 audit the previous-tag..HEAD commit range, not this |
| 200 | # tree, so a receipt another merge forgot reddens every open PR. They |
| 201 | # report here and block on every release path (release-candidate.yml, |
| 202 | # auto-tag.yml, release.yml, prepare-release.sh), which is where a |
| 203 | # missing receipt actually matters. |
| 204 | run: ./scripts/release/check-versions.sh --range-audit-advisory |
| 205 | - name: Check OHOS dependency graph |
| 206 | run: ./scripts/release/check-ohos-deps.sh |
| 207 | - name: Check release helper contracts |
| 208 | run: | |
| 209 | bash .github/scripts/agent-task-metadata.test.sh |
| 210 | bash scripts/release/check-feature-release-notes.test.sh |
| 211 | bash scripts/release/generate-release-body.test.sh |
| 212 | bash scripts/release/install-dogfood.test.sh |
| 213 | bash scripts/release/prepare-release.test.sh |
| 214 | bash scripts/release/require-release-tag-checkout.test.sh |
| 215 | bash scripts/release/validate-crate-publish-order.test.sh |
| 216 | python3 scripts/release/publish-crates.test.py |
| 217 | bash scripts/release/verify-remote-tag.test.sh |
| 218 | bash packaging/aur/render.test.sh |
| 219 | sh scripts/dev-cache.test.sh |
| 220 | sh scripts/with-hermetic-test-home.test.sh |
| 221 | bash .github/scripts/update-homebrew-tap.test.sh |
| 222 | node .github/scripts/release-workflows.test.js |
| 223 | node --test scripts/release/assemble-release-assets.test.js |
| 224 | node --test scripts/release/ensure-release-assets-absent.test.js |
| 225 | - name: Run runtime web client tests |
| 226 | # crates/tui/tests/runtime_web_client.test.mjs exercises the embedded |
| 227 | # web client's event/snapshot state machine; it ran nowhere before. |
| 228 | run: node --test crates/tui/tests/runtime_web_client.test.mjs |
| 229 | |
| 230 | integrations: |
| 231 | name: Integrations |
| 232 | timeout-minutes: 15 |
| 233 | runs-on: ubuntu-latest |
| 234 | steps: |
| 235 | - uses: actions/checkout@v7 |
| 236 | - uses: actions/setup-node@v7 |
| 237 | with: |
| 238 | node-version: 22 |
| 239 | - name: Run chat-bridge suites |
| 240 | # All four bridges + bridge-core ship dependency-free node --test |
| 241 | # suites that no workflow ran. weixin has no lockfile by design |
| 242 | # (zero deps); npm test works without npm ci everywhere here. |
| 243 | run: | |
| 244 | set -euo pipefail |
| 245 | for bridge in bridge-core feishu-bridge telegram-bridge wecom-bridge weixin-bridge; do |
| 246 | echo "== ${bridge}" |
| 247 | (cd "integrations/${bridge}" && npm test) |
| 248 | done |
| 249 | |
| 250 | - name: Build computer-use test desktop |
| 251 | # Spawn assertions have short request deadlines; keep the cold image |
| 252 | # build outside those deadlines and fail image preparation explicitly. |
| 253 | timeout-minutes: 10 |
| 254 | run: >- |
| 255 | docker build --tag codewhale-cu-linux |
| 256 | --file crates/tui/plugins/computer-use/docker/Dockerfile |
| 257 | crates/tui/plugins/computer-use |
| 258 | |
| 259 | - name: Run computer-use plugin suites |
| 260 | # The bundled plugin is dependency-free too; its suites cover the |
| 261 | # manifest contract, the registry, the exec/ssh transport, the four |
| 262 | # platform backends, and the MCP stdio protocol. No GUI input runs. |
| 263 | run: (cd crates/tui/plugins/computer-use && npm test) |
| 264 | |
| 265 | vscode-extension: |
| 266 | name: VS Code extension |
| 267 | timeout-minutes: 15 |
| 268 | runs-on: ubuntu-latest |
| 269 | defaults: |
| 270 | run: |
| 271 | working-directory: extensions/vscode |
| 272 | steps: |
| 273 | - uses: actions/checkout@v7 |
| 274 | - uses: actions/setup-node@v7 |
| 275 | with: |
| 276 | # The extension targets VS Code >=1.90, whose extension host is |
| 277 | # Node 20, and its @types/node pin is ^20. Build and test on the |
| 278 | # runtime the extension actually ships against. |
| 279 | node-version: 20 |
| 280 | - name: Install extension dependencies |
| 281 | run: npm ci |
| 282 | - name: Run VS Code extension suites |
| 283 | # extensions/vscode ships node --test suites (api, markdown, sse) that |
| 284 | # NO workflow ran: release.yml only reads package.json for a version |
| 285 | # string, so the whole client compiled and shipped without its tests or |
| 286 | # `tsc` ever running in CI. `npm test` compiles first (tsc -p ./), so |
| 287 | # this is the type-check gate for the extension too. |
| 288 | run: npm test |
| 289 | - name: Package VS Code extension |
| 290 | run: npm run package |
| 291 | |
| 292 | safety-gate: |
| 293 | name: Safety gate |
| 294 | needs: changes |
| 295 | if: needs.changes.outputs.heavy == 'true' |
| 296 | timeout-minutes: 15 |
| 297 | runs-on: ubuntu-latest |
| 298 | steps: |
| 299 | - uses: actions/checkout@v7 |
| 300 | - uses: dtolnay/rust-toolchain@master |
| 301 | with: |
| 302 | toolchain: stable |
| 303 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 304 | id: sccache |
| 305 | continue-on-error: true |
| 306 | - name: Enable sccache |
| 307 | if: steps.sccache.outcome == 'success' |
| 308 | shell: bash |
| 309 | run: | |
| 310 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 311 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 312 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 313 | - name: Install Linux system dependencies |
| 314 | run: | |
| 315 | for i in 1 2 3 4 5; do |
| 316 | sudo apt-get update && break |
| 317 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 318 | sleep 15 |
| 319 | done |
| 320 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 321 | - uses: Swatinem/rust-cache@v2 |
| 322 | with: |
| 323 | cache-bin: false |
| 324 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 325 | - name: Hermetic safety and authorization tests |
| 326 | env: |
| 327 | RUST_MIN_STACK: "8388608" |
| 328 | run: | |
| 329 | sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-tui --lib --locked -- auto_review authority sandbox |
| 330 | sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-execpolicy --locked |
| 331 | |
| 332 | lint: |
| 333 | name: Lint |
| 334 | needs: changes |
| 335 | timeout-minutes: 45 |
| 336 | runs-on: ubuntu-latest |
| 337 | steps: |
| 338 | - uses: actions/checkout@v7 |
| 339 | with: |
| 340 | fetch-depth: 0 |
| 341 | - uses: dtolnay/rust-toolchain@master |
| 342 | if: needs.changes.outputs.heavy == 'true' |
| 343 | with: |
| 344 | toolchain: stable |
| 345 | components: rustfmt, clippy |
| 346 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 347 | id: sccache |
| 348 | # Cache bootstrap failures (e.g. GitHub 504s fetching the sccache |
| 349 | # binary) degrade to an uncached build instead of failing product CI. |
| 350 | continue-on-error: true |
| 351 | if: needs.changes.outputs.heavy == 'true' |
| 352 | - name: Enable sccache |
| 353 | if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success' |
| 354 | shell: bash |
| 355 | run: | |
| 356 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 357 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 358 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 359 | - name: Install Linux system dependencies |
| 360 | if: needs.changes.outputs.heavy == 'true' |
| 361 | run: | |
| 362 | for i in 1 2 3 4 5; do |
| 363 | sudo apt-get update && break |
| 364 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 365 | sleep 15 |
| 366 | done |
| 367 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 368 | - uses: Swatinem/rust-cache@v2 |
| 369 | if: needs.changes.outputs.heavy == 'true' |
| 370 | with: |
| 371 | cache-bin: false |
| 372 | # PRs restore the cache seeded by main but skip the expensive |
| 373 | # post-job save; sccache covers PR-specific compilation deltas. |
| 374 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 375 | - name: Check formatting |
| 376 | if: needs.changes.outputs.heavy == 'true' |
| 377 | run: cargo fmt --all -- --check |
| 378 | - name: Run clippy |
| 379 | # --all-targets, because without it CI never lints test code at all. |
| 380 | # That gap is not theoretical: the v0.9.10 release gate opened with |
| 381 | # four clippy failures sitting on a green main, and every one of them |
| 382 | # was in a test target. crates/tui/AGENTS.md already documents the |
| 383 | # all-targets command as the release gate; this makes CI run the gate |
| 384 | # it points contributors at instead of a weaker subset. |
| 385 | # |
| 386 | # collapsible_if and assertions_on_constants are no longer allowed for |
| 387 | # the same reason — they were three of those four, so the allowances |
| 388 | # were hiding exactly the class of problem that reached the gate. The |
| 389 | # three that remain are deliberate project style, not oversights. |
| 390 | if: needs.changes.outputs.heavy == 'true' |
| 391 | run: | |
| 392 | cargo clippy --workspace --all-targets --all-features --locked -- \ |
| 393 | -D warnings \ |
| 394 | -A clippy::uninlined_format_args \ |
| 395 | -A clippy::too_many_arguments \ |
| 396 | -A clippy::unnecessary_map_or |
| 397 | - name: sccache stats |
| 398 | if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success' |
| 399 | continue-on-error: true |
| 400 | shell: bash |
| 401 | run: sccache --show-stats |
| 402 | - name: Check provider registry drift |
| 403 | if: needs.changes.outputs.heavy == 'true' |
| 404 | run: python3 scripts/check-provider-registry.py |
| 405 | - name: Check command-contract prototype boundary |
| 406 | if: needs.changes.outputs.heavy == 'true' |
| 407 | run: | |
| 408 | python3 scripts/test_check_command_crate_boundaries.py |
| 409 | python3 scripts/check-command-crate-boundaries.py |
| 410 | - name: Check command migration manifest |
| 411 | if: needs.changes.outputs.heavy == 'true' |
| 412 | env: |
| 413 | PR_BASE_SHA: ${{ github.event.pull_request.base.sha }} |
| 414 | PUSH_BEFORE_SHA: ${{ github.event.before }} |
| 415 | run: | |
| 416 | python3 scripts/test_check_command_migration_manifest.py |
| 417 | baseline="${PR_BASE_SHA:-${PUSH_BEFORE_SHA:-}}" |
| 418 | if [[ -n "${baseline}" && ! "${baseline}" =~ ^0+$ ]]; then |
| 419 | git fetch --no-tags origin "${baseline}" |
| 420 | python3 scripts/check-command-migration-manifest.py --baseline-ref "${baseline}" |
| 421 | else |
| 422 | python3 scripts/check-command-migration-manifest.py |
| 423 | fi |
| 424 | - name: Check reqwest client constructors |
| 425 | if: needs.changes.outputs.heavy == 'true' |
| 426 | run: | |
| 427 | python3 scripts/test_check_reqwest_builders.py |
| 428 | python3 scripts/check-reqwest-builders.py |
| 429 | # Clippy above runs without `--all-targets`, so it cannot see dead code |
| 430 | # that only tests keep alive. This ratchet covers that blind spot by |
| 431 | # refusing to let the `#[allow(dead_code)]` total rise (#4785). |
| 432 | - name: Check dead-code budget |
| 433 | if: needs.changes.outputs.heavy == 'true' |
| 434 | # Advisory on pull requests: this asserts a whole-repo property, so a |
| 435 | # branch can fail it for debt it inherited rather than added, and the |
| 436 | # fix would be rebasing instead of editing code. It stays blocking on |
| 437 | # pushes to main, where the number is actually actionable. |
| 438 | continue-on-error: ${{ github.event_name == 'pull_request' }} |
| 439 | run: python3 scripts/check-dead-code-budget.py |
| 440 | # Ratchet for blocking calls that could park Tokio workers: any new |
| 441 | # thread::sleep/std::fs site outside spawn_blocking, dedicated-thread, |
| 442 | # or test scopes must be isolated or budgeted (#6149). |
| 443 | - name: Check blocking-calls budget |
| 444 | if: needs.changes.outputs.heavy == 'true' |
| 445 | continue-on-error: ${{ github.event_name == 'pull_request' }} |
| 446 | run: python3 scripts/check-blocking-calls-budget.py |
| 447 | - name: Test runtime-contract measurement harness |
| 448 | if: needs.changes.outputs.heavy == 'true' |
| 449 | run: | |
| 450 | python3 scripts/test_measure_runtime_contract.py |
| 451 | python3 scripts/test_check_runtime_contract_budget.py |
| 452 | # The offline runtime-contract measurement needs the full locked graph, |
| 453 | # dev-dependencies included (e.g. wiremock -> assert-json-diff), but |
| 454 | # clippy above builds no test targets and the rust-cache registry key |
| 455 | # derives from Cargo.lock, so any lock-changing PR (every dependabot |
| 456 | # bump) restores an empty cache and the hermetic `cargo test --offline` |
| 457 | # dies with "failed to download ... --offline was specified" before a |
| 458 | # single budget is measured. Fetch the locked graph once here so the |
| 459 | # measurement below is deterministic on every branch. |
| 460 | - name: Fetch locked dependency graph for offline measurement |
| 461 | if: needs.changes.outputs.heavy == 'true' |
| 462 | run: cargo fetch --locked |
| 463 | # Provider-free local measurement. The checker forces Cargo offline and |
| 464 | # the measurement script runs only locked, ignored Rust metric tests. |
| 465 | - name: Check runtime-contract budget |
| 466 | if: needs.changes.outputs.heavy == 'true' |
| 467 | # Advisory on pull requests: this asserts a whole-repo property, so a |
| 468 | # branch can fail it for debt it inherited rather than added, and the |
| 469 | # fix would be rebasing instead of editing code. It stays blocking on |
| 470 | # pushes to main, where the number is actually actionable. |
| 471 | continue-on-error: ${{ github.event_name == 'pull_request' }} |
| 472 | run: python3 scripts/check-runtime-contract-budget.py |
| 473 | # Provider-free paused-consumer measurement of the production |
| 474 | # persistence request channel. RSS is sampled only on macOS; every host |
| 475 | # enforces the accepted/retained request and payload contract. |
| 476 | - name: Test persistence-backlog measurement and checker harnesses |
| 477 | if: needs.changes.outputs.heavy == 'true' |
| 478 | run: | |
| 479 | python3 scripts/test_measure_persistence_backlog.py |
| 480 | python3 scripts/test_check_persistence_backlog_budget.py |
| 481 | - name: Check persistence-backlog budget |
| 482 | if: needs.changes.outputs.heavy == 'true' |
| 483 | # Advisory on pull requests: this asserts a whole-repo property, so a |
| 484 | # branch can fail it for debt it inherited rather than added, and the |
| 485 | # fix would be rebasing instead of editing code. It stays blocking on |
| 486 | # pushes to main, where the number is actually actionable. |
| 487 | continue-on-error: ${{ github.event_name == 'pull_request' }} |
| 488 | run: python3 scripts/check-persistence-backlog-budget.py |
| 489 | - name: Check README translations stay in sync |
| 490 | if: github.event_name != 'schedule' |
| 491 | run: python3 scripts/check-readme-translations.py |
| 492 | - name: Check README locale link symmetry |
| 493 | if: github.event_name != 'schedule' |
| 494 | run: bash scripts/check-readme-locales.sh |
| 495 | - name: Check TUI locale pack parity |
| 496 | if: github.event_name != 'schedule' |
| 497 | run: python3 scripts/check-tui-locale-parity.py |
| 498 | - name: Check TUI product vocabulary |
| 499 | if: github.event_name != 'schedule' |
| 500 | run: sh scripts/check-tui-product-vocabulary.sh |
| 501 | - name: Check website locale dictionary parity |
| 502 | if: github.event_name != 'schedule' |
| 503 | run: node web/scripts/check-locales.mjs |
| 504 | - name: Skip Rust lint for light change |
| 505 | if: needs.changes.outputs.heavy != 'true' |
| 506 | run: echo "No executable Rust changes detected; preserving required Lint context." |
| 507 | - name: Linux clippy location |
| 508 | if: needs.changes.outputs.heavy == 'true' |
| 509 | run: echo "Linux clippy/test gates run on CNB for mirrored fix/*, rebrand/*, work/v*, and main branches." |
| 510 | |
| 511 | workflow-rlm-cache: |
| 512 | name: Workflow RLM cache |
| 513 | needs: changes |
| 514 | if: needs.changes.outputs.workflow == 'true' |
| 515 | timeout-minutes: 30 |
| 516 | runs-on: ubuntu-latest |
| 517 | steps: |
| 518 | - uses: actions/checkout@v7 |
| 519 | - uses: dtolnay/rust-toolchain@stable |
| 520 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 521 | id: sccache |
| 522 | continue-on-error: true |
| 523 | - name: Enable sccache |
| 524 | if: steps.sccache.outcome == 'success' |
| 525 | shell: bash |
| 526 | run: | |
| 527 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 528 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 529 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 530 | - uses: Swatinem/rust-cache@v2 |
| 531 | with: |
| 532 | cache-bin: false |
| 533 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 534 | - name: Run workflow crate tests |
| 535 | run: sh scripts/with-hermetic-test-home.sh cargo test -p codewhale-workflow --locked |
| 536 | |
| 537 | test: |
| 538 | name: Test |
| 539 | needs: changes |
| 540 | # Required contexts "Test (ubuntu-latest)" / "Test (macos-latest)" / |
| 541 | # "Test (windows-latest)" derive from job name + matrix.os and are |
| 542 | # independent of runs-on. For light changes the macOS/Windows legs only |
| 543 | # echo a skip line, so run them on ubuntu instead of queueing for scarce |
| 544 | # macOS/Windows runners. Heavy pull requests run the Linux lane directly; |
| 545 | # non-PR release/main pushes use CNB for Linux. |
| 546 | # The ternary is safe: matrix.os is always a non-empty literal, so |
| 547 | # runs-on can never evaluate to empty. |
| 548 | timeout-minutes: 90 |
| 549 | # macOS legs go to the self-hosted Mac ONLY when all three hold: the |
| 550 | # change is heavy, the event is trusted (not a fork PR), and the |
| 551 | # CW_SELF_HOSTED_MAC repo variable is 'true'. That variable is the kill |
| 552 | # switch: unset it and every leg falls back to GitHub-hosted runners |
| 553 | # immediately, with no commit — important because an offline |
| 554 | # self-hosted runner queues jobs forever, which is worse than a slow one. |
| 555 | runs-on: ${{ needs.changes.outputs.heavy != 'true' && 'ubuntu-latest' || (matrix.os == 'macos-latest' && needs.changes.outputs.trusted == 'true' && vars.CW_SELF_HOSTED_MAC == 'true' && fromJSON('["self-hosted","macOS","ARM64","codewhale-mac"]')) || matrix.os }} |
| 556 | strategy: |
| 557 | # A failure on one desktop platform must not erase evidence from the |
| 558 | # other one. We need both conclusions to diagnose and release safely. |
| 559 | fail-fast: false |
| 560 | matrix: |
| 561 | # Linux workspace tests run directly for pull requests. CNB remains |
| 562 | # the Linux lane for non-PR release/main pushes. |
| 563 | os: [ubuntu-latest, macos-latest, windows-latest] |
| 564 | steps: |
| 565 | - name: Skip tests for light change |
| 566 | if: needs.changes.outputs.heavy != 'true' |
| 567 | run: echo "No executable Rust changes detected; preserving required Test context." |
| 568 | - uses: actions/checkout@v7 |
| 569 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 570 | - name: Test Windows installer PATH helper |
| 571 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest' |
| 572 | shell: pwsh |
| 573 | run: ./scripts/installer/update-user-path.tests.ps1 |
| 574 | - name: Install NSIS for Windows installer regression |
| 575 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest' |
| 576 | shell: pwsh |
| 577 | # Bounded retry, not a weaker check (#5403). Every observed failure here |
| 578 | # was Chocolatey's feed, not the code: a 504 from the V2 API, and |
| 579 | # "package was not found with the source(s) listed". A single attempt |
| 580 | # made `Test (windows-latest)` — a required check on every PR — report |
| 581 | # on community.chocolatey.org's availability instead of on the tree. |
| 582 | # NSIS must still install for the regression below to run; this only |
| 583 | # survives a transient outage. |
| 584 | run: | |
| 585 | $ErrorActionPreference = 'Continue' |
| 586 | $delays = @(0, 20, 45) |
| 587 | for ($attempt = 0; $attempt -lt $delays.Count; $attempt++) { |
| 588 | if ($delays[$attempt] -gt 0) { |
| 589 | Write-Host "NSIS install attempt $($attempt + 1) after $($delays[$attempt])s backoff" |
| 590 | Start-Sleep -Seconds $delays[$attempt] |
| 591 | } |
| 592 | choco install nsis -y --no-progress |
| 593 | if ($LASTEXITCODE -eq 0) { |
| 594 | Write-Host "NSIS installed on attempt $($attempt + 1)" |
| 595 | exit 0 |
| 596 | } |
| 597 | Write-Host "::warning::choco install nsis failed (exit $LASTEXITCODE)" |
| 598 | } |
| 599 | Write-Host "::error::NSIS could not be provisioned from Chocolatey after $($delays.Count) attempts" |
| 600 | exit 1 |
| 601 | - name: Test Windows installer PATH regression |
| 602 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'windows-latest' |
| 603 | shell: pwsh |
| 604 | run: ./scripts/installer/installer-path-regression.tests.ps1 -AllowUserPathMutation |
| 605 | - uses: dtolnay/rust-toolchain@stable |
| 606 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 607 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 608 | id: sccache |
| 609 | continue-on-error: true |
| 610 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 611 | - name: Enable sccache |
| 612 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' |
| 613 | shell: bash |
| 614 | run: | |
| 615 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 616 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 617 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 618 | - name: Install Linux system dependencies |
| 619 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 620 | run: | |
| 621 | for i in 1 2 3 4 5; do |
| 622 | sudo apt-get update && break |
| 623 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 624 | sleep 15 |
| 625 | done |
| 626 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 627 | - uses: Swatinem/rust-cache@v2 |
| 628 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 629 | with: |
| 630 | cache-bin: false |
| 631 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 632 | - uses: taiki-e/install-action@nextest |
| 633 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 634 | - name: Run tests |
| 635 | # Same test binaries as `cargo test`, run by cargo-nextest: one |
| 636 | # process per test, all runner cores busy, slow tests named instead |
| 637 | # of stalling the binary. `.config/nextest.toml` serializes the PTY |
| 638 | # binary and bounds the integration binary that spawns the real |
| 639 | # executable; retries are off, so a flake is a red run, not a hidden |
| 640 | # one. nextest does not run doctests — the next step keeps them. |
| 641 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 642 | shell: bash |
| 643 | run: sh scripts/with-hermetic-test-home.sh cargo nextest run --workspace --all-features --locked --profile ci |
| 644 | env: |
| 645 | # sccache 0.17 panics resolving its config directory under the |
| 646 | # isolated Windows home before Cargo can compile or run any test. |
| 647 | # Bypass only that optional cache; keep the full suite and isolation. |
| 648 | RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }} |
| 649 | # Give test threads the stack the product gives itself. main.rs runs |
| 650 | # the owner thread and every tokio worker at |
| 651 | # CODEWHALE_MAIN_STACK_BYTES (32 MiB) because the engine and |
| 652 | # runtime-thread futures are genuinely deep. `#[tokio::test]` builds |
| 653 | # its own runtime and never sees that, so tests ran the same code on |
| 654 | # ~2 MiB (~1 MiB on Windows) — a configuration that never ships. |
| 655 | # That gap is what aborted the whole Windows test binary with |
| 656 | # STATUS_STACK_OVERFLOW in start_turn_accepts_dynamic_tools_and_ |
| 657 | # environment_id, masking every other Windows result (78afd8d3d4 |
| 658 | # Box::pin'd that one frame; the mismatch itself remained). std reads |
| 659 | # this for any thread spawned without an explicit size, which covers |
| 660 | # both libtest's per-test threads and tokio's workers. Test threads |
| 661 | # hold 16 MiB: the engine-only chains they run measured a ~2.5 MiB |
| 662 | # debug high-water, while the full ~16.5 MiB UI-loop chain that |
| 663 | # forced 32 MiB lives in spawned binaries, which size their own |
| 664 | # stacks explicitly and never read this variable. |
| 665 | RUST_MIN_STACK: '16777216' |
| 666 | - name: Run doctests |
| 667 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 668 | shell: bash |
| 669 | run: sh scripts/with-hermetic-test-home.sh cargo test --workspace --all-features --locked --doc |
| 670 | env: |
| 671 | RUSTC_WRAPPER: ${{ matrix.os != 'windows-latest' && env.RUSTC_WRAPPER || '' }} |
| 672 | RUST_MIN_STACK: '16777216' |
| 673 | # The Ubuntu lint lane validates non-RSS backlog fields. Run the same |
| 674 | # source-bound measurement on macOS so loss or growth of RSS evidence |
| 675 | # fails closed instead of becoming an unsupported-field skip. |
| 676 | - name: Check persistence-backlog RSS budget |
| 677 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' |
| 678 | run: python3 scripts/check-persistence-backlog-budget.py |
| 679 | - name: Lockfile drift guard |
| 680 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 681 | run: git diff --exit-code -- Cargo.lock |
| 682 | - name: Run Offline Eval Harness |
| 683 | # The eval harness is OS-independent prompt/composition checking; |
| 684 | # running it once (on the faster macOS leg, warm from the test build) |
| 685 | # instead of once per desktop OS keeps the coverage while taking |
| 686 | # ~2min off the Windows critical path. |
| 687 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' |
| 688 | run: cargo run -p codewhale-tui --all-features -- eval |
| 689 | - name: sccache stats |
| 690 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' |
| 691 | continue-on-error: true |
| 692 | shell: bash |
| 693 | run: sccache --show-stats |
| 694 | - name: Linux test location (CNB) |
| 695 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' |
| 696 | run: echo "Linux workspace tests run on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu." |
| 697 | |
| 698 | npm-wrapper-smoke: |
| 699 | name: npm wrapper smoke |
| 700 | needs: changes |
| 701 | if: github.event_name != 'schedule' |
| 702 | # Same ternary rationale as the Test job: light legs only echo, so keep |
| 703 | # them off macOS/Windows runners. On pull_request the matrix is |
| 704 | # ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)" |
| 705 | # context is unaffected. Heavy pull requests execute the Ubuntu smoke |
| 706 | # here; their branches may not be mirrored to CNB. |
| 707 | # A cold Windows build can take 29 minutes before the smoke even starts. |
| 708 | # Leave room for installation; bound the smoke itself independently below. |
| 709 | timeout-minutes: 45 |
| 710 | runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }} |
| 711 | strategy: |
| 712 | matrix: |
| 713 | os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} |
| 714 | steps: |
| 715 | - name: Skip npm wrapper smoke for light change |
| 716 | if: needs.changes.outputs.heavy != 'true' |
| 717 | run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context." |
| 718 | - uses: actions/checkout@v7 |
| 719 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 720 | - uses: dtolnay/rust-toolchain@stable |
| 721 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 722 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 723 | id: sccache |
| 724 | continue-on-error: true |
| 725 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 726 | - name: Enable sccache |
| 727 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' |
| 728 | shell: bash |
| 729 | run: | |
| 730 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 731 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 732 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 733 | - uses: actions/setup-node@v7 |
| 734 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 735 | with: |
| 736 | node-version: 20 |
| 737 | - name: Install Linux system dependencies |
| 738 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 739 | run: | |
| 740 | for i in 1 2 3 4 5; do |
| 741 | sudo apt-get update && break |
| 742 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 743 | sleep 15 |
| 744 | done |
| 745 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 746 | - uses: Swatinem/rust-cache@v2 |
| 747 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 748 | with: |
| 749 | cache-bin: false |
| 750 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 751 | - name: Build wrapper binaries |
| 752 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 753 | # The smoke validates wrapper install/delegation plumbing, not |
| 754 | # codegen quality, so skip fat LTO + codegen-units=1 for a much |
| 755 | # cheaper release build. Shipped binaries keep the real profile via |
| 756 | # the Release workflow. |
| 757 | env: |
| 758 | CARGO_PROFILE_RELEASE_LTO: 'off' |
| 759 | CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16' |
| 760 | run: cargo build --release --locked -p codewhale-cli -p codewhale-tui |
| 761 | - name: Smoke wrapper install and delegated entrypoints |
| 762 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') |
| 763 | timeout-minutes: 5 |
| 764 | run: node scripts/release/npm-wrapper-smoke.js |
| 765 | - name: sccache stats |
| 766 | if: needs.changes.outputs.heavy == 'true' && (matrix.os != 'ubuntu-latest' || github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request') && steps.sccache.outcome == 'success' |
| 767 | continue-on-error: true |
| 768 | shell: bash |
| 769 | run: sccache --show-stats |
| 770 | - name: Linux smoke location |
| 771 | if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' |
| 772 | run: echo "Linux npm wrapper smoke runs on CNB for non-PR release/main pushes; pull requests run directly on Ubuntu." |
| 773 | |
| 774 | mobile-smoke: |
| 775 | name: Mobile runtime smoke |
| 776 | needs: changes |
| 777 | # Not a required PR context. Pull requests run it only when the mobile |
| 778 | # runtime surface changed (see the `mobile` filter above); every push to |
| 779 | # main runs it unconditionally as the pre-release safety net. |
| 780 | if: >- |
| 781 | github.event_name != 'schedule' && |
| 782 | needs.changes.outputs.heavy == 'true' && |
| 783 | (github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true') |
| 784 | timeout-minutes: 30 |
| 785 | runs-on: ubuntu-latest |
| 786 | steps: |
| 787 | - uses: actions/checkout@v7 |
| 788 | - uses: dtolnay/rust-toolchain@stable |
| 789 | - uses: mozilla-actions/sccache-action@v0.0.11 |
| 790 | id: sccache |
| 791 | continue-on-error: true |
| 792 | - name: Enable sccache |
| 793 | if: steps.sccache.outcome == 'success' |
| 794 | shell: bash |
| 795 | run: | |
| 796 | echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" |
| 797 | echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" |
| 798 | echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" |
| 799 | - name: Install Linux system dependencies |
| 800 | run: | |
| 801 | for i in 1 2 3 4 5; do |
| 802 | sudo apt-get update && break |
| 803 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 804 | sleep 15 |
| 805 | done |
| 806 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 807 | - uses: Swatinem/rust-cache@v2 |
| 808 | with: |
| 809 | cache-bin: false |
| 810 | save-if: ${{ github.ref == 'refs/heads/main' }} |
| 811 | - name: Run mobile smoke tests |
| 812 | # The smoke exercises HTTP/SSE runtime behaviour, not codegen |
| 813 | # quality; skipping fat LTO + codegen-units=1 cuts the in-script |
| 814 | # release build from ~12min to a fraction of that. |
| 815 | env: |
| 816 | CARGO_PROFILE_RELEASE_LTO: 'off' |
| 817 | CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16' |
| 818 | run: ./scripts/mobile-smoke.sh |
| 819 | - name: sccache stats |
| 820 | if: steps.sccache.outcome == 'success' |
| 821 | continue-on-error: true |
| 822 | shell: bash |
| 823 | run: sccache --show-stats |
| 824 | |
| 825 | actionlint: |
| 826 | name: Workflow lint |
| 827 | needs: changes |
| 828 | if: needs.changes.outputs.actions == 'true' |
| 829 | timeout-minutes: 15 |
| 830 | runs-on: ubuntu-latest |
| 831 | steps: |
| 832 | - uses: actions/checkout@v7 |
| 833 | - name: Run actionlint |
| 834 | uses: docker://rhysd/actionlint:1.7.12 |
| 835 | with: |
| 836 | # SC2129 (grouped redirects) is style-only and endemic to the |
| 837 | # existing GITHUB_ENV/GITHUB_OUTPUT append pattern; SC2221/SC2222 |
| 838 | # flag the long-standing `*.md` glob shadowing the PR-template |
| 839 | # entry in change detection, which is intentional. |
| 840 | args: -color -ignore SC2129 -ignore SC2221 -ignore SC2222 |
| 841 | |
| 842 | # Check documentation builds without warnings |
| 843 | docs: |
| 844 | name: Documentation |
| 845 | if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' |
| 846 | timeout-minutes: 60 |
| 847 | runs-on: ubuntu-latest |
| 848 | steps: |
| 849 | - uses: actions/checkout@v7 |
| 850 | - uses: dtolnay/rust-toolchain@stable |
| 851 | - name: Install Linux system dependencies |
| 852 | if: runner.os == 'Linux' |
| 853 | run: | |
| 854 | for i in 1 2 3 4 5; do |
| 855 | sudo apt-get update && break |
| 856 | echo "apt-get update failed (attempt $i); retrying in 15s" |
| 857 | sleep 15 |
| 858 | done |
| 859 | sudo apt-get install -y libdbus-1-dev pkg-config |
| 860 | - uses: Swatinem/rust-cache@v2 |
| 861 | with: |
| 862 | cache-bin: false |
| 863 | - name: Build docs |
| 864 | run: cargo doc --workspace --no-deps |
| 865 | env: |
| 866 | RUSTDOCFLAGS: -Dwarnings |
| 867 |