Skip to content

#273 ci: retire changelog-refresh cron, release-plz is the single cha… #588

#273 ci: retire changelog-refresh cron, release-plz is the single cha…

#273 ci: retire changelog-refresh cron, release-plz is the single cha… #588

Workflow file for this run

name: CI
on:
push:
branches:
- main
tags:
- v[0-9]+.[0-9]+.[0-9]+*
pull_request:
branches:
- main
workflow_dispatch:
inputs:
force_publish:
description: "Force publish to crates.io even if version hasn't changed"
required: false
default: "false"
type: boolean
skip_tests:
description: "Skip all tests jobs"
required: false
default: "false"
type: boolean
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
env:
CARGO_INCREMENTAL: 0
RUSTFLAGS: -D warnings
jobs:
msrv:
name: Extract MSRV
runs-on: ubuntu-latest
outputs:
msrv: ${{ steps.parse_msrv.outputs.msrv }}
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Parse MSRV from Cargo.toml
id: parse_msrv
run: |
MSRV=$(grep -E "rust-version\s*=\s*\"" Cargo.toml | sed 's/.*"\([^"]*\)".*/\1/')
echo "msrv=$MSRV" >> "$GITHUB_OUTPUT"
check:
name: Check
needs: msrv
runs-on: ${{ matrix.os }}
timeout-minutes: 30
continue-on-error: ${{ matrix.rust == 'nightly' }}
strategy:
fail-fast: false
matrix:
rust:
- ${{ needs.msrv.outputs.msrv }}
- stable
- nightly
os:
- ubuntu-latest
- macos-latest
- windows-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: ${{ matrix.rust }}
components: rustfmt, clippy
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: ${{ matrix.rust }}-${{ matrix.os }}-check
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check with all features
run: cargo check --all-features
- name: Check with no default features
run: cargo check --no-default-features
- name: Clippy with all targets and all features
run: |
cargo clippy --all-targets --all-features -- --deny warnings
- name: Clippy with no default features
run: |
cargo clippy --no-default-features -- --deny warnings
# `pedantic` and `nursery` lints shift between clippy versions, so gate
# them to the pinned MSRV toolchain for reproducible results. A floating
# `stable`/`nightly` clippy would surface new lints and break green
# builds on unrelated PRs.
- name: Clippy pedantic and nursery (MSRV pin)
if: matrix.rust == needs.msrv.outputs.msrv
run: |
cargo clippy --all-targets --all-features -- --deny warnings -W clippy::pedantic -W clippy::nursery
cargo clippy --no-default-features -- --deny warnings -W clippy::pedantic -W clippy::nursery
fmt:
name: Format
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: nightly
components: rustfmt
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: fmt
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Check formatting
run: cargo +nightly fmt --all -- --check
docs:
name: Documentation
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
components: rust-docs
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: docs
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build documentation
run: |
RUSTDOCFLAGS="-D warnings" cargo doc --all-features
- name: Verify version references match Cargo.toml
run: ./scripts/sync-versions.sh --check
semver_checks:
name: Semver Checks
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: semver-checks
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-semver-checks
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-semver-checks
- name: Compare public API against the last crates.io release
run: cargo semver-checks check-release --verbose
book:
name: Book
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Install mdBook
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: mdbook
- name: Build book
run: mdbook build docs
public_api:
name: Public API
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- name: Setup nightly Rust toolchain for rustdoc JSON
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: nightly
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: public-api
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-public-api
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-public-api
- name: Verify public API matches docs/public-api.txt
run: |
set -euo pipefail
current=$(mktemp)
cargo public-api -sss > "$current"
if diff -u docs/public-api.txt "$current" > /tmp/papi.diff; then
echo "Public API surface matches the baseline."
exit 0
fi
{
echo "## Public API drift"
echo
echo "\`docs/public-api.txt\` no longer matches the surface that"
echo "\`cargo public-api -sss\` produces. Regenerate the baseline"
echo "with:"
echo
echo '```bash'
echo 'cargo public-api -sss > docs/public-api.txt'
echo '```'
echo
echo 'Diff:'
echo
echo '```diff'
cat /tmp/papi.diff
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
echo "::error::Public API surface changed. Run 'cargo public-api -sss > docs/public-api.txt' and commit."
exit 1
security:
name: Security
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: security
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cargo-deny
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-deny
- name: Run cargo-deny
run: cargo deny check
- name: Install cargo-audit
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-audit
- name: Run cargo-audit
run: cargo audit
- name: Install cargo-machete
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-machete
- name: Run cargo-machete
run: cargo machete
- name: Setup nightly Rust toolchain for cargo-udeps
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: nightly
- name: Install cargo-udeps
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-udeps
- name: Run cargo-udeps
run: cargo +nightly udeps --all-targets --all-features
reuse:
name: REUSE Compliance
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Check REUSE compliance
uses: fsfe/reuse-action@676e2d560c9a403aa252096d99fcab3e1132b0f5 # v6.0.0
test:
name: Test
needs: msrv
if: ${{ github.event_name != 'workflow_dispatch' || github.event.inputs.skip_tests == 'false' }}
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: ${{ needs.msrv.outputs.msrv }}
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: ${{ needs.msrv.outputs.msrv }}-test
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install nextest
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-nextest
- name: Run tests with nextest
run: cargo nextest run --all-features --profile ci
- name: Run doctests
run: cargo test --doc --all-features
wasm_tests:
name: WASM Tests
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
targets: wasm32-unknown-unknown
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: wasm-tests
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install wasm-pack
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: wasm-pack
- name: Install chromedriver
uses: nanasess/setup-chromedriver@ef5c64a93512d266b23b80bae95e46a67f30e703 # v2
- name: Install geckodriver
uses: browser-actions/setup-geckodriver@fac5b0424c584257f32cf12c5eb4ba86014c34f8 # latest
with:
token: ${{ github.token }}
- name: Run browser tests on Chrome
run: wasm-pack test --headless --chrome --test wasm
- name: Run browser tests on Firefox
run: wasm-pack test --headless --firefox --test wasm
coverage:
name: Coverage
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
components: llvm-tools-preview
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: coverage
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install tools
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: cargo-llvm-cov,cargo-nextest
- name: Generate coverage
run: |
cargo llvm-cov nextest --all-features --profile ci --lcov --output-path lcov.info
cargo llvm-cov report --html
- name: Upload coverage to Codecov
uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v6
with:
files: lcov.info
fail_ci_if_error: false
token: ${{ secrets.CODECOV_TOKEN }}
- name: Coverage summary
run: |
COVERAGE=$(cargo llvm-cov report 2>/dev/null | grep TOTAL | awk '{print $NF}' || echo "N/A")
echo "## Coverage: $COVERAGE" >> "$GITHUB_STEP_SUMMARY"
wasm-build:
name: Example WASM build
needs: msrv
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
targets: wasm32-unknown-unknown
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: example
key: wasm-example
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install trunk
uses: jetli/trunk-action@1346cc09eace4beb84e403e199a471346d4684c9 # v0.5.1
with:
version: latest
- name: Build example with trunk
run: trunk build --release
working-directory: example
- name: Enforce WASM size budget
env:
MAX_RAW_BYTES: 1572864 # 1.5 MiB
MAX_GZIP_BYTES: 389120 # 380 KiB
run: |
set -euo pipefail
wasm=$(find example/dist -maxdepth 1 -name '*.wasm' -print -quit)
if [ -z "$wasm" ]; then
echo "No .wasm in example/dist" >&2
exit 1
fi
raw=$(stat -c '%s' "$wasm")
gzipped=$(gzip -c "$wasm" | wc -c)
{
echo "## WASM size budget"
echo
printf '| Metric | Bytes | Budget |\n'
printf '|---|---:|---:|\n'
printf '| Raw | %s | %s |\n' "$raw" "$MAX_RAW_BYTES"
printf '| Gzipped | %s | %s |\n' "$gzipped" "$MAX_GZIP_BYTES"
} >> "$GITHUB_STEP_SUMMARY"
if [ "$raw" -gt "$MAX_RAW_BYTES" ]; then
echo "::error::Raw WASM ($raw) exceeds budget ($MAX_RAW_BYTES)"
exit 1
fi
if [ "$gzipped" -gt "$MAX_GZIP_BYTES" ]; then
echo "::error::Gzipped WASM ($gzipped) exceeds budget ($MAX_GZIP_BYTES)"
exit 1
fi
e2e:
name: E2E
needs: wasm-build
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
targets: wasm32-unknown-unknown
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: example
key: e2e-example
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install trunk
uses: jetli/trunk-action@1346cc09eace4beb84e403e199a471346d4684c9 # v0.5.1
with:
version: latest
- name: Build example with trunk
run: trunk build --release
working-directory: example
- name: Setup Node.js
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: 20
- name: Install Playwright dependencies
run: npm ci --no-audit --no-fund
working-directory: example/e2e
- name: Cache Playwright browsers
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ~/.cache/ms-playwright
key: playwright-${{ runner.os }}-1.60.0
- name: Install Playwright browsers
run: npx playwright install --with-deps chromium firefox
working-directory: example/e2e
- name: Run Playwright tests
run: npm test
working-directory: example/e2e
- name: Upload Playwright report on failure
if: failure()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report
path: example/e2e/playwright-report/
retention-days: 14
lighthouse:
name: Lighthouse
needs: msrv
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
targets: wasm32-unknown-unknown
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
workspaces: example
key: lighthouse-example
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install trunk
uses: jetli/trunk-action@1346cc09eace4beb84e403e199a471346d4684c9 # v0.5.1
with:
version: latest
- name: Build demo for Lighthouse
run: trunk build --release --public-url /
working-directory: example
- name: Run Lighthouse CI
uses: treosh/lighthouse-ci-action@3e7e23fb74242897f95c0ba9cabad3d0227b9b18 # v12
with:
configPath: ./.lighthouserc.json
uploadArtifacts: true
temporaryPublicStorage: true
benchmarks:
name: Benchmarks
needs: test
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- name: Load cached target directory
uses: Swatinem/rust-cache@e18b497796c12c097a38f9edb9d0641fb99eee32 # v2
with:
key: benchmarks
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Run benchmarks
run: cargo bench --no-run
actionlint:
name: Actionlint
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- name: Run actionlint
uses: reviewdog/action-actionlint@dbe5299849118fd6f099ba563d263d770955a64a # v1
changelog:
name: Changelog
runs-on: ubuntu-latest
timeout-minutes: 10
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@2c7215f132e9ebf062739d9130488b56d53c060c # master
with:
toolchain: stable
- name: Install git-cliff
uses: taiki-e/install-action@ba47c86ac325773530516bb756137ac718732518 # v2.86.5
with:
tool: git-cliff
- name: Preview Unreleased section in run summary
id: changelog
run: |
# Generate the [Unreleased] section from conventional commits and
# surface it in the workflow run summary so every push to main
# shows reviewers exactly what git-cliff sees. CHANGELOG.md itself
# is written only by release-plz inside the release PR.
{
echo "## CHANGELOG preview (Unreleased)"
echo
echo '```markdown'
git-cliff --unreleased 2>/dev/null || echo "(git-cliff produced no output)"
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
pr_title:
name: PR title
runs-on: ubuntu-latest
timeout-minutes: 5
if: github.event_name == 'pull_request'
steps:
- name: Enforce the conventional PR title format
env:
TITLE: ${{ github.event.pull_request.title }}
run: |
set -euo pipefail
pattern='^(#[0-9]+ )?(feat|fix|docs|ci|chore|refactor|test|perf|build)(\(.+\))?!?: .+'
if [[ "$TITLE" =~ $pattern ]]; then
echo "PR title OK: $TITLE"
else
echo "::error::PR title '$TITLE' must match '#<issue> <type>: <description>'" >&2
echo "::error::Allowed types: feat fix docs ci chore refactor test perf build" >&2
exit 1
fi
ci-success:
name: CI Success
runs-on: ubuntu-latest
needs: [msrv, check, fmt, docs, semver_checks, public_api, book, security, reuse, test, wasm_tests, coverage, benchmarks, wasm-build, e2e, lighthouse, changelog, actionlint, pr_title]
if: always()
steps:
- name: Check job status
run: |
echo "All CI jobs completed"
echo "Job outcomes:"
echo " MSRV: ${{ needs.msrv.result }}"
echo " Check: ${{ needs.check.result }}"
echo " Format: ${{ needs.fmt.result }}"
echo " Docs: ${{ needs.docs.result }}"
echo " Semver Checks: ${{ needs.semver_checks.result }}"
echo " Public API: ${{ needs.public_api.result }}"
echo " Book: ${{ needs.book.result }}"
echo " Security: ${{ needs.security.result }}"
echo " REUSE: ${{ needs.reuse.result }}"
echo " Test: ${{ needs.test.result }}"
echo " WASM Tests: ${{ needs.wasm_tests.result }}"
echo " Coverage: ${{ needs.coverage.result }}"
echo " Benchmarks: ${{ needs.benchmarks.result }}"
echo " WASM build: ${{ needs.wasm-build.result }}"
echo " E2E: ${{ needs.e2e.result }}"
echo " Lighthouse: ${{ needs.lighthouse.result }}"
echo " Changelog: ${{ needs.changelog.result }}"
echo " Actionlint: ${{ needs.actionlint.result }}"
echo " PR title: ${{ needs.pr_title.result }}"
if [[ "${{ needs.msrv.result }}" != "success" || "${{ needs.check.result }}" != "success" || "${{ needs.fmt.result }}" != "success" || "${{ needs.docs.result }}" != "success" || "${{ needs.semver_checks.result }}" != "success" || "${{ needs.public_api.result }}" != "success" || "${{ needs.book.result }}" != "success" || "${{ needs.security.result }}" != "success" || "${{ needs.reuse.result }}" != "success" || ("${{ needs.test.result }}" != "success" && "${{ needs.test.result }}" != "skipped") || "${{ needs.wasm_tests.result }}" != "success" || ("${{ needs.coverage.result }}" != "success" && "${{ needs.coverage.result }}" != "skipped") || ("${{ needs.benchmarks.result }}" != "success" && "${{ needs.benchmarks.result }}" != "skipped") || "${{ needs.wasm-build.result }}" != "success" || "${{ needs.e2e.result }}" != "success" || "${{ needs.lighthouse.result }}" != "success" || ("${{ needs.changelog.result }}" != "success" && "${{ needs.changelog.result }}" != "skipped") || "${{ needs.actionlint.result }}" != "success" || ("${{ needs.pr_title.result }}" != "success" && "${{ needs.pr_title.result }}" != "skipped") ]]; then
echo "❌ CI failed"
exit 1
fi
echo "✅ CI succeeded"