馃懛 ci: gate coverage on lines nothing ran (#2162) #3857
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: [main] | |
| pull_request: | |
| workflow_dispatch: | |
| inputs: | |
| nightly: | |
| description: Run nightly checks | |
| default: false | |
| type: boolean | |
| permissions: | |
| actions: read | |
| contents: read | |
| # A push to main keeps its run: cancelling it whenever the next merge lands is what left main without a | |
| # verdict and without a cache write for every job that had not finished yet. | |
| concurrency: | |
| group: ci-${{ github.event_name == 'push' && github.sha || github.ref }} | |
| cancel-in-progress: ${{ github.event_name == 'pull_request' }} | |
| jobs: | |
| # A pull request that touches only the site, Markdown, or the Read the Docs and Renovate manifests | |
| # cannot change a compiled artifact, so the Rust jobs stand down for it. The exception is a document | |
| # a crate embeds with `include_str!`; `just _embedded-docs-contract` keeps that list current. Every | |
| # push and dispatch still runs everything. | |
| changes: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| outputs: | |
| rust: ${{ steps.filter.outputs.code == 'true' || steps.filter.outputs.embedded == 'true' }} | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: dorny/paths-filter@ceb8a2b8f2d89434be7ff52d3de7ec3738c5cc9d # v4.0.3 | |
| id: filter | |
| with: | |
| predicate-quantifier: some-with-excludes | |
| filters: | | |
| code: | |
| - '**' | |
| - '!site/**' | |
| - '!**/*.md' | |
| - '!.readthedocs.yaml' | |
| - '!renovate.json' | |
| - '!LICENSE' | |
| embedded: | |
| - 'site/content/ecosystems/oci/reference/policy.md' | |
| - 'site/static/icon.svg' | |
| - 'site/static/mark.svg' | |
| source: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 45 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: just aqua:EmbarkStudios/cargo-deny | |
| - run: just lint-source | |
| - run: just lint-deps | |
| automation: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 20 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: just node prek | |
| - run: just lint-automation | |
| contracts: | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-24.04 | |
| # A cold `just snapshots` measured 5m19s: 2m36s to build peryx-ecosystem-pypi with every feature | |
| # and 112s to run its 2814 tests. | |
| timeout-minutes: 15 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| fetch-depth: 0 | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: >- | |
| just jq uv aqua:axodotdev/cargo-dist aqua:mitsuhiko/insta github:nextest-rs/nextest | |
| - run: just snapshots | |
| - run: just release-plan | |
| platform: | |
| name: platform (${{ matrix.os }}) | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [macos-26, windows-2025] | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| env: | |
| HOMEBREW_NO_REQUIRE_TAP_TRUST: "1" | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: just github:nextest-rs/nextest | |
| - run: just platform-test | |
| coverage: | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 120 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: >- | |
| just uv aqua:taiki-e/cargo-llvm-cov github:nextest-rs/nextest | |
| github:Shopify/toxiproxy | |
| - run: just coverage-native | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: coverage-native | |
| path: | | |
| .tox/coverage/native.lcov | |
| .tox/coverage/native-gaps.json | |
| if-no-files-found: error | |
| # A body reached only from the browser runs in the target `frontend` measures, so the two halves are | |
| # unioned here rather than each gating alone. Both artifacts are small, so this waits on them both | |
| # and adds a minute rather than serialising the two long jobs. | |
| coverage-lines: | |
| needs: [coverage, frontend] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 10 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: just | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-native | |
| path: .tox/coverage | |
| - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: coverage-frontend | |
| path: .tox/coverage | |
| - run: just coverage-lines | |
| frontend: | |
| needs: changes | |
| if: github.event_name != 'pull_request' || needs.changes.outputs.rust == 'true' | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 60 | |
| env: | |
| # `coverage-frontend` builds under `$CARGO_TARGET_DIR/frontend`; rooting that inside the workspace | |
| # target directory is what lets rust-cache carry its dependencies between runs. | |
| CARGO_TARGET_DIR: ${{ github.workspace }}/target | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| components: llvm-tools-preview | |
| targets: wasm32-unknown-unknown | |
| tools: >- | |
| just jq node aqua:taiki-e/cargo-llvm-cov github:nextest-rs/nextest github:leptos-rs/cargo-leptos | |
| - run: just frontend-deps | |
| - name: Install LLVM 22 | |
| run: | | |
| curl --fail --location --output "$RUNNER_TEMP/llvm.asc" https://apt.llvm.org/llvm-snapshot.gpg.key | |
| fingerprint=$(gpg --show-keys --with-colons "$RUNNER_TEMP/llvm.asc" | awk -F: '$1 == "fpr" { print $10; exit }') | |
| test "$fingerprint" = 6084F3CF814B57C1CF12EFD515CF4D18AF4F7421 | |
| sudo install -m 644 "$RUNNER_TEMP/llvm.asc" /etc/apt/keyrings/llvm.asc | |
| repository='https://apt.llvm.org/noble/ llvm-toolchain-noble-22 main' | |
| echo "deb [signed-by=/etc/apt/keyrings/llvm.asc] $repository" | sudo tee /etc/apt/sources.list.d/llvm.list | |
| # Refresh only the list this step just wrote. A plain `apt-get update` reads every list on | |
| # the runner image, so a third party serving a corrupt index fails the whole command: on | |
| # 2026-09-09 Google's Chrome repository returned a Packages.gz whose hash did not match its | |
| # Release file, exit 100, and clang was never installed even though apt.llvm.org was healthy. | |
| sudo apt-get update \ | |
| -o Dir::Etc::sourcelist=sources.list.d/llvm.list \ | |
| -o Dir::Etc::sourceparts=- \ | |
| -o APT::Get::List-Cleanup=0 | |
| sudo apt-get install --yes clang-22 lcov | |
| echo "CLANG=clang-22" >> "$GITHUB_ENV" | |
| - run: just coverage-frontend | |
| - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 | |
| if: always() | |
| with: | |
| name: coverage-frontend | |
| path: .tox/coverage/frontend-*.lcov | |
| if-no-files-found: error | |
| docs: | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 30 | |
| steps: | |
| - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 | |
| with: | |
| persist-credentials: false | |
| - uses: $/.github/actions/setup | |
| with: | |
| tools: jq just node prek zola | |
| - run: just lint-docs | |
| - run: just docs | |
| nightly: | |
| if: github.event_name == 'workflow_dispatch' && inputs.nightly | |
| uses: $/.github/workflows/nightly.yml | |
| ci-gate: | |
| if: always() | |
| needs: [automation, changes, contracts, coverage, coverage-lines, docs, frontend, platform, source] | |
| runs-on: ubuntu-24.04 | |
| timeout-minutes: 5 | |
| steps: | |
| # A job the `changes` filter stood down reports `skipped`; anything else short of success fails. | |
| - if: >- | |
| needs.automation.result != 'success' || needs.changes.result != 'success' || | |
| needs.docs.result != 'success' || needs.source.result != 'success' || | |
| !contains(fromJSON('["success", "skipped"]'), needs.contracts.result) || | |
| !contains(fromJSON('["success", "skipped"]'), needs.coverage.result) || | |
| !contains(fromJSON('["success", "skipped"]'), needs['coverage-lines'].result) || | |
| !contains(fromJSON('["success", "skipped"]'), needs.frontend.result) || | |
| !contains(fromJSON('["success", "skipped"]'), needs.platform.result) | |
| run: exit 1 |