CI #1
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| on: | |
| push: | |
| branches: [master, main] | |
| pull_request: | |
| branches: [master, main] | |
| schedule: | |
| - cron: '31 6 * * 1' | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: ci-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| env: | |
| CARGO_TERM_COLOR: always | |
| CARGO_INCREMENTAL: 0 | |
| RUSTFLAGS: -Dwarnings | |
| jobs: | |
| changes: | |
| name: Change detection | |
| runs-on: ubuntu-latest | |
| outputs: | |
| heavy: ${{ steps.detect.outputs.heavy }} | |
| workflow: ${{ steps.detect.outputs.workflow }} | |
| mobile: ${{ steps.detect.outputs.mobile }} | |
| actions: ${{ steps.detect.outputs.actions }} | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect executable changes | |
| id: detect | |
| shell: bash | |
| env: | |
| EVENT_NAME: ${{ github.event_name }} | |
| BASE_REF: ${{ github.base_ref }} | |
| BEFORE_SHA: ${{ github.event.before }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${EVENT_NAME}" == "schedule" || "${EVENT_NAME}" == "workflow_dispatch" ]]; then | |
| echo "heavy=true" >> "${GITHUB_OUTPUT}" | |
| echo "workflow=true" >> "${GITHUB_OUTPUT}" | |
| echo "mobile=true" >> "${GITHUB_OUTPUT}" | |
| echo "actions=true" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| base="" | |
| if [[ "${EVENT_NAME}" == "pull_request" && -n "${BASE_REF}" ]]; then | |
| git fetch --no-tags origin "${BASE_REF}:refs/remotes/origin/${BASE_REF}" --depth=1 | |
| base="origin/${BASE_REF}" | |
| elif [[ -n "${BEFORE_SHA}" && "${BEFORE_SHA}" != "0000000000000000000000000000000000000000" ]]; then | |
| base="${BEFORE_SHA}" | |
| fi | |
| if [[ -z "${base}" ]]; then | |
| echo "heavy=true" >> "${GITHUB_OUTPUT}" | |
| echo "workflow=true" >> "${GITHUB_OUTPUT}" | |
| echo "mobile=true" >> "${GITHUB_OUTPUT}" | |
| echo "actions=true" >> "${GITHUB_OUTPUT}" | |
| exit 0 | |
| fi | |
| mapfile -t changed < <(git diff --name-only "${base}" "${GITHUB_SHA}" | sort) | |
| heavy=false | |
| workflow=false | |
| mobile=false | |
| actions=false | |
| for path in "${changed[@]}"; do | |
| # Heavy classification. ORDER MATTERS: must-stay-heavy inputs are | |
| # matched BEFORE any light entry so a script that only a | |
| # heavy-gated job exercises can never be misclassified as light. | |
| # Anything unrecognized falls through to the default-heavy `*)` | |
| # arm (fail-safe default-heavy). Light-classified scripts below | |
| # are either never run by CI (v0867-setup-qa.sh) or exercised by | |
| # ALWAYS-on jobs/steps that run regardless of `heavy` | |
| # (check-versions.sh / check-ohos-deps.sh via Version drift, | |
| # check-coauthor-trailers.py via Lint), so no coverage is lost. | |
| case "${path}" in | |
| scripts/release/npm-wrapper-smoke.js|scripts/mobile-smoke.sh|scripts/check-provider-registry.py) | |
| heavy=true | |
| ;; | |
| 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/v0867-setup-qa.sh|scripts/release/check-versions.sh|scripts/release/check-ohos-deps.sh|scripts/release/prepare-release.sh|scripts/release/prepare-release.test.sh|scripts/check-coauthor-trailers.py) | |
| ;; | |
| *) | |
| heavy=true | |
| ;; | |
| esac | |
| case "${path}" in | |
| crates/workflow/*|workflows/rlm_cache_change.star|.github/workflows/ci.yml) | |
| workflow=true | |
| ;; | |
| esac | |
| # Mobile runtime surface: the `codewhale-tui serve --mobile` | |
| # HTTP/SSE stack that scripts/mobile-smoke.sh exercises. Pull | |
| # requests run the smoke only when one of these changes; every | |
| # push to main still runs it unconditionally as the pre-release | |
| # safety net for anything this filter misses. | |
| case "${path}" in | |
| 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) | |
| mobile=true | |
| ;; | |
| esac | |
| case "${path}" in | |
| .github/workflows/*|.github/actionlint.yml) | |
| actions=true | |
| ;; | |
| esac | |
| done | |
| echo "heavy=${heavy}" >> "${GITHUB_OUTPUT}" | |
| echo "workflow=${workflow}" >> "${GITHUB_OUTPUT}" | |
| echo "mobile=${mobile}" >> "${GITHUB_OUTPUT}" | |
| echo "actions=${actions}" >> "${GITHUB_OUTPUT}" | |
| echo "Heavy Rust CI required: ${heavy}" | |
| echo "Workflow RLM cache CI required: ${workflow}" | |
| echo "Mobile runtime smoke required (PRs): ${mobile}" | |
| echo "Workflow lint required: ${actions}" | |
| printf 'Changed files:\n' | |
| printf ' %s\n' "${changed[@]}" | |
| versions: | |
| name: Version drift | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: actions/setup-node@v6 | |
| with: | |
| node-version: 20 | |
| - name: Check version drift | |
| run: ./scripts/release/check-versions.sh | |
| - name: Check OHOS dependency graph | |
| run: ./scripts/release/check-ohos-deps.sh | |
| - name: Check release helper contracts | |
| run: | | |
| bash .github/scripts/agent-task-metadata.test.sh | |
| bash scripts/release/generate-release-body.test.sh | |
| bash scripts/release/prepare-release.test.sh | |
| bash scripts/release/require-release-tag-checkout.test.sh | |
| bash scripts/release/verify-remote-tag.test.sh | |
| bash .github/scripts/update-homebrew-tap.test.sh | |
| lint: | |
| name: Lint | |
| needs: changes | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| fetch-depth: 0 | |
| - uses: dtolnay/rust-toolchain@master | |
| if: needs.changes.outputs.heavy == 'true' | |
| with: | |
| toolchain: stable | |
| components: rustfmt, clippy | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| id: sccache | |
| # Cache bootstrap failures (e.g. GitHub 504s fetching the sccache | |
| # binary) degrade to an uncached build instead of failing product CI. | |
| continue-on-error: true | |
| if: needs.changes.outputs.heavy == 'true' | |
| - name: Enable sccache | |
| if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success' | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" | |
| echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" | |
| echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" | |
| - name: Install Linux system dependencies | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: | | |
| for i in 1 2 3 4 5; do | |
| sudo apt-get update && break | |
| echo "apt-get update failed (attempt $i); retrying in 15s" | |
| sleep 15 | |
| done | |
| sudo apt-get install -y libdbus-1-dev pkg-config | |
| - uses: Swatinem/rust-cache@v2 | |
| if: needs.changes.outputs.heavy == 'true' | |
| with: | |
| cache-bin: false | |
| # PRs restore the cache seeded by main but skip the expensive | |
| # post-job save; sccache covers PR-specific compilation deltas. | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Check formatting | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: cargo fmt --all -- --check | |
| - name: Run clippy | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: | | |
| cargo clippy --workspace --all-features --locked -- \ | |
| -D warnings \ | |
| -A clippy::uninlined_format_args \ | |
| -A clippy::too_many_arguments \ | |
| -A clippy::unnecessary_map_or \ | |
| -A clippy::collapsible_if \ | |
| -A clippy::assertions_on_constants | |
| - name: sccache stats | |
| if: needs.changes.outputs.heavy == 'true' && steps.sccache.outcome == 'success' | |
| continue-on-error: true | |
| shell: bash | |
| run: sccache --show-stats | |
| - name: Check provider registry drift | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: python3 scripts/check-provider-registry.py | |
| - name: Check README translations stay in sync | |
| if: github.event_name != 'schedule' | |
| run: python3 scripts/check-readme-translations.py | |
| - name: Check harvested contributor credit | |
| if: github.event_name != 'schedule' | |
| shell: bash | |
| run: | | |
| if [[ "${{ github.event_name }}" == "pull_request" ]]; then | |
| git fetch --no-tags origin "${{ github.base_ref }}" | |
| RANGE="origin/${{ github.base_ref }}..HEAD" | |
| elif [[ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]]; then | |
| RANGE="${{ github.event.before }}..${{ github.sha }}" | |
| else | |
| RANGE="HEAD~1..HEAD" | |
| fi | |
| python3 scripts/check-coauthor-trailers.py \ | |
| --author-map .github/AUTHOR_MAP \ | |
| --range "$RANGE" \ | |
| --check-authors | |
| - name: Skip Rust lint for light change | |
| if: needs.changes.outputs.heavy != 'true' | |
| run: echo "No executable Rust changes detected; preserving required Lint context." | |
| - name: Linux clippy location | |
| if: needs.changes.outputs.heavy == 'true' | |
| run: echo "Linux clippy/test gates run on CNB for mirrored fix/*, rebrand/*, work/v*, and main branches." | |
| workflow-rlm-cache: | |
| name: Workflow RLM cache | |
| needs: changes | |
| if: needs.changes.outputs.workflow == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| id: sccache | |
| continue-on-error: true | |
| - name: Enable sccache | |
| if: steps.sccache.outcome == 'success' | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" | |
| echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" | |
| echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-bin: false | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Run RLM cache workflow mock/replay tests | |
| run: cargo test -p codewhale-workflow --locked rlm_cache_change | |
| test: | |
| name: Test | |
| needs: changes | |
| # Required contexts "Test (ubuntu-latest)" / "Test (macos-latest)" / | |
| # "Test (windows-latest)" derive from job name + matrix.os and are | |
| # independent of runs-on. For light changes the macOS/Windows legs only | |
| # echo a skip line, so run them on ubuntu instead of queueing for scarce | |
| # macOS/Windows runners. Heavy changes use the real matrix OS as before. | |
| # The ternary is safe: matrix.os is always a non-empty literal, so | |
| # runs-on can never evaluate to empty. | |
| runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }} | |
| strategy: | |
| # A failure on one desktop platform must not erase evidence from the | |
| # other one. We need both conclusions to diagnose and release safely. | |
| fail-fast: false | |
| matrix: | |
| # Linux workspace tests moved to CNB; GitHub keeps the platform | |
| # coverage CNB cannot provide. | |
| os: [ubuntu-latest, macos-latest, windows-latest] | |
| steps: | |
| - name: Skip tests for light change | |
| if: needs.changes.outputs.heavy != 'true' | |
| run: echo "No executable Rust changes detected; preserving required Test context." | |
| - uses: actions/checkout@v7 | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - uses: dtolnay/rust-toolchain@stable | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| id: sccache | |
| continue-on-error: true | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - name: Enable sccache | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' && steps.sccache.outcome == 'success' | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" | |
| echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" | |
| echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" | |
| - uses: Swatinem/rust-cache@v2 | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| with: | |
| cache-bin: false | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Run tests | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| run: cargo test --workspace --all-features --locked | |
| - name: Lockfile drift guard | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| run: git diff --exit-code -- Cargo.lock | |
| - name: Run Offline Eval Harness | |
| # The eval harness is OS-independent prompt/composition checking; | |
| # running it once (on the faster macOS leg, warm from the test build) | |
| # instead of once per desktop OS keeps the coverage while taking | |
| # ~2min off the Windows critical path. | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os == 'macos-latest' | |
| run: cargo run -p codewhale-tui --all-features -- eval | |
| - name: sccache stats | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' && steps.sccache.outcome == 'success' | |
| continue-on-error: true | |
| shell: bash | |
| run: sccache --show-stats | |
| - name: Linux test location | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' | |
| run: echo "Linux workspace tests run on CNB for mirrored first-party branches." | |
| npm-wrapper-smoke: | |
| name: npm wrapper smoke | |
| needs: changes | |
| if: github.event_name != 'schedule' | |
| # Same ternary rationale as the Test job: light legs only echo, so keep | |
| # them off macOS/Windows runners. On pull_request the matrix is | |
| # ubuntu-only, so the required "npm wrapper smoke (ubuntu-latest)" | |
| # context is unaffected. | |
| runs-on: ${{ needs.changes.outputs.heavy == 'true' && matrix.os || 'ubuntu-latest' }} | |
| strategy: | |
| matrix: | |
| os: ${{ fromJSON(github.event_name == 'pull_request' && '["ubuntu-latest"]' || '["ubuntu-latest","macos-latest","windows-latest"]') }} | |
| steps: | |
| - name: Skip npm wrapper smoke for light change | |
| if: needs.changes.outputs.heavy != 'true' | |
| run: echo "No executable Rust changes detected; preserving required npm wrapper smoke context." | |
| - uses: actions/checkout@v7 | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - uses: dtolnay/rust-toolchain@stable | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| id: sccache | |
| continue-on-error: true | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| - name: Enable sccache | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' && steps.sccache.outcome == 'success' | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" | |
| echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" | |
| echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" | |
| - uses: actions/setup-node@v6 | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| with: | |
| node-version: 20 | |
| - uses: Swatinem/rust-cache@v2 | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| with: | |
| cache-bin: false | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Build wrapper binaries | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| # The smoke validates wrapper install/delegation plumbing, not | |
| # codegen quality, so skip fat LTO + codegen-units=1 for a much | |
| # cheaper release build. Shipped binaries keep the real profile via | |
| # the Release workflow. | |
| env: | |
| CARGO_PROFILE_RELEASE_LTO: 'off' | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16' | |
| run: cargo build --release --locked -p codewhale-cli -p codewhale-tui | |
| - name: Smoke wrapper install and delegated entrypoints | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' | |
| run: node scripts/release/npm-wrapper-smoke.js | |
| - name: sccache stats | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os != 'ubuntu-latest' && steps.sccache.outcome == 'success' | |
| continue-on-error: true | |
| shell: bash | |
| run: sccache --show-stats | |
| - name: Linux smoke location | |
| if: needs.changes.outputs.heavy == 'true' && matrix.os == 'ubuntu-latest' | |
| run: echo "Linux npm wrapper smoke runs on CNB for mirrored first-party branches." | |
| mobile-smoke: | |
| name: Mobile runtime smoke | |
| needs: changes | |
| # Not a required PR context. Pull requests run it only when the mobile | |
| # runtime surface changed (see the `mobile` filter above); every push to | |
| # main runs it unconditionally as the pre-release safety net. | |
| if: >- | |
| github.event_name != 'schedule' && | |
| needs.changes.outputs.heavy == 'true' && | |
| (github.event_name != 'pull_request' || needs.changes.outputs.mobile == 'true') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - uses: mozilla-actions/sccache-action@v0.0.10 | |
| id: sccache | |
| continue-on-error: true | |
| - name: Enable sccache | |
| if: steps.sccache.outcome == 'success' | |
| shell: bash | |
| run: | | |
| echo "SCCACHE_GHA_ENABLED=true" >> "${GITHUB_ENV}" | |
| echo "RUSTC_WRAPPER=sccache" >> "${GITHUB_ENV}" | |
| echo "SCCACHE_IGNORE_SERVER_IO_ERROR=1" >> "${GITHUB_ENV}" | |
| - name: Install Linux system dependencies | |
| run: | | |
| for i in 1 2 3 4 5; do | |
| sudo apt-get update && break | |
| echo "apt-get update failed (attempt $i); retrying in 15s" | |
| sleep 15 | |
| done | |
| sudo apt-get install -y libdbus-1-dev pkg-config | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-bin: false | |
| save-if: ${{ github.ref == 'refs/heads/main' }} | |
| - name: Run mobile smoke tests | |
| # The smoke exercises HTTP/SSE runtime behaviour, not codegen | |
| # quality; skipping fat LTO + codegen-units=1 cuts the in-script | |
| # release build from ~12min to a fraction of that. | |
| env: | |
| CARGO_PROFILE_RELEASE_LTO: 'off' | |
| CARGO_PROFILE_RELEASE_CODEGEN_UNITS: '16' | |
| run: ./scripts/mobile-smoke.sh | |
| - name: sccache stats | |
| if: steps.sccache.outcome == 'success' | |
| continue-on-error: true | |
| shell: bash | |
| run: sccache --show-stats | |
| actionlint: | |
| name: Workflow lint | |
| needs: changes | |
| if: needs.changes.outputs.actions == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Run actionlint | |
| uses: docker://rhysd/actionlint:1.7.7 | |
| with: | |
| # SC2129 (grouped redirects) is style-only and endemic to the | |
| # existing GITHUB_ENV/GITHUB_OUTPUT append pattern; SC2221/SC2222 | |
| # flag the long-standing `*.md` glob shadowing the PR-template | |
| # entry in change detection, which is intentional. | |
| args: -color -ignore SC2129 -ignore SC2221 -ignore SC2222 | |
| # Check documentation builds without warnings | |
| docs: | |
| name: Documentation | |
| if: github.event_name == 'schedule' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: dtolnay/rust-toolchain@stable | |
| - name: Install Linux system dependencies | |
| if: runner.os == 'Linux' | |
| run: | | |
| for i in 1 2 3 4 5; do | |
| sudo apt-get update && break | |
| echo "apt-get update failed (attempt $i); retrying in 15s" | |
| sleep 15 | |
| done | |
| sudo apt-get install -y libdbus-1-dev pkg-config | |
| - uses: Swatinem/rust-cache@v2 | |
| with: | |
| cache-bin: false | |
| - name: Build docs | |
| run: cargo doc --workspace --no-deps | |
| env: | |
| RUSTDOCFLAGS: -Dwarnings |