From 8408f8cbc31ec3f2d5edea7622943ade413766c9 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:40:22 +0530 Subject: [PATCH 1/2] ci: split ci.yml into reusable per-suite workflows --- .github/workflows/ci-node.yml | 52 ++++++ .github/workflows/ci-python.yml | 101 ++++++++++++ .github/workflows/ci-rust.yml | 127 +++++++++++++++ .github/workflows/ci.yml | 273 +++----------------------------- 4 files changed, 303 insertions(+), 250 deletions(-) create mode 100644 .github/workflows/ci-node.yml create mode 100644 .github/workflows/ci-python.yml create mode 100644 .github/workflows/ci-rust.yml diff --git a/.github/workflows/ci-node.yml b/.github/workflows/ci-node.yml new file mode 100644 index 00000000..2ad48fb0 --- /dev/null +++ b/.github/workflows/ci-node.yml @@ -0,0 +1,52 @@ +name: CI Node + +# Reusable workflow: build the napi addon + TypeScript + dashboard SPA, then +# typecheck, lint, and test the Node SDK. Called by ci.yml when Node-relevant +# paths change. +on: + workflow_call: + +permissions: + contents: read + +env: + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_RETRY: "10" + +jobs: + node-test: + name: Node SDK Tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Rust + uses: ./.github/actions/setup-rust + + - name: Set up Node + pnpm + uses: ./.github/actions/setup-node + + - name: Install dashboard dependencies + working-directory: dashboard + run: pnpm install --frozen-lockfile + + - name: Build addon, TypeScript, and dashboard SPA + working-directory: sdks/node + run: | + pnpm run build:native + pnpm run build:ts + pnpm -C ../../dashboard exec tsr generate # route tree is gitignored + pnpm run build:dashboard + + - name: Typecheck + working-directory: sdks/node + run: pnpm typecheck + + - name: Lint + working-directory: sdks/node + run: pnpm lint + + - name: Run Node test suite + working-directory: sdks/node + run: pnpm test diff --git a/.github/workflows/ci-python.yml b/.github/workflows/ci-python.yml new file mode 100644 index 00000000..0209a985 --- /dev/null +++ b/.github/workflows/ci-python.yml @@ -0,0 +1,101 @@ +name: CI Python + +# Reusable workflow: Python lint (Ruff + mypy) and the cross-platform pytest +# matrix. Called by ci.yml when Python-relevant paths change. +on: + workflow_call: + +permissions: + contents: read + +env: + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_RETRY: "10" + +jobs: + lint: + name: Lint (Python) + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uv + uses: ./.github/actions/setup-python + + - name: Lint Python with Ruff + working-directory: sdks/python + run: uv run ruff check taskito/ tests/ + + - name: Check Python formatting with Ruff + working-directory: sdks/python + run: uv run ruff format --check taskito/ tests/ + + - name: Type-check Python with mypy + working-directory: sdks/python + run: uv run mypy taskito/ tests/ --no-incremental + + test: + name: Python Tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) + needs: lint + runs-on: ${{ matrix.os }} + env: + # setup-python on macOS upgrades certifi via pip; pip's HTTP cache + # occasionally fails to deserialize across runner image versions and + # surfaces as a noisy "Cache entry deserialization failed" annotation. + # Disabling pip's cache eliminates the warning at zero cost (we use uv + # for actual package installs). + PIP_NO_CACHE_DIR: "1" + strategy: + matrix: + os: [ubuntu-latest] + python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + include: + - os: macos-15 + python-version: "3.14" + - os: macos-15 + python-version: "3.10" + - os: windows-2025 + python-version: "3.14" + - os: windows-2025 + python-version: "3.10" + steps: + - name: Point vendored OpenSSL at Strawberry Perl (Windows) + if: runner.os == 'Windows' + shell: bash + # The postgres feature pulls openssl-sys (vendored), whose Configure + # runs perl. Under `shell: bash` the incomplete MSYS perl shadows the + # runner's Strawberry Perl and lacks modules OpenSSL needs + # (Locale::Maketext::Simple), so pin openssl-src to Strawberry Perl. + # Must precede the maturin build that `uv sync` triggers below. + run: echo "OPENSSL_SRC_PERL=C:/Strawberry/perl/bin/perl.exe" >> "$GITHUB_ENV" + + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uv + uses: ./.github/actions/setup-python + with: + python-version: ${{ matrix.python-version }} + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + # Only the non-Linux runners save the cache here; the Linux Cargo + # cache is written by the Rust lint job. + save-if: ${{ matrix.os != 'ubuntu-latest' }} + + - name: Build native extension with maturin + uses: PyO3/maturin-action@v1.51.0 + with: + command: develop + args: --release --features extension-module,postgres,redis,native-async,workflows,mesh + maturin-version: v1.13.3 + working-directory: sdks/python + + - name: Run Python test suite + working-directory: sdks/python + # The pytest_unconfigure hook in tests/conftest.py calls + # ``os._exit(0)`` on a clean run to bypass CPython finalization and + # avoid the PyO3 daemon-thread SIGABRT we used to paper over here. + run: uv run python -m pytest tests/ -v --junitxml=test-results.xml diff --git a/.github/workflows/ci-rust.yml b/.github/workflows/ci-rust.yml new file mode 100644 index 00000000..25b8501c --- /dev/null +++ b/.github/workflows/ci-rust.yml @@ -0,0 +1,127 @@ +name: CI Rust + +# Reusable workflow: Rust lint + the SQLite/PostgreSQL/Redis test matrices. +# Called by ci.yml when Rust-relevant paths change. See ci.yml for the env +# rationale (Cargo HTTP/1.1 + retry budget) — reusable workflows do not inherit +# the caller's env, so it is repeated here. +on: + workflow_call: + +permissions: + contents: read + +env: + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_RETRY: "10" + +jobs: + lint: + name: Lint (Rust) + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Python + uv + uses: ./.github/actions/setup-python + + - name: Set up Rust + uses: ./.github/actions/setup-rust + with: + components: rustfmt, clippy + save-if: "true" # the Rust lint job is the sole Cargo cache writer + + - name: Check Rust formatting + run: cargo fmt --all --check + + - name: Run Clippy + run: cargo clippy --all-targets --all-features -- -D warnings + + - name: Generic crates stay binding-free + # The engine crates (core/workflows/mesh) must remain binding-agnostic so + # the Python, Node, and Java shells can all reuse them. A pyo3, napi, OR + # jni dep would let binding-specific code creep back in. cargo tree is + # authoritative — you cannot `use pyo3`/`use napi`/`use jni` without + # depending on it. See crates/taskito-core/BINDING_CONTRACT.md. + run: | + set -euo pipefail + for crate in taskito-core taskito-workflows taskito-mesh; do + # Capture first so a `cargo tree` failure aborts (set -e) instead of + # being swallowed by the pipe and silently passing the check. + tree_output="$(cargo tree -p "$crate" -e normal --all-features)" + for binding in pyo3 napi jni; do + if grep -qi "$binding" <<<"$tree_output"; then + echo "::error::$binding is in the dependency tree of $crate — generic crates must stay binding-agnostic" + exit 1 + fi + done + done + echo "Generic crates are free of pyo3, napi, and jni." + + rust-test: + name: Rust Tests (SQLite) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/rust-prelude + + # $pythonLocation is exported to the job env by setup-python at runtime. + - name: Run Rust test suite + run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace + + - name: Run mesh crate tests + run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test -p taskito-mesh + + - name: Check build with native-async features + run: cargo check --workspace --features native-async + + - name: Check build with mesh feature + run: cargo check --workspace --features mesh + + rust-test-postgres: + name: Rust Tests (PostgreSQL) + runs-on: ubuntu-latest + services: + postgres: + image: postgres:16-alpine + env: + POSTGRES_USER: postgres + POSTGRES_PASSWORD: test + POSTGRES_DB: taskito_test + ports: + - 5432:5432 + options: >- + --health-cmd "pg_isready -U postgres" + --health-interval 5s + --health-timeout 5s + --health-retries 10 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/rust-prelude + + - name: Run Rust test suite (PostgreSQL backend) + run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace --features postgres,workflows + env: + TASKITO_POSTGRES_TEST_URL: postgres://postgres:test@localhost:5432/taskito_test + + rust-test-redis: + name: Rust Tests (Redis) + runs-on: ubuntu-latest + services: + redis: + image: redis:7-alpine + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 5s + --health-timeout 3s + --health-retries 10 + steps: + - uses: actions/checkout@v7 + - uses: ./.github/actions/rust-prelude + + - name: Run Rust test suite (Redis backend) + run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace --features redis,workflows + env: + TASKITO_REDIS_TEST_URL: redis://localhost:6379/15 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4965553d..4032c1f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,23 +21,13 @@ permissions: contents: read pull-requests: read -# Cargo's HTTP downloader intermittently aborts crate downloads on CI -# ("Error in the HTTP2 framing layer" / "unexpected eof while reading") when -# fetching uncached crates from crates.io. Force HTTP/1.1 and raise the retry -# budget so a transient download error retries instead of failing the job. -# Set at workflow level so it also covers the maturin build that `uv sync` -# triggers inside the setup-python action, before setup-rust runs. -env: - CARGO_HTTP_MULTIPLEXING: "false" - CARGO_NET_RETRY: "10" - jobs: changes: name: Detect changed paths runs-on: ubuntu-latest outputs: # Run policy folded in: true on push/dispatch (run everything) or when a - # suite's paths changed on a PR. Jobs gate on these directly. + # suite's paths changed on a PR. The per-suite jobs gate on these. rust: ${{ steps.decide.outputs.rust }} python: ${{ steps.decide.outputs.python }} node: ${{ steps.decide.outputs.node }} @@ -49,29 +39,28 @@ jobs: id: filter uses: dorny/paths-filter@v4 with: + # Each suite reruns when its own sources, the shared Rust core, the + # composite actions, or its workflow (orchestrator + reusable) change. filters: | - rust: + shared: &shared - 'crates/**' - 'Cargo.toml' - 'Cargo.lock' - - 'rust-toolchain.toml' - '.github/actions/**' - '.github/workflows/ci.yml' + rust: + - *shared + - 'rust-toolchain.toml' + - '.github/workflows/ci-rust.yml' python: + - *shared - 'sdks/python/**' - - 'crates/**' - - 'Cargo.toml' - - 'Cargo.lock' - - '.github/actions/**' - - '.github/workflows/ci.yml' + - '.github/workflows/ci-python.yml' node: + - *shared - 'sdks/node/**' - - 'crates/**' - - 'Cargo.toml' - - 'Cargo.lock' - 'dashboard/**' - - '.github/actions/**' - - '.github/workflows/ci.yml' + - '.github/workflows/ci-node.yml' - name: Decide which suites run id: decide @@ -94,244 +83,28 @@ jobs: decide python "$PYTHON" decide node "$NODE" - lint: - name: Lint & Static Analysis - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v7 - - - name: Set up Python + uv - uses: ./.github/actions/setup-python - - - name: Set up Rust - uses: ./.github/actions/setup-rust - with: - components: rustfmt, clippy - save-if: "true" # the lint job is the sole Cargo cache writer - - - name: Check Rust formatting - run: cargo fmt --all --check - - - name: Run Clippy - run: cargo clippy --all-targets --all-features -- -D warnings - - - name: Generic crates stay binding-free - # The engine crates (core/workflows/mesh) must remain binding-agnostic so - # the Python, Node, and Java shells can all reuse them. A pyo3, napi, OR - # jni dep would let binding-specific code creep back in. cargo tree is - # authoritative — you cannot `use pyo3`/`use napi`/`use jni` without - # depending on it. See crates/taskito-core/BINDING_CONTRACT.md. - run: | - set -euo pipefail - for crate in taskito-core taskito-workflows taskito-mesh; do - # Capture first so a `cargo tree` failure aborts (set -e) instead of - # being swallowed by the pipe and silently passing the check. - tree_output="$(cargo tree -p "$crate" -e normal --all-features)" - for binding in pyo3 napi jni; do - if grep -qi "$binding" <<<"$tree_output"; then - echo "::error::$binding is in the dependency tree of $crate — generic crates must stay binding-agnostic" - exit 1 - fi - done - done - echo "Generic crates are free of pyo3, napi, and jni." - - - name: Lint Python with Ruff - working-directory: sdks/python - run: uv run ruff check taskito/ tests/ - - - name: Check Python formatting with Ruff - working-directory: sdks/python - run: uv run ruff format --check taskito/ tests/ - - - name: Type-check Python with mypy - working-directory: sdks/python - run: uv run mypy taskito/ tests/ --no-incremental - - rust-test: - name: Rust Tests (SQLite) + rust: + name: Rust needs: changes if: needs.changes.outputs.rust == 'true' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v7 - - uses: ./.github/actions/rust-prelude - - # $pythonLocation is exported to the job env by setup-python at runtime. - - name: Run Rust test suite - run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace - - - name: Run mesh crate tests - run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test -p taskito-mesh + uses: ./.github/workflows/ci-rust.yml - - name: Check build with native-async features - run: cargo check --workspace --features native-async - - - name: Check build with mesh feature - run: cargo check --workspace --features mesh - - rust-test-postgres: - name: Rust Tests (PostgreSQL) + python: + name: Python needs: changes - if: needs.changes.outputs.rust == 'true' - runs-on: ubuntu-latest - services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: test - POSTGRES_DB: taskito_test - ports: - - 5432:5432 - options: >- - --health-cmd "pg_isready -U postgres" - --health-interval 5s - --health-timeout 5s - --health-retries 10 - steps: - - uses: actions/checkout@v7 - - uses: ./.github/actions/rust-prelude - - - name: Run Rust test suite (PostgreSQL backend) - run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace --features postgres,workflows - env: - TASKITO_POSTGRES_TEST_URL: postgres://postgres:test@localhost:5432/taskito_test - - rust-test-redis: - name: Rust Tests (Redis) - needs: changes - if: needs.changes.outputs.rust == 'true' - runs-on: ubuntu-latest - services: - redis: - image: redis:7-alpine - ports: - - 6379:6379 - options: >- - --health-cmd "redis-cli ping" - --health-interval 5s - --health-timeout 3s - --health-retries 10 - steps: - - uses: actions/checkout@v7 - - uses: ./.github/actions/rust-prelude - - - name: Run Rust test suite (Redis backend) - run: LD_LIBRARY_PATH="$pythonLocation/lib" cargo test --workspace --features redis,workflows - env: - TASKITO_REDIS_TEST_URL: redis://localhost:6379/15 + if: needs.changes.outputs.python == 'true' + uses: ./.github/workflows/ci-python.yml - node-test: - name: Node SDK Tests + node: + name: Node needs: changes if: needs.changes.outputs.node == 'true' - runs-on: ubuntu-latest - steps: - - name: Check out repository - uses: actions/checkout@v7 - - - name: Set up Rust - uses: ./.github/actions/setup-rust - - - name: Set up Node + pnpm - uses: ./.github/actions/setup-node - - - name: Install dashboard dependencies - working-directory: dashboard - run: pnpm install --frozen-lockfile - - - name: Build addon, TypeScript, and dashboard SPA - working-directory: sdks/node - run: | - pnpm run build:native - pnpm run build:ts - pnpm -C ../../dashboard exec tsr generate # route tree is gitignored - pnpm run build:dashboard - - - name: Typecheck - working-directory: sdks/node - run: pnpm typecheck - - - name: Lint - working-directory: sdks/node - run: pnpm lint - - - name: Run Node test suite - working-directory: sdks/node - run: pnpm test - - test: - name: Python Tests (${{ matrix.os }} / Python ${{ matrix.python-version }}) - needs: [lint, changes] - if: needs.changes.outputs.python == 'true' - runs-on: ${{ matrix.os }} - env: - # setup-python on macOS upgrades certifi via pip; pip's HTTP cache - # occasionally fails to deserialize across runner image versions and - # surfaces as a noisy "Cache entry deserialization failed" annotation. - # Disabling pip's cache eliminates the warning at zero cost (we use uv - # for actual package installs). - PIP_NO_CACHE_DIR: "1" - strategy: - matrix: - os: [ubuntu-latest] - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] - include: - - os: macos-15 - python-version: "3.14" - - os: macos-15 - python-version: "3.10" - - os: windows-2025 - python-version: "3.14" - - os: windows-2025 - python-version: "3.10" - steps: - - name: Point vendored OpenSSL at Strawberry Perl (Windows) - if: runner.os == 'Windows' - shell: bash - # The postgres feature pulls openssl-sys (vendored), whose Configure - # runs perl. Under `shell: bash` the incomplete MSYS perl shadows the - # runner's Strawberry Perl and lacks modules OpenSSL needs - # (Locale::Maketext::Simple), so pin openssl-src to Strawberry Perl. - # Must precede the maturin build that `uv sync` triggers below. - run: echo "OPENSSL_SRC_PERL=C:/Strawberry/perl/bin/perl.exe" >> "$GITHUB_ENV" - - - name: Check out repository - uses: actions/checkout@v7 - - - name: Set up Python + uv - uses: ./.github/actions/setup-python - with: - python-version: ${{ matrix.python-version }} - - - name: Set up Rust - uses: ./.github/actions/setup-rust - with: - # Only the non-Linux runners save the cache here; the Linux Cargo - # cache is written by the lint job. - save-if: ${{ matrix.os != 'ubuntu-latest' }} - - - name: Build native extension with maturin - uses: PyO3/maturin-action@v1.51.0 - with: - command: develop - args: --release --features extension-module,postgres,redis,native-async,workflows,mesh - maturin-version: v1.13.3 - working-directory: sdks/python - - - name: Run Python test suite - working-directory: sdks/python - # The pytest_unconfigure hook in tests/conftest.py calls - # ``os._exit(0)`` on a clean run to bypass CPython finalization and - # avoid the PyO3 daemon-thread SIGABRT we used to paper over here. - run: uv run python -m pytest tests/ -v --junitxml=test-results.xml + uses: ./.github/workflows/ci-node.yml ci-status: name: CI status if: always() - needs: [changes, lint, rust-test, rust-test-postgres, rust-test-redis, node-test, test] + needs: [changes, rust, python, node] runs-on: ubuntu-latest steps: - name: Check that no required job failed From bd0dd2142482b36401d886795820dd1ff8c5f632 Mon Sep 17 00:00:00 2001 From: Pratyush Sharma <56130065+pratyush618@users.noreply.github.com> Date: Fri, 26 Jun 2026 07:40:22 +0530 Subject: [PATCH 2/2] ci(java): add Java SDK test job to CI --- .github/actions/setup-java/action.yml | 22 ++++++++++++++++ .github/workflows/ci-java.yml | 36 +++++++++++++++++++++++++++ .github/workflows/ci.yml | 15 ++++++++++- 3 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 .github/actions/setup-java/action.yml create mode 100644 .github/workflows/ci-java.yml diff --git a/.github/actions/setup-java/action.yml b/.github/actions/setup-java/action.yml new file mode 100644 index 00000000..5e172d29 --- /dev/null +++ b/.github/actions/setup-java/action.yml @@ -0,0 +1,22 @@ +name: Set up Java +description: >- + Installs a Temurin JDK and enables Gradle build caching. Used by every + Java-touching job so toolchain setup lives in one place. The SDK targets Java + 11 bytecode via `--release 11`, so any JDK >= 11 works; we use 21 to match the + publish workflow. + +inputs: + java-version: + description: "Temurin JDK major version to install." + required: false + default: "21" + +runs: + using: composite + steps: + - name: Install JDK + Gradle cache + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: ${{ inputs.java-version }} + cache: gradle diff --git a/.github/workflows/ci-java.yml b/.github/workflows/ci-java.yml new file mode 100644 index 00000000..0a728753 --- /dev/null +++ b/.github/workflows/ci-java.yml @@ -0,0 +1,36 @@ +name: CI Java + +# Reusable workflow: build the JNI cdylib (via Gradle's cargoBuild) and run the +# Java SDK's full `gradle build` — compile, Spotless + Checkstyle, and the JUnit +# suite across the runtime, processor, and test-support subprojects. Called by +# ci.yml when Java-relevant paths change. +on: + workflow_call: + +permissions: + contents: read + +env: + CARGO_HTTP_MULTIPLEXING: "false" + CARGO_NET_RETRY: "10" + +jobs: + java-test: + name: Java SDK Tests + runs-on: ubuntu-latest + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Rust + uses: ./.github/actions/setup-rust + + - name: Set up Java + Gradle + uses: ./.github/actions/setup-java + + # `gradle build` runs cargoBuild (the JNI cdylib), staging the native + # library into the jar, then compile + Spotless + Checkstyle + tests for + # every subproject. --no-daemon keeps the runner clean. + - name: Build and test the Java SDK + working-directory: sdks/java + run: ./gradlew build --no-daemon diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4032c1f3..abd23052 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: rust: ${{ steps.decide.outputs.rust }} python: ${{ steps.decide.outputs.python }} node: ${{ steps.decide.outputs.node }} + java: ${{ steps.decide.outputs.java }} steps: - name: Check out repository uses: actions/checkout@v7 @@ -61,6 +62,10 @@ jobs: - 'sdks/node/**' - 'dashboard/**' - '.github/workflows/ci-node.yml' + java: + - *shared + - 'sdks/java/**' + - '.github/workflows/ci-java.yml' - name: Decide which suites run id: decide @@ -71,6 +76,7 @@ jobs: RUST: ${{ steps.filter.outputs.rust }} PYTHON: ${{ steps.filter.outputs.python }} NODE: ${{ steps.filter.outputs.node }} + JAVA: ${{ steps.filter.outputs.java }} run: | decide() { if [ "$FORCE" = "true" ] || [ "$2" = "true" ]; then @@ -82,6 +88,7 @@ jobs: decide rust "$RUST" decide python "$PYTHON" decide node "$NODE" + decide java "$JAVA" rust: name: Rust @@ -101,10 +108,16 @@ jobs: if: needs.changes.outputs.node == 'true' uses: ./.github/workflows/ci-node.yml + java: + name: Java + needs: changes + if: needs.changes.outputs.java == 'true' + uses: ./.github/workflows/ci-java.yml + ci-status: name: CI status if: always() - needs: [changes, rust, python, node] + needs: [changes, rust, python, node, java] runs-on: ubuntu-latest steps: - name: Check that no required job failed