build: adopt cargo-anvil check catalog (github backend) #3916
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: main | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| on: | |
| pull_request: | |
| branches: [main] | |
| push: | |
| branches: [main] | |
| jobs: | |
| # Emits per-tier `--exclude NAME ...` args (modified/affected/required) to scope | |
| # downstream `cargo --workspace` jobs. PRs snapshot base vs. the merge commit | |
| # (same SHA downstream jobs build); pushes emit empty (full backstop). | |
| # `skip=true` when no crates are impacted (e.g. doc-only PR). | |
| delta: | |
| runs-on: ubuntu-latest | |
| outputs: | |
| exclude_not_modified: ${{ steps.compute.outputs.exclude_not_modified }} | |
| exclude_not_affected: ${{ steps.compute.outputs.exclude_not_affected }} | |
| exclude_not_required: ${{ steps.compute.outputs.exclude_not_required }} | |
| skip: ${{ steps.compute.outputs.skip }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup | |
| if: github.event_name == 'pull_request' | |
| uses: ./.github/actions/setup | |
| with: | |
| rust-toolchain: RUST_LATEST | |
| cargo-tools: CARGO_DELTA_VERSION | |
| - name: Compute Impact | |
| id: compute | |
| shell: bash | |
| env: | |
| BASE_REF: ${{ github.base_ref }} | |
| run: | | |
| set -euo pipefail | |
| if [[ "${{ github.event_name }}" != "pull_request" ]]; then | |
| # push to main (or any non-PR trigger) -> full backstop: no excludes. | |
| { | |
| echo "exclude_not_modified=" | |
| echo "exclude_not_affected=" | |
| echo "exclude_not_required=" | |
| echo "skip=false" | |
| } >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # Snapshots written outside the working tree so `git checkout` can't trip on them. | |
| baseline="$RUNNER_TEMP/delta-baseline.json" | |
| current="$RUNNER_TEMP/delta-current.json" | |
| head_sha=$(git rev-parse HEAD) | |
| git fetch --no-tags origin "+refs/heads/${BASE_REF}:refs/remotes/origin/${BASE_REF}" | |
| git checkout "origin/$BASE_REF" | |
| cargo delta -c .delta.toml snapshot > "$baseline" | |
| git checkout "$head_sha" | |
| cargo delta -c .delta.toml snapshot > "$current" | |
| # `cargo-excludes` emits the workspace complement of the selected tier as | |
| # `--exclude NAME ...`, scoped via downstream `cargo --workspace`. `--exclude` | |
| # only matches workspace members, avoiding `-p` ambiguity with same-named | |
| # registry deps. | |
| impact() { | |
| cargo delta -c .delta.toml impact --baseline "$baseline" --current "$current" "$@" | |
| } | |
| exclude_not_modified=$(impact -f cargo-excludes --modified) | |
| exclude_not_affected=$(impact -f cargo-excludes --affected) | |
| exclude_not_required=$(impact -f cargo-excludes --required) | |
| # `required` is the superset (required ⊇ affected ⊇ modified). If it | |
| # has no impacted crates, nothing is impacted and downstream | |
| # source-scoped jobs can be skipped entirely. | |
| if [[ -z "$(impact -f names --required)" ]]; then | |
| skip=true | |
| else | |
| skip=false | |
| fi | |
| { | |
| echo "exclude_not_modified=$exclude_not_modified" | |
| echo "exclude_not_affected=$exclude_not_affected" | |
| echo "exclude_not_required=$exclude_not_required" | |
| echo "skip=$skip" | |
| } | tee -a "$GITHUB_OUTPUT" | |
| testing: | |
| needs: [delta] | |
| if: needs.delta.outputs.skip != 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, ubuntu-24.04-arm, windows-11-arm] | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_MSRV, RUST_NIGHTLY | |
| cargo-tools: CARGO_NEXTEST_VERSION, CARGO_HACK_VERSION, JUST_VERSION | |
| # execute | |
| - name: Build | |
| run: cargo hack build --each-feature --workspace ${{ needs.delta.outputs.exclude_not_affected }} --verbose --locked --color always | |
| - name: Tests | |
| if: success() || failure() | |
| run: cargo nextest run --verbose --workspace ${{ needs.delta.outputs.exclude_not_affected }} --all-features | |
| - name: Docs | |
| if: success() || failure() | |
| env: | |
| RUSTDOCFLAGS: "--cfg docsrs -D warnings" | |
| run: cargo +${{ env.RUST_NIGHTLY }} doc --no-deps --verbose --workspace ${{ needs.delta.outputs.exclude_not_required }} --all-features | |
| - name: Doc Tests | |
| if: success() || failure() | |
| run: cargo test --doc --verbose --workspace ${{ needs.delta.outputs.exclude_not_affected }} --all-features | |
| - name: Doc Tests (default features) | |
| if: success() || failure() | |
| run: cargo test --doc --verbose --workspace ${{ needs.delta.outputs.exclude_not_affected }} | |
| - name: Examples | |
| if: success() || failure() | |
| run: cargo +${{ env.RUST_NIGHTLY }} -Zscript ./scripts/run-examples.rs --cargo-profile dev ${{ needs.delta.outputs.exclude_not_affected }} | |
| static-analysis: | |
| needs: [delta] | |
| if: needs.delta.outputs.skip != 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest, ubuntu-24.04-arm, windows-11-arm] | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_LATEST | |
| rust-components: clippy | |
| cargo-tools: CARGO_DOC2README_VERSION, CARGO_WORKSPACES_VERSION, CARGO_ENSURE_NO_DEFAULT_FEATURES_VERSION, CARGO_ENSURE_NO_CYCLIC_DEPS_VERSION, CARGO_DENY_VERSION, CARGO_SORT_VERSION | |
| # execute | |
| - name: Clippy | |
| run: cargo clippy --workspace ${{ needs.delta.outputs.exclude_not_modified }} --all-targets --all-features -- -D warnings | |
| - name: Install Nightly Rustfmt | |
| if: success() || failure() | |
| run: rustup toolchain install ${{ env.RUST_NIGHTLY }} --profile minimal --component rustfmt | |
| - name: Source Format (nightly) | |
| if: success() || failure() | |
| run: cargo +${{ env.RUST_NIGHTLY }} fmt --all -- --config-path ./unstable-rustfmt.toml --check | |
| - name: Cargo.toml Format | |
| if: success() || failure() | |
| run: cargo sort --check --grouped --workspace | |
| - name: Readmes | |
| if: success() || failure() | |
| run: cargo workspaces exec --ignore-private cargo doc2readme --check --lib --template ../README.j2 | |
| env: | |
| # Disable sccache: macro expansion (rustc -Zunpretty=expanded) does not | |
| # produce the artifact files sccache expects to cache, causing it to fail. | |
| RUSTC_WRAPPER: "" | |
| - name: Default Features | |
| if: success() || failure() | |
| run: cargo ensure-no-default-features | |
| - name: Cyclic Dependencies | |
| if: success() || failure() | |
| run: cargo ensure-no-cyclic-deps | |
| - name: Dependency Validation | |
| if: success() || failure() | |
| run: cargo deny --all-features --workspace --color always check all | |
| spell-check: | |
| runs-on: ubuntu-slim | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| rust-toolchain: RUST_LATEST | |
| cargo-tools: CARGO_SPELLCHECK_VERSION, JUST_VERSION | |
| - name: Check Spelling | |
| run: just spellcheck | |
| semver: | |
| needs: [delta] | |
| if: github.event_name == 'pull_request' && needs.delta.outputs.skip != 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_LATEST | |
| cargo-tools: CARGO_SEMVER_CHECKS_VERSION | |
| # execute | |
| - name: Semver Compatibility | |
| id: semver | |
| continue-on-error: true | |
| run: | | |
| set -o pipefail | |
| cargo semver-checks --all-features --workspace ${{ needs.delta.outputs.exclude_not_affected }} --color never --baseline-rev origin/${{ github.base_ref }} 2>&1 | tee semver-output.txt | |
| # report | |
| - name: Prepare Semver Comment | |
| if: steps.semver.outcome == 'failure' | |
| run: | | |
| echo "## ⚠️ Breaking Changes Detected" > semver-comment.txt | |
| echo "" >> semver-comment.txt | |
| echo '```' >> semver-comment.txt | |
| grep -v "^ *Cloning " semver-output.txt | grep -v "^ *Building " | grep -v "^ *Built \[" | grep -v "^ *Parsing " | grep -v "^ *Parsed \[" | grep -v "^ *Checking" | grep -v "^ *Checked \[" | grep -v "^ *Finished \[" | grep -v "^ *Summary " >> semver-comment.txt | |
| echo '```' >> semver-comment.txt | |
| echo "" >> semver-comment.txt | |
| echo "If the breaking changes are intentional then everything is fine - this message is merely informative." >> semver-comment.txt | |
| echo "" >> semver-comment.txt | |
| echo "Remember to apply a version number bump with the correct severity when publishing a version with breaking changes (\`1.x.x -> 2.x.x\` or \`0.1.x -> 0.2.x\`)." >> semver-comment.txt | |
| - name: Post Semver Failure Comment | |
| if: steps.semver.outcome == 'failure' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: marocchino/sticky-pull-request-comment@v3.0.4 | |
| with: | |
| header: semver-check | |
| path: semver-comment.txt | |
| - name: Remove Semver Comment on Success | |
| if: steps.semver.outcome == 'success' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: marocchino/sticky-pull-request-comment@v3.0.4 | |
| with: | |
| header: semver-check | |
| delete: true | |
| extended-analysis: | |
| name: extended-analysis (${{ matrix.os }}) | |
| if: github.event_name == 'pull_request' && needs.delta.outputs.skip != 'true' | |
| needs: [delta, testing, static-analysis] | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: ubuntu-latest | |
| target: x86_64-unknown-linux-gnu | |
| - os: windows-latest | |
| target: x86_64-pc-windows-msvc | |
| - os: ubuntu-24.04-arm | |
| target: aarch64-unknown-linux-gnu | |
| - os: windows-11-arm | |
| target: aarch64-pc-windows-msvc | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_NIGHTLY | |
| rust-components: miri | |
| cargo-tools: CARGO_UDEPS_VERSION, CARGO_CAREFUL_VERSION, CARGO_NEXTEST_VERSION | |
| # execute | |
| - name: Udeps | |
| shell: pwsh | |
| run: | | |
| $ErrorActionPreference = "Stop" | |
| $PSNativeCommandUseErrorActionPreference = $true | |
| # Without --all-targets: enforce that every [dependencies] entry is used | |
| # by the library/bins (catches normal deps used only by tests/benches/ | |
| # examples -- i.e. misplaced deps that should be [dev-dependencies]). | |
| cargo +${{ env.RUST_NIGHTLY }} udeps --locked --all-features --workspace ${{ needs.delta.outputs.exclude_not_modified }} --color always | |
| # With --all-targets: enforce that every [dev-dependencies] entry is used | |
| # by some target (catches unused dev-dependencies). | |
| cargo +${{ env.RUST_NIGHTLY }} udeps --locked --all-features --all-targets --workspace ${{ needs.delta.outputs.exclude_not_modified }} --color always | |
| - name: Careful | |
| if: success() || failure() | |
| run: cargo +${{ env.RUST_NIGHTLY }} careful nextest run --all-features --workspace ${{ needs.delta.outputs.exclude_not_affected }} --color always --target ${{ matrix.target }} | |
| - name: Miri (stacked borrows) | |
| if: success() || failure() | |
| run: cargo +${{ env.RUST_NIGHTLY }} miri test --all-features --workspace ${{ needs.delta.outputs.exclude_not_affected }} --lib --tests | |
| model-checking: | |
| if: github.event_name == 'pull_request' && needs.delta.outputs.skip != 'true' | |
| needs: [delta, testing] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 30 | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_LATEST | |
| cargo-tools: JUST_VERSION | |
| # execute | |
| - name: Loom | |
| run: just loom | |
| fuzz-testing: | |
| if: github.event_name == 'pull_request' && needs.delta.outputs.skip != 'true' | |
| needs: [delta, testing] | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 60 | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_NIGHTLY | |
| cargo-tools: CARGO_BOLERO_VERSION, JUST_VERSION | |
| # execute | |
| - name: Bolero | |
| run: just bolero 60s | |
| mutation-testing: | |
| if: github.event_name == 'pull_request' | |
| needs: [testing] | |
| runs-on: ubuntu-latest | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| enable-wild-linker: true | |
| rust-toolchain: RUST_NIGHTLY | |
| cargo-tools: CARGO_MUTANTS_VERSION | |
| # execute | |
| - name: Generate PR Diff | |
| run: git diff origin/main...HEAD >diff.txt | |
| - name: Mutate Diff | |
| timeout-minutes: 240 | |
| run: cargo +${{ env.RUST_NIGHTLY }} -Zscript scripts/mutants.rs --in-place --in-diff diff.txt | |
| coverage: | |
| needs: [delta] | |
| # Coverage always runs (no `delta.skip` gate) so the externally-posted | |
| # `codecov/project` status check is always reported. Codecov posts that | |
| # status in reaction to a coverage upload from this job; if `coverage` | |
| # is skipped, `codecov/project` stays in | |
| # `Expected — Waiting for status to be reported` and blocks merge. The | |
| # `required-checks` fan-in only solves stuck contexts for jobs we own, | |
| # so we eat the cost of running coverage on doc-only PRs here to keep | |
| # the externally-posted check unblocked. | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| # Note: windows-11-arm excluded due to LLVM coverage instrumentation bugs | |
| # causing "malformed instrumentation profile data: symbol name is empty" errors | |
| # Note: when changing the number of OS entries here, update | |
| # `codecov.notify.after_n_builds` in codecov.yml to match, otherwise | |
| # Codecov reports the project status before all platforms have uploaded. | |
| os: [ubuntu-latest, windows-latest, ubuntu-24.04-arm] | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_NIGHTLY | |
| rust-components: llvm-tools-preview | |
| cargo-tools: CARGO_LLVM_COV_VERSION | |
| # execute | |
| - name: Generate Coverage (all-features) | |
| run: cargo +${{ env.RUST_NIGHTLY }} llvm-cov --all-features --workspace --lcov --output-path lcov-all.info | |
| - name: Generate Coverage (no-default-features) | |
| run: cargo +${{ env.RUST_NIGHTLY }} llvm-cov --no-default-features --workspace --lcov --output-path lcov-no-def.info | |
| - name: Upload Coverage to Codecov | |
| uses: codecov/codecov-action@v7.0.0 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: lcov-all.info,lcov-no-def.info | |
| fail_ci_if_error: true | |
| pr-title: | |
| runs-on: ubuntu-slim | |
| if: github.event_name == 'pull_request' | |
| steps: | |
| - name: Check PR Title | |
| uses: amannn/action-semantic-pull-request@v6.1.1 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| types: | | |
| build | |
| chore | |
| ci | |
| doc | |
| docs | |
| feat | |
| fix | |
| misc | |
| miscellaneous | |
| perf | |
| refactor | |
| style | |
| task | |
| test | |
| license-headers: | |
| runs-on: ubuntu-slim | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| with: | |
| fetch-depth: 1 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| rust-toolchain: RUST_LATEST | |
| cargo-tools: CARGO_HEATHER_VERSION | |
| - name: Check License Headers | |
| run: cargo heather | |
| external-type-exposure: | |
| needs: [delta] | |
| if: needs.delta.outputs.skip != 'true' | |
| # Runs `cargo check-external-types --all-features` sequentially over every | |
| # workspace crate, which exceeds the 15-minute `ubuntu-slim` execution cap as | |
| # the crate/dependency graph grows. Use a standard runner with an explicit | |
| # timeout so the job has headroom. | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 25 | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| uses: ./.github/actions/setup | |
| with: | |
| enable-sccache: true | |
| rust-toolchain: RUST_NIGHTLY_EXTERNAL_TYPES | |
| cargo-tools: CARGO_CHECK_EXTERNAL_TYPES_VERSION | |
| # execute | |
| - name: Check External Type Exposure | |
| run: cargo +${{ env.RUST_NIGHTLY_EXTERNAL_TYPES }} -Zscript scripts/check-external-types.rs ${{ env.RUST_NIGHTLY_EXTERNAL_TYPES }} | |
| script-tests: | |
| # Pester test suite for the release-related PowerShell scripts | |
| # (release-packages.ps1, lib/releasing.ps1, lib/release-flow.ps1). | |
| # Always runs (no delta gate) because script-level changes don't move any | |
| # Rust crates so delta would mark the PR skippable, but the scripts | |
| # themselves may still need validation. Cross-platform matrix mirrors the | |
| # platforms developers run the scripts on locally. | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| os: [ubuntu-latest, windows-latest] | |
| steps: | |
| # prep | |
| - name: Checkout | |
| uses: actions/checkout@v7.0.0 | |
| - name: Setup | |
| # The synthetic-workspace fixture builder shells out to `cargo metadata`, | |
| # so Rust must be available even though no Rust code is compiled. | |
| uses: ./.github/actions/setup | |
| with: | |
| rust-toolchain: RUST_LATEST | |
| - name: Install Pester | |
| shell: pwsh | |
| run: | | |
| $existing = Get-Module Pester -ListAvailable | | |
| Sort-Object Version -Descending | | |
| Select-Object -First 1 | |
| if ($null -eq $existing -or $existing.Version -lt [version]'5.7.0') { | |
| Install-Module -Name Pester -MinimumVersion 5.7.1 -Force -Scope CurrentUser -SkipPublisherCheck | |
| } else { | |
| Write-Host "Pester $($existing.Version) already installed; skipping." | |
| } | |
| # execute | |
| - name: Run Pester Suite | |
| shell: pwsh | |
| run: ./scripts/tests/Pester/Run-Tests.ps1 | |
| # Single aggregating ("fan-in") status check. Branch protection should | |
| # require ONLY this context for the workflow jobs below: it succeeds when | |
| # every dependency either succeeded or was skipped, and fails if any | |
| # dependency failed or was cancelled. This avoids the GitHub Actions | |
| # quirk where a matrix job gated by a job-level `if:` posts only a single | |
| # skipped check under the bare job name, leaving matrix-expanded required | |
| # contexts (`testing (ubuntu-latest)` etc.) stuck on | |
| # `Expected — Waiting for status to be reported`. | |
| # | |
| # Maintenance — when to add a job to `needs:` below: | |
| # | |
| # * Do NOT add jobs that are intentionally optional / advisory; this | |
| # fan-in is only for jobs that must pass before merge. | |
| # | |
| # * MUST add if the job has BOTH a `strategy.matrix` AND a job-level | |
| # `if:` that can evaluate to false (e.g. gated on | |
| # `needs.delta.outputs.skip`, `github.event_name`, etc.). Such jobs | |
| # trigger the stuck-context bug above and can only be made required | |
| # safely via this fan-in. | |
| # | |
| # * SHOULD add any other job that must pass before merge. Funnelling | |
| # every required job through this single context keeps branch | |
| # protection simple: one workflow context to require here, plus | |
| # externally-posted statuses (`license/cla`, `codecov/project`). | |
| # | |
| # When a new job is added below, also remove any direct entry for it | |
| # from the branch protection ruleset (only `required-checks` should be | |
| # listed for jobs defined in this workflow). | |
| required-checks: | |
| name: required-checks | |
| if: always() | |
| needs: | |
| - delta | |
| - testing | |
| - static-analysis | |
| - spell-check | |
| - semver | |
| - extended-analysis | |
| - model-checking | |
| - fuzz-testing | |
| - mutation-testing | |
| - coverage | |
| - pr-title | |
| - license-headers | |
| - external-type-exposure | |
| - script-tests | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate Dependency Results | |
| shell: bash | |
| env: | |
| NEEDS_JSON: ${{ toJSON(needs) }} | |
| run: | | |
| set -euo pipefail | |
| echo "$NEEDS_JSON" | jq -r 'to_entries[] | " \(.key): \(.value.result)"' | |
| # Pass when every dependency is success or skipped; fail otherwise | |
| # (failure, cancelled, or any unexpected state). | |
| bad=$(echo "$NEEDS_JSON" | jq -r ' | |
| to_entries | |
| | map(select(.value.result != "success" and .value.result != "skipped")) | |
| | .[] | "\(.key): \(.value.result)" | |
| ') | |
| if [[ -n "$bad" ]]; then | |
| printf '::error::One or more required jobs did not succeed or skip:\n%s\n' "$bad" | |
| exit 1 | |
| fi | |
| echo "All required jobs succeeded or were skipped." |