ci: skip unaffected language jobs in coverage baseline (#3822)
Every master push ran all 7 language coverage suites plus the
macOS cache warm (~55 runner-minutes) even when the push touched
a single SDK or only docs. Every Codecov flag has carryforward
enabled and all statuses are informational, so re-measuring
untouched languages re-uploads identical data.
A detect job now diffs the push range and gates each job on the
paths feeding its Codecov flag; untouched flags carry forward
their previous master report. The differ fails open to a full
run on any uncertainty (initial push, force push, CI or tooling
changes), and a new workflow_dispatch trigger forces a full
baseline on demand. The macOS cache warm follows the rust gate
since that cache only goes stale when Rust sources change.
diff --git a/.github/workflows/coverage-baseline.yml b/.github/workflows/coverage-baseline.yml
index 35aab11..b23d314 100644
--- a/.github/workflows/coverage-baseline.yml
+++ b/.github/workflows/coverage-baseline.yml
@@ -15,29 +15,73 @@
# specific language governing permissions and limitations
# under the License.
-# Full coverage baseline for all 7 languages (Rust, Java, C#, Python,
-# PHP, Node, Go). Runs on every push to master so Codecov has complete data
-# for carryforward on PR builds where only a subset of SDKs is tested.
+# Coverage baseline for all 7 languages (Rust, Java, C#, Python, PHP,
+# Node, Go). Runs on push to master so Codecov has data on master SHAs for
+# carryforward on PR builds where only a subset of SDKs is tested.
+#
+# Jobs are path-gated: a language job runs only when the paths feeding its
+# Codecov flag changed in the push (scripts/ci/coverage-baseline-affected.sh).
+# Untouched flags carry forward their previous master report, which stays
+# accurate because their sources did not change. workflow_dispatch forces a
+# full run (manual backfill).
name: Coverage baseline
on:
push:
branches: [master]
+ workflow_dispatch:
permissions:
contents: read
+# Per-event groups so a manual backfill run is not cancelled by the next
+# merge push (and vice versa).
concurrency:
- group: coverage-baseline-${{ github.ref }}
+ group: coverage-baseline-${{ github.event_name }}-${{ github.ref }}
cancel-in-progress: true
env:
IGGY_CI_BUILD: true
jobs:
+ detect:
+ name: Detect affected coverage jobs
+ # workflow_dispatch offers a branch picker, but baseline uploads and
+ # cache warms only make sense for master. Skipping detect skips every
+ # downstream job.
+ if: github.ref == 'refs/heads/master'
+ runs-on: ubuntu-latest
+ timeout-minutes: 10
+ outputs:
+ rust: ${{ steps.affected.outputs.rust }}
+ java: ${{ steps.affected.outputs.java }}
+ csharp: ${{ steps.affected.outputs.csharp }}
+ python: ${{ steps.affected.outputs.python }}
+ php: ${{ steps.affected.outputs.php }}
+ node: ${{ steps.affected.outputs.node }}
+ go: ${{ steps.affected.outputs.go }}
+ steps:
+ - uses: actions/checkout@v7.0.1
+ with:
+ # The gate diffs against the pre-push tip, which a shallow clone
+ # does not have.
+ fetch-depth: 0
+
+ - name: Compute affected gates
+ id: affected
+ # github.event.before is empty on workflow_dispatch; the script
+ # fails open to all-true there.
+ run: >-
+ scripts/ci/coverage-baseline-affected.sh
+ "${{ github.event.before }}" "${{ github.sha }}"
+ >> "$GITHUB_OUTPUT"
+ shell: bash
+
rust-coverage:
name: Rust coverage baseline
+ needs: detect
+ if: needs.detect.outputs.rust == 'true'
runs-on: ubuntu-latest
timeout-minutes: 45
steps:
@@ -108,6 +152,8 @@
java-coverage:
name: Java coverage baseline
+ needs: detect
+ if: needs.detect.outputs.java == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -151,6 +197,8 @@
csharp-coverage:
name: C# coverage baseline
+ needs: detect
+ if: needs.detect.outputs.csharp == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -223,6 +271,8 @@
python-coverage:
name: Python coverage baseline
+ needs: detect
+ if: needs.detect.outputs.python == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -316,6 +366,8 @@
php-coverage:
name: PHP coverage baseline
+ needs: detect
+ if: needs.detect.outputs.php == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -337,6 +389,8 @@
node-coverage:
name: Node coverage baseline
+ needs: detect
+ if: needs.detect.outputs.node == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -404,6 +458,8 @@
go-coverage:
name: Go coverage baseline
+ needs: detect
+ if: needs.detect.outputs.go == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
@@ -478,6 +534,11 @@
warm-cache-macos:
name: Warm macOS cache
+ needs: detect
+ # Gated on the rust key: the cache goes stale only when Rust sources
+ # change, and the Swatinem key is Cargo.lock-based, so skipping the save
+ # on non-Rust pushes leaves the previous cache valid.
+ if: needs.detect.outputs.rust == 'true'
runs-on: macos-14
timeout-minutes: 30
steps:
diff --git a/scripts/ci/coverage-baseline-affected.sh b/scripts/ci/coverage-baseline-affected.sh
new file mode 100755
index 0000000..60cc3bd
--- /dev/null
+++ b/scripts/ci/coverage-baseline-affected.sh
@@ -0,0 +1,96 @@
+#!/usr/bin/env bash
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements. See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership. The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied. See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+set -euo pipefail
+
+# Decide which coverage-baseline jobs a master push needs. Every Codecov flag
+# has carryforward enabled, so a language whose sources did not change keeps
+# serving its previous master report; only changed surfaces are re-measured.
+#
+# Fail-open: on any uncertainty (initial push, force push, unknown base) emit
+# true for every gate so a needed refresh is never skipped. workflow_dispatch
+# passes an empty base and lands on that path by design (manual full backfill).
+#
+# Usage: coverage-baseline-affected.sh <base-sha> <head-sha>
+# Output: one "<gate>=true|false" line per gate on stdout, shaped for
+# appending to $GITHUB_OUTPUT. Diagnostics go to stderr.
+
+BASE="${1:-}"
+HEAD="${2:-}"
+ZERO="0000000000000000000000000000000000000000"
+GATES=(rust java csharp python php node go)
+
+emit_all() {
+ local gate
+ for gate in "${GATES[@]}"; do
+ echo "${gate}=true"
+ done
+}
+
+if [[ -z "$HEAD" ]]; then
+ echo "usage: $0 <base-sha> <head-sha>" >&2
+ exit 2
+fi
+
+# Unusable base: initial push, force push, workflow_dispatch (empty base),
+# or a commit not in history.
+if [[ -z "$BASE" || "$BASE" == "$ZERO" ]] || ! git cat-file -e "${BASE}^{commit}" 2>/dev/null; then
+ echo "coverage-gate: unusable base '${BASE:-<empty>}', running all jobs" >&2
+ emit_all
+ exit 0
+fi
+
+# CI plumbing changes how every job builds and measures; re-baseline all.
+if ! git diff --quiet "$BASE" "$HEAD" -- .github scripts codecov.yml; then
+ echo "coverage-gate: CI/tooling change, running all jobs" >&2
+ emit_all
+ exit 0
+fi
+
+# A git failure here (exit > 1, e.g. corrupt object) reads as "changed",
+# falling open for that gate.
+changed() {
+ ! git diff --quiet "$BASE" "$HEAD" -- "$@"
+}
+
+# GATES is the single source for both this loop and emit_all, so a gate can
+# never be present on one path and missing on the other. A GATES entry
+# without a pathspec arm fails loud instead of reusing the previous
+# iteration's paths.
+for gate in "${GATES[@]}"; do
+ case "$gate" in
+ rust) paths=(core Cargo.toml Cargo.lock rust-toolchain.toml .cargo) ;;
+ java) paths=(foreign/java) ;;
+ csharp) paths=(foreign/csharp) ;;
+ python) paths=(foreign/python) ;;
+ php) paths=(foreign/php) ;;
+ node) paths=(foreign/node) ;;
+ # The go job also runs the bdd/go suite with foreign/go in -coverpkg.
+ go) paths=(foreign/go bdd/go) ;;
+ *)
+ echo "coverage-gate: no pathspecs defined for gate '$gate'" >&2
+ exit 1
+ ;;
+ esac
+ if changed "${paths[@]}"; then
+ echo "${gate}=true"
+ echo "coverage-gate: ${gate} affected" >&2
+ else
+ echo "${gate}=false"
+ fi
+done