From a4d92524cb268b9cab994872ed6308feaab0f401 Mon Sep 17 00:00:00 2001 From: Andres Cera Date: Thu, 25 Jun 2026 14:00:09 -0500 Subject: [PATCH] ci(srtla): parallelize compat-matrix pairs --- .github/workflows/compat-matrix.yml | 90 +++++++++++++++++++++++++---- tests/compat/run-matrix.sh | 36 ++++++++++-- 2 files changed, 111 insertions(+), 15 deletions(-) diff --git a/.github/workflows/compat-matrix.yml b/.github/workflows/compat-matrix.yml index 29c9cc6..f8cce6b 100644 --- a/.github/workflows/compat-matrix.yml +++ b/.github/workflows/compat-matrix.yml @@ -9,6 +9,11 @@ name: Compatibility Matrix # * weekly drift -> re-runs the informational tier against upstream HEADs, # purely to surface upstream divergence; never blocks # +# Each tier fans out one job per pair (strategy.matrix from generate-matrix), so +# the independent pairs run in parallel rather than as one long sequential job. +# Per-tier gate jobs ("Blocking tier" / "Informational tier") aggregate the legs +# back into a single, stable merge-gating status check. +# # amd64 only — bonded UDP interop is exercised on a single ubuntu-latest host. on: @@ -91,13 +96,19 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" # --------------------------------------------------------------------- # - # Blocking tier — a failing pair fails the workflow. # + # Blocking tier — one job per pair, fanned out from matrix.yaml so the # + # independent pairs run in parallel instead of one sequential job. # + # fail-fast: false keeps every pair reporting; the compat-blocking-gate # + # job below folds the legs into the single merge-gating status. # # --------------------------------------------------------------------- # compat-blocking: - name: Blocking tier + name: "blocking: ${{ matrix.sender }} -> ${{ matrix.receiver }}" needs: generate-matrix if: github.event_name != 'schedule' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.generate-matrix.outputs.blocking) }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -115,25 +126,55 @@ jobs: with: tier: blocking - - name: Run blocking compatibility tier - run: tests/compat/run-matrix.sh --tier blocking --duration 20 + - name: Run compatibility pair + run: >- + tests/compat/run-matrix.sh + --sender "${{ matrix.sender }}" + --receiver "${{ matrix.receiver }}" + --duration 20 - name: Upload results if: always() uses: actions/upload-artifact@v7 with: - name: compat-results-blocking + name: compat-results-blocking-${{ matrix.sender }}-x-${{ matrix.receiver }} path: tests/compat/results/**/result.json if-no-files-found: ignore # --------------------------------------------------------------------- # - # Informational tier — gates PRs (pinned SHAs; a failure is a regression). # + # Blocking gate — one stable status that fails if any blocking leg # + # failed, so "a failing blocking pair fails this run" holds under one # + # check name no matter how many pairs the matrix fans into. # + # --------------------------------------------------------------------- # + compat-blocking-gate: + name: Blocking tier + needs: compat-blocking + if: always() && github.event_name != 'schedule' + runs-on: ubuntu-latest + steps: + - name: Require all blocking pairs to pass + env: + RESULT: ${{ needs.compat-blocking.result }} + run: | + echo "blocking matrix result: $RESULT" + [ "$RESULT" = "success" ] || { + echo "::error title=Blocking tier::one or more blocking compat pairs failed ($RESULT)" + exit 1 + } + + # --------------------------------------------------------------------- # + # Informational tier — same per-pair fan-out (pinned SHAs; a failure is # + # a regression). Only materializes when matrix.yaml has informational # + # pairs (has_informational guard). # # --------------------------------------------------------------------- # compat-informational: - name: Informational tier + name: "informational: ${{ matrix.sender }} -> ${{ matrix.receiver }}" needs: generate-matrix if: github.event_name != 'schedule' && needs.generate-matrix.outputs.has_informational == 'true' runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: ${{ fromJSON(needs.generate-matrix.outputs.informational) }} steps: - name: Checkout code uses: actions/checkout@v7 @@ -151,17 +192,46 @@ jobs: with: tier: informational - - name: Run informational compatibility tier - run: tests/compat/run-matrix.sh --tier informational --duration 20 + - name: Run compatibility pair + run: >- + tests/compat/run-matrix.sh + --sender "${{ matrix.sender }}" + --receiver "${{ matrix.receiver }}" + --duration 20 - name: Upload results if: always() uses: actions/upload-artifact@v7 with: - name: compat-results-informational + name: compat-results-informational-${{ matrix.sender }}-x-${{ matrix.receiver }} path: tests/compat/results/**/result.json if-no-files-found: ignore + # --------------------------------------------------------------------- # + # Informational gate — stable status mirroring the blocking gate; passes # + # vacuously when no informational pairs are registered. # + # --------------------------------------------------------------------- # + compat-informational-gate: + name: Informational tier + needs: [generate-matrix, compat-informational] + if: always() && github.event_name != 'schedule' + runs-on: ubuntu-latest + steps: + - name: Require all informational pairs to pass + env: + RESULT: ${{ needs.compat-informational.result }} + HAS_INFO: ${{ needs.generate-matrix.outputs.has_informational }} + run: | + echo "informational matrix result: $RESULT (has_informational=$HAS_INFO)" + if [ "$HAS_INFO" != "true" ]; then + echo "No informational pairs registered; nothing to gate." + exit 0 + fi + [ "$RESULT" = "success" ] || { + echo "::error title=Informational tier::one or more informational compat pairs failed ($RESULT)" + exit 1 + } + # --------------------------------------------------------------------- # # pcap replay — runs only when an LFS fixture is present. Missing # # fixture (or replay tool not yet wired) is a graceful skip, mirroring # diff --git a/tests/compat/run-matrix.sh b/tests/compat/run-matrix.sh index c8cc58f..4bc2903 100755 --- a/tests/compat/run-matrix.sh +++ b/tests/compat/run-matrix.sh @@ -23,10 +23,18 @@ # # Usage: # run-matrix.sh --pair x [options] +# run-matrix.sh --sender --receiver [options] # run-matrix.sh --tier blocking|informational|all [options] # # Options: -# --pair x Run a single pair by token (e.g. belabox-senderxours, oursxours). +# --pair x Run a single pair by harness token (e.g. belabox-senderxours, +# oursxours). +# --sender With --receiver: run a single pair addressed by its +# --receiver matrix.yaml names (e.g. --sender ceralive-srtla-send-rs +# --receiver ours). This is the CI-matrix fan-out entry point — +# the names map to harness tokens via the same table the --tier +# path uses, so no token aliasing has to leak into the workflow. +# Tier is resolved from matrix.yaml. # --tier Run every matrix.yaml pair of tier blocking|informational|all. # --scenario stream (default, healthy) | port-mismatch (negative/broken). # --duration Measurement window length (default 20). @@ -62,6 +70,8 @@ now_ms() { date +%s%3N; } # --------------------------------------------------------------------------- # TIER="" PAIR="" +SENDER_NAME="" +RECEIVER_NAME="" SCENARIO="stream" DURATION=20 KEEP_LOGS=0 @@ -71,17 +81,27 @@ while [[ $# -gt 0 ]]; do case "$1" in --tier) TIER="${2:?--tier needs a value}"; shift 2 ;; --pair) PAIR="${2:?--pair needs a value}"; shift 2 ;; + --sender) SENDER_NAME="${2:?--sender needs a value}"; shift 2 ;; + --receiver) RECEIVER_NAME="${2:?--receiver needs a value}"; shift 2 ;; --scenario) SCENARIO="${2:?--scenario needs a value}"; shift 2 ;; --duration) DURATION="${2:?--duration needs a value}"; shift 2 ;; --keep-logs) KEEP_LOGS=1; shift ;; --build-dir) BUILD_DIR="${2:?--build-dir needs a value}"; shift 2 ;; - -h|--help) sed -n '2,40p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; + -h|--help) sed -n '2,46p' "${BASH_SOURCE[0]}" | sed 's/^# \{0,1\}//'; exit 0 ;; *) die "unknown argument '$1' (try --help)" ;; esac done -[[ -n "$TIER" || -n "$PAIR" ]] || die "specify --pair x or --tier " -[[ -z "$TIER" || -z "$PAIR" ]] || die "--pair and --tier are mutually exclusive" +SELECT_BY_NAME=0 +[[ -n "$SENDER_NAME" || -n "$RECEIVER_NAME" ]] && SELECT_BY_NAME=1 +[[ "$SELECT_BY_NAME" -eq 0 || ( -n "$SENDER_NAME" && -n "$RECEIVER_NAME" ) ]] \ + || die "--sender and --receiver must be given together" +_modes=0 +[[ -n "$PAIR" ]] && _modes=$((_modes + 1)) +[[ "$SELECT_BY_NAME" -eq 1 ]] && _modes=$((_modes + 1)) +[[ -n "$TIER" ]] && _modes=$((_modes + 1)) +[[ "$_modes" -eq 1 ]] \ + || die "specify exactly one of: --pair x, --sender --receiver , or --tier " case "$SCENARIO" in stream|port-mismatch) ;; *) die "unknown --scenario '$SCENARIO'";; esac [[ "$DURATION" =~ ^[0-9]+$ && "$DURATION" -ge 1 ]] || die "--duration must be a positive integer" @@ -456,7 +476,13 @@ run_pair() { declare -a JOBS add_job() { JOBS+=("$1|$2|$3"); } -if [[ -n "$PAIR" ]]; then +if [[ "$SELECT_BY_NAME" -eq 1 ]]; then + s="$(matrix_to_token "$SENDER_NAME")" \ + || die "unknown matrix sender name '$SENDER_NAME' (see matrix.yaml senders:)" + r="$(matrix_to_token "$RECEIVER_NAME")" \ + || die "unknown matrix receiver name '$RECEIVER_NAME' (see matrix.yaml receivers:)" + add_job "$s" "$r" "${PAIR_TIER["$s:$r"]:-blocking}" +elif [[ -n "$PAIR" ]]; then read -r s r < <(split_pair "$PAIR") \ || die "could not parse --pair '$PAIR' into known sender x receiver tokens" add_job "$s" "$r" "${PAIR_TIER["$s:$r"]:-blocking}"