From e68e505e27013d3e673d8cfc867d637f9dfa0455 Mon Sep 17 00:00:00 2001 From: cunninghamcard-bit Date: Thu, 15 Oct 2026 15:34:59 +0800 Subject: [PATCH 1/2] ci: skip build and test jobs for documentation-only changes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit No job reads README, ROADMAP, the RFCs, docs/ or assets/, yet every documentation pull request and every merge of one to master ran the whole workflow: four documentation merges on 2026-10-15 queued four full runs behind each other, after the pull requests had run it once per push. A small "changes" job diffs the push or pull request against its base (the test merge commit's first parent for a pull request, the previous head for a push) and the build and test jobs run only when something outside the documentation paths changed. docs/api/providers.md counts as code: contract-tests checks it against its generator. Without a base to compare with, it counts as a code change. A workflow-level path filter would have skipped the whole run, which leaves a required check waiting forever. Instead "CI result" always runs and passes only when every job succeeded or was skipped because "changes" found no code change, as RFC-0043 §9.2 asks for documentation-only pull requests. It is the one check to require once branch protection requires any. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS --- .github/workflows/ci.yml | 97 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 26fe5a904..ddd424a23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,10 +10,53 @@ env: CARGO_TERM_COLOR: always jobs: + # Decides once whether a change can affect anything the jobs below build or + # test. A change that only touches documentation skips them; "CI result" at + # the end still runs and reports it, so a check required by branch + # protection never waits on a skipped workflow (RFC-0043 §9.2). + changes: + name: Detect code changes + runs-on: ubuntu-latest + outputs: + code: ${{ steps.diff.outputs.code }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 2 + - id: diff + env: + BEFORE: ${{ github.event.before }} + run: | + if [ "$GITHUB_EVENT_NAME" = pull_request ]; then + # The test merge commit's first parent is the base branch head, + # so this diff is exactly what the pull request changes. + base=HEAD^1 + elif [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ] \ + && git fetch --no-tags --depth=1 origin "$BEFORE"; then + base=$BEFORE + else + echo "No base to compare with: treating it as a code change." + echo "code=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + git diff --name-only "$base" HEAD + # Documentation nothing builds or tests: Markdown at the top level, + # docs/, rfc/ and assets/. docs/api/providers.md is generated and + # contract-tests checks it against its generator, so it is code. + if git diff --quiet "$base" HEAD -- . ':(exclude,glob)*.md' \ + ':(exclude)docs' ':(exclude)rfc' ':(exclude)assets' \ + && git diff --quiet "$base" HEAD -- docs/api/providers.md; then + echo "code=false" >> "$GITHUB_OUTPUT" + else + echo "code=true" >> "$GITHUB_OUTPUT" + fi + # ── Rust core: fmt + clippy + tests (single OS — cassettes are platform-agnostic) ── rust-test: name: Rust core (fmt + clippy + test) runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -33,6 +76,8 @@ jobs: node-binding: name: Node binding (${{ matrix.settings.target }}) runs-on: ${{ matrix.settings.host }} + needs: changes + if: needs.changes.outputs.code == 'true' env: MACOSX_DEPLOYMENT_TARGET: '10.13' strategy: @@ -145,6 +190,8 @@ jobs: python-binding: name: Python binding runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -170,6 +217,8 @@ jobs: ffi-build: name: aimux-ffi (${{ matrix.target }}) runs-on: ${{ matrix.host }} + needs: changes + if: needs.changes.outputs.code == 'true' strategy: fail-fast: false matrix: @@ -205,6 +254,8 @@ jobs: java-binding: name: Java binding (JDK ${{ matrix.java }}) runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' strategy: fail-fast: false matrix: @@ -235,6 +286,8 @@ jobs: kotlin-binding: name: Kotlin binding (JDK ${{ matrix.java }}) runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' strategy: fail-fast: false matrix: @@ -262,6 +315,8 @@ jobs: go-binding: name: Go binding (${{ matrix.os }}) runs-on: ${{ matrix.os }} + needs: changes + if: needs.changes.outputs.code == 'true' strategy: fail-fast: false matrix: @@ -296,6 +351,8 @@ jobs: flutter-binding: name: Flutter binding runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -324,6 +381,8 @@ jobs: flutter-example-build: name: Flutter example (${{ matrix.platform }}) runs-on: ${{ matrix.host }} + needs: changes + if: needs.changes.outputs.code == 'true' strategy: fail-fast: false matrix: @@ -394,6 +453,8 @@ jobs: contract-tests: name: Contract tests runs-on: ubuntu-latest + needs: changes + if: needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - name: ProviderName generator drift (checked-in files must match generator output) @@ -426,6 +487,8 @@ jobs: swift-contract-tests: name: Swift contract tests runs-on: macos-latest + needs: changes + if: needs.changes.outputs.code == 'true' steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -438,3 +501,37 @@ jobs: DYLD_LIBRARY_PATH: ${{ github.workspace }}/target/release run: | swift test -Xlinker -L${{ github.workspace }}/target/release + + # The one check to require in branch protection. It runs whatever happened + # above: a job may be skipped only because "changes" found no code change; + # any other skip, failure or cancellation fails it. + ci-result: + name: CI result + if: always() + needs: + - changes + - rust-test + - node-binding + - python-binding + - ffi-build + - java-binding + - kotlin-binding + - go-binding + - flutter-binding + - flutter-example-build + - contract-tests + - swift-contract-tests + runs-on: ubuntu-latest + steps: + - env: + NEEDS: ${{ toJSON(needs) }} + CODE: ${{ needs.changes.outputs.code }} + run: | + echo "$NEEDS" | jq -e --arg code "$CODE" ' + .changes.result == "success" and + (del(.changes) | all(.result == "success" + or (.result == "skipped" and $code == "false")))' + if [ "$CODE" = false ]; then + echo "Documentation-only change: build and test jobs not applicable." \ + | tee -a "$GITHUB_STEP_SUMMARY" + fi From cdd499cb15fab06ce9663c25d1415697a52d676f Mon Sep 17 00:00:00 2001 From: cunninghamcard-bit Date: Thu, 15 Oct 2026 16:50:48 +0800 Subject: [PATCH 2/2] ci: run only the jobs a pull request needs, on Linux unless it is platform-specific Every code pull request ran all 24 jobs, five of them on macOS, whose runners the free plan limits to five at a time across all open pull requests. Most pull requests change neither the platform-specific parts nor more than one binding. "changes" now decides per job, as one JSON map that every job and "CI result" read: - documentation only: nothing, as before; - only inside bindings//: that binding's jobs (the Rust workspace does not read the bindings); - aimux-ffi/: the Rust jobs and every binding that links the C ABI (Node and Python link the Rust crates directly); - anything else: every job. Contract tests run for any code change, because they check generated files that live in every binding. On pull requests the jobs run on Linux, and the macOS and Windows legs of the Node, aimux-ffi, Go and Flutter example jobs are added only when the change touches the C ABI, the Node, Go, Flutter or Swift bindings, a Cargo manifest, a build script, .gitattributes, the toolchain or the workflows; the Rust code itself uses rustls and no platform cfg outside tools/. A change to the shared Rust code still runs the Windows x64 Node tests, the only job that runs that code on Windows, where I/O errors and refused connections behave differently. Swift contract tests, macOS-only, run whenever they are needed: they caught a stack overflow no Linux job hit. Pushes to master run everything, as the AI SDK does, so a platform problem surfaces on master at the latest. "CI result" now accepts a skipped job only when "changes" marked it as not needed. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_0112xDERMUyD5JALkFhnsdWS --- .github/workflows/ci.yml | 251 +++++++++++++++++++++++++++------------ 1 file changed, 172 insertions(+), 79 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ddd424a23..e0ad561ce 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,15 +10,44 @@ env: CARGO_TERM_COLOR: always jobs: - # Decides once whether a change can affect anything the jobs below build or - # test. A change that only touches documentation skips them; "CI result" at - # the end still runs and reports it, so a check required by branch + # Decides once which jobs a change needs and where they run. "CI result" at + # the end always runs and reports it, so a check required by branch # protection never waits on a skipped workflow (RFC-0043 §9.2). + # + # What runs, on a pull request: + # - Documentation only (top-level Markdown, docs/, rfc/, assets/): nothing. + # docs/api/providers.md is generated and checked, so it counts as code. + # - Only inside bindings//: that binding's jobs. The Rust workspace + # does not read the bindings, so nothing else can break. + # - aimux-ffi/: the Rust jobs and every binding that links the C ABI. Node + # and Python link the Rust crates directly, not the C ABI. + # - Anything else (the Rust crates, tools, scripts, contract fixtures, + # Cargo manifests, workflows): every job. + # Contract tests run for any code change: they check generated files that + # live in every binding. + # + # Where it runs, on a pull request: the Rust code is platform-neutral + # (rustls, no platform cfg outside tools/), so jobs run on Linux, and the + # macOS and Windows legs are added only when the change touches what differs + # per platform: the C ABI, the Node, Go, Flutter or Swift bindings, a Cargo + # manifest (dependencies), a build script, line-ending rules + # (.gitattributes), the toolchain or the workflows. + # Shared Rust changes still get one Windows leg (Node x64 tests); Swift + # contract tests are macOS-only and run whenever they are needed: they + # drive the C ABI from async Swift, which caught a stack overflow no Linux + # job hit. + # + # Pushes to master, and runs with no base to compare with, run everything. changes: name: Detect code changes runs-on: ubuntu-latest outputs: code: ${{ steps.diff.outputs.code }} + run: ${{ steps.diff.outputs.run }} + node-legs: ${{ steps.legs.outputs.node }} + ffi-legs: ${{ steps.legs.outputs.ffi }} + go-legs: ${{ steps.legs.outputs.go }} + example-legs: ${{ steps.legs.outputs.example }} steps: - uses: actions/checkout@v4 with: @@ -31,32 +60,134 @@ jobs: # The test merge commit's first parent is the base branch head, # so this diff is exactly what the pull request changes. base=HEAD^1 + everything=false elif [ -n "$BEFORE" ] && [ "$BEFORE" != 0000000000000000000000000000000000000000 ] \ && git fetch --no-tags --depth=1 origin "$BEFORE"; then base=$BEFORE + everything=true else - echo "No base to compare with: treating it as a code change." - echo "code=true" >> "$GITHUB_OUTPUT" - exit 0 + echo "No base to compare with: running everything." + base= + everything=true fi - git diff --name-only "$base" HEAD - # Documentation nothing builds or tests: Markdown at the top level, - # docs/, rfc/ and assets/. docs/api/providers.md is generated and - # contract-tests checks it against its generator, so it is code. - if git diff --quiet "$base" HEAD -- . ':(exclude,glob)*.md' \ - ':(exclude)docs' ':(exclude)rfc' ':(exclude)assets' \ - && git diff --quiet "$base" HEAD -- docs/api/providers.md; then - echo "code=false" >> "$GITHUB_OUTPUT" - else - echo "code=true" >> "$GITHUB_OUTPUT" + if [ -n "$base" ]; then + git diff --name-only "$base" HEAD + fi + # changed PATHSPEC...: prints true when the change touches any of them. + changed() { + if [ -n "$base" ] && git diff --quiet "$base" HEAD -- "$@"; then + echo false + else + echo true + fi + } + not_docs=(. ':(exclude,glob)*.md' ':(exclude)docs' ':(exclude)rfc' ':(exclude)assets') + generated_doc=$(changed docs/api/providers.md) + code=$(changed "${not_docs[@]}") + shared=$(changed "${not_docs[@]}" ':(exclude)bindings' ':(exclude)aimux-ffi') + # Line-ending rules apply to files of every job, wherever the + # .gitattributes lives. + if [ "$generated_doc" = true ] \ + || [ "$(changed ':(glob)**/.gitattributes')" = true ]; then + code=true + shared=true + fi + if [ "$everything" = true ]; then + shared=$code + fi + platforms=$everything + if [ "$(changed .github rust-toolchain.toml .cargo aimux-ffi bindings/node \ + bindings/go bindings/flutter bindings/swift \ + scripts/build-ios-xcframework.sh ':(glob)**/.gitattributes' \ + ':(glob)**/Cargo.toml' ':(glob)**/build.rs')" = true ]; then + platforms=true fi + # job NAME DIRECTORY...: the job runs when shared code changed, or + # any of the directories it builds and tests. + job() { + local name=$1 run=$shared + shift + if [ "$(changed "$@")" = true ]; then + run=true + fi + printf '"%s": %s,' "$name" "$run" + } + run="{ + $(job rust-test aimux-ffi) + $(job node-binding bindings/node) + $(job python-binding bindings/python) + $(job ffi-build aimux-ffi) + $(job java-binding aimux-ffi bindings/java) + $(job kotlin-binding aimux-ffi bindings/kotlin) + $(job go-binding aimux-ffi bindings/go) + $(job flutter-binding aimux-ffi bindings/flutter) + $(job flutter-example-build aimux-ffi bindings/flutter) + $(job swift-contract-tests aimux-ffi bindings/swift) + \"contract-tests\": $code}" + run=$(jq -c . <<< "$run") + printf 'code=%s\nshared=%s\nplatforms=%s\nrun=%s\n' \ + "$code" "$shared" "$platforms" "$run" \ + | tee -a "$GITHUB_OUTPUT" + # The matrices of the jobs that have macOS or Windows legs. A leg on + # another host than ubuntu-latest runs only when "platforms" is true, + # except the Windows x64 Node test leg, which also runs whenever the + # shared Rust code changed: it is the only job that runs that code on + # Windows, where I/O errors and refused connections behave differently + # (Swift contract tests cover macOS). + - id: legs + env: + PLATFORMS: ${{ steps.diff.outputs.platforms }} + SHARED: ${{ steps.diff.outputs.shared }} + run: | + legs() { + jq -c --arg platforms "$PLATFORMS" --arg shared "$SHARED" \ + 'map(select(.host == "ubuntu-latest" or $platforms == "true" + or (.shared == true and $shared == "true")) | del(.shared))' + } + node=$(legs <<'EOF' + [ + {"host": "windows-latest", "target": "x86_64-pc-windows-msvc", "binary": "aimux.win32-x64-msvc.node", "test": true, "shared": true}, + {"host": "windows-latest", "target": "aarch64-pc-windows-msvc", "binary": "aimux.win32-arm64-msvc.node", "test": false}, + {"host": "macos-15-intel", "target": "x86_64-apple-darwin", "binary": "aimux.darwin-x64.node", "test": true}, + {"host": "macos-latest", "target": "aarch64-apple-darwin", "binary": "aimux.darwin-arm64.node", "test": true}, + {"host": "ubuntu-latest", "target": "x86_64-unknown-linux-gnu", "binary": "aimux.linux-x64-gnu.node", "test": true}, + {"host": "ubuntu-latest", "target": "aarch64-unknown-linux-gnu", "binary": "aimux.linux-arm64-gnu.node", "test": false} + ] + EOF + ) + ffi=$(legs <<'EOF' + [ + {"host": "ubuntu-latest", "target": "x86_64-unknown-linux-gnu", "artifact": "libaimux_ffi.so"}, + {"host": "macos-latest", "target": "aarch64-apple-darwin", "artifact": "libaimux_ffi.dylib"}, + {"host": "windows-latest", "target": "x86_64-pc-windows-msvc", "artifact": "aimux_ffi.dll"} + ] + EOF + ) + # macOS included because the cgo LDFLAGS are platform-specific + # (ld64 rejects GNU -Bstatic/-Bdynamic; see bindings/go/aimux.go). + go=$(legs <<'EOF' + [ + {"host": "ubuntu-latest"}, + {"host": "macos-latest"} + ] + EOF + ) + example=$(legs <<'EOF' + [ + {"platform": "ios", "host": "macos-latest", "targets": "aarch64-apple-ios aarch64-apple-ios-sim"}, + {"platform": "android", "host": "ubuntu-latest", "targets": "aarch64-linux-android armv7-linux-androideabi x86_64-linux-android"} + ] + EOF + ) + printf 'node=%s\nffi=%s\ngo=%s\nexample=%s\n' "$node" "$ffi" "$go" "$example" \ + | tee -a "$GITHUB_OUTPUT" # ── Rust core: fmt + clippy + tests (single OS — cassettes are platform-agnostic) ── rust-test: name: Rust core (fmt + clippy + test) runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['rust-test'] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -77,37 +208,13 @@ jobs: name: Node binding (${{ matrix.settings.target }}) runs-on: ${{ matrix.settings.host }} needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['node-binding'] env: MACOSX_DEPLOYMENT_TARGET: '10.13' strategy: fail-fast: false matrix: - settings: - - host: windows-latest - target: x86_64-pc-windows-msvc - binary: aimux.win32-x64-msvc.node - test: true - - host: windows-latest - target: aarch64-pc-windows-msvc - binary: aimux.win32-arm64-msvc.node - test: false - - host: macos-15-intel - target: x86_64-apple-darwin - binary: aimux.darwin-x64.node - test: true - - host: macos-latest - target: aarch64-apple-darwin - binary: aimux.darwin-arm64.node - test: true - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - binary: aimux.linux-x64-gnu.node - test: true - - host: ubuntu-latest - target: aarch64-unknown-linux-gnu - binary: aimux.linux-arm64-gnu.node - test: false + settings: ${{ fromJSON(needs.changes.outputs.node-legs) }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -191,7 +298,7 @@ jobs: name: Python binding runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['python-binding'] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -218,20 +325,11 @@ jobs: name: aimux-ffi (${{ matrix.target }}) runs-on: ${{ matrix.host }} needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['ffi-build'] strategy: fail-fast: false matrix: - include: - - host: ubuntu-latest - target: x86_64-unknown-linux-gnu - artifact: libaimux_ffi.so - - host: macos-latest - target: aarch64-apple-darwin - artifact: libaimux_ffi.dylib - - host: windows-latest - target: x86_64-pc-windows-msvc - artifact: aimux_ffi.dll + include: ${{ fromJSON(needs.changes.outputs.ffi-legs) }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -255,7 +353,7 @@ jobs: name: Java binding (JDK ${{ matrix.java }}) runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['java-binding'] strategy: fail-fast: false matrix: @@ -287,7 +385,7 @@ jobs: name: Kotlin binding (JDK ${{ matrix.java }}) runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['kotlin-binding'] strategy: fail-fast: false matrix: @@ -313,16 +411,14 @@ jobs: # ── Go binding: build FFI static archive + vet/test ─────────────────────── go-binding: - name: Go binding (${{ matrix.os }}) - runs-on: ${{ matrix.os }} + name: Go binding (${{ matrix.host }}) + runs-on: ${{ matrix.host }} needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['go-binding'] strategy: fail-fast: false matrix: - # macOS included because the cgo LDFLAGS are platform-specific - # (ld64 rejects GNU -Bstatic/-Bdynamic; see bindings/go/aimux.go). - os: [ubuntu-latest, macos-latest] + include: ${{ fromJSON(needs.changes.outputs.go-legs) }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -352,7 +448,7 @@ jobs: name: Flutter binding runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['flutter-binding'] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -382,17 +478,11 @@ jobs: name: Flutter example (${{ matrix.platform }}) runs-on: ${{ matrix.host }} needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['flutter-example-build'] strategy: fail-fast: false matrix: - include: - - platform: ios - host: macos-latest - targets: aarch64-apple-ios aarch64-apple-ios-sim - - platform: android - host: ubuntu-latest - targets: aarch64-linux-android armv7-linux-androideabi x86_64-linux-android + include: ${{ fromJSON(needs.changes.outputs.example-legs) }} steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -454,7 +544,7 @@ jobs: name: Contract tests runs-on: ubuntu-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['contract-tests'] steps: - uses: actions/checkout@v4 - name: ProviderName generator drift (checked-in files must match generator output) @@ -488,7 +578,7 @@ jobs: name: Swift contract tests runs-on: macos-latest needs: changes - if: needs.changes.outputs.code == 'true' + if: fromJSON(needs.changes.outputs.run)['swift-contract-tests'] steps: - uses: actions/checkout@v4 - uses: dtolnay/rust-toolchain@stable @@ -503,8 +593,10 @@ jobs: swift test -Xlinker -L${{ github.workspace }}/target/release # The one check to require in branch protection. It runs whatever happened - # above: a job may be skipped only because "changes" found no code change; - # any other skip, failure or cancellation fails it. + # above: a job may be skipped only because "changes" decided it is not + # needed; any other skip, failure or cancellation fails it. A new job goes + # into the map in "changes" (a job missing there turns this red) and into + # the list below (a job missing here is not checked at all). ci-result: name: CI result if: always() @@ -525,12 +617,13 @@ jobs: steps: - env: NEEDS: ${{ toJSON(needs) }} + RUN: ${{ needs.changes.outputs.run }} CODE: ${{ needs.changes.outputs.code }} run: | - echo "$NEEDS" | jq -e --arg code "$CODE" ' + echo "$NEEDS" | jq -e --argjson run "${RUN:-null}" ' .changes.result == "success" and - (del(.changes) | all(.result == "success" - or (.result == "skipped" and $code == "false")))' + (del(.changes) | to_entries | all(.value.result == "success" + or (.value.result == "skipped" and $run[.key] == false)))' if [ "$CODE" = false ]; then echo "Documentation-only change: build and test jobs not applicable." \ | tee -a "$GITHUB_STEP_SUMMARY"