From ae5bb0bdeac4c57ffec96a720bec39c7ff1eb1d7 Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Wed, 30 Sep 2026 23:30:57 +0200 Subject: [PATCH] ci: PR shard profile (x86 and Windows whole, arm64 x2) _test.yml takes a shard_profile input: "standard" (the default; dry run and release pass nothing and keep ubuntu x3 / windows x2, byte-identical matrices) or "pr", which pr.yml now uses: ubuntu x86 and Windows run as one job each and only arm64, the slowest leg, splits in two. A PR runs few legs and every shard re-pays its setup, so fewer, longer jobs cost less. An unknown profile is an error. The wiring contract pins both the default and PR's use of the profile. Signed-off-by: Martin Vogel --- .github/workflows/_test.yml | 25 ++++++++++++++++++++----- .github/workflows/pr.yml | 8 +++++--- tests/test_lane_wiring_contract.sh | 10 ++++++++-- 3 files changed, 33 insertions(+), 10 deletions(-) diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index bc19ab8d3..d250dd031 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -16,6 +16,10 @@ on: description: 'Split each C-suite leg across parallel shard jobs (ubuntu x3, windows x2); off = one job per leg, byte-identical to the pre-shard topology' type: boolean default: false + shard_profile: + description: 'Shard counts when shard_suites is on: standard = ubuntu x3, windows x2 (dry run, release); pr = ubuntu x86 x1, ubuntu arm64 x2, windows x1 (fewer jobs re-paying setup)' + type: string + default: standard lanes: description: 'PR lane selection (scripts/ci/select-lanes.sh JSON list): only those legs and jobs run; "all" = every one (dry run, release)' type: string @@ -44,6 +48,7 @@ jobs: env: BROAD: ${{ inputs.broad_platforms }} SHARDS: ${{ inputs.shard_suites }} + SHARD_PROFILE: ${{ inputs.shard_profile }} LANES: ${{ inputs.lanes }} run: | # Every CORE leg is tagged with the lane (scripts/ci/select-lanes.sh) @@ -113,14 +118,24 @@ jobs: # shards); Windows gets 2 (every extra shard re-pays ~5 min of # MSYS2 setup); macOS stays at 1 — mac legs are not the critical # path and mac runner concurrency ceilings are the tightest. + # The pr profile shards less: a PR runs few legs and every shard + # re-pays its setup, so x86 and Windows run whole and only arm64, + # the slowest leg (~72 min unsharded), splits in two. + case "$SHARD_PROFILE" in + standard) N_X86=3; N_ARM=3; N_WIN=2 ;; + pr) N_X86=1; N_ARM=2; N_WIN=1 ;; + *) echo "::error::unknown shard_profile '$SHARD_PROFILE'"; exit 1 ;; + esac if [ "$SHARDS" = "true" ]; then - UNIX=$(jq -c '[.[] | . as $e - | (if ($e.os | startswith("ubuntu")) then 3 else 1 end) as $n + UNIX=$(jq -c --argjson x86 "$N_X86" --argjson arm "$N_ARM" '[.[] | . as $e + | (if ($e.os | startswith("ubuntu")) then + (if ($e.os | endswith("-arm")) then $arm else $x86 end) + else 1 end) as $n | range(1; $n + 1) as $i | $e + {shard: "\($i)/\($n)"}]' <<< "$UNIX") - WIN=$(jq -c '[.[] | . as $e - | range(1; 3) as $i - | $e + {shard: "\($i)/2"}]' <<< "$WIN") + WIN=$(jq -c --argjson n "$N_WIN" '[.[] | . as $e + | range(1; $n + 1) as $i + | $e + {shard: "\($i)/\($n)"}]' <<< "$WIN") fi echo "unix={\"include\":$UNIX}" >> "$GITHUB_OUTPUT" echo "windows={\"include\":$WIN}" >> "$GITHUB_OUTPUT" diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index d6d966912..9f45fb0cb 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -53,10 +53,12 @@ jobs: # Perf assertions are timing-sensitive on shared runners; they stay in # dry runs and releases where a flaky red does not block a merge. skip_perf: true - # Iteration speed: split each C-suite leg across parallel shard jobs - # (ubuntu x3, windows x2; coverage re-proven every run by the - # shard-completeness job). Dry runs and releases shard identically. + # Iteration speed: split C-suite legs across parallel shard jobs + # (coverage re-proven every run by the shard-completeness job). The PR + # profile shards only arm64 (x2); x86 and Windows run whole, since each + # shard re-pays its setup. Dry runs and releases keep x3 / x2. shard_suites: true + shard_profile: pr # ── Which lanes does this PR need? The changed files go through # scripts/ci/select-lanes.sh (tier, lanes, full, reasons); docs / CI / diff --git a/tests/test_lane_wiring_contract.sh b/tests/test_lane_wiring_contract.sh index 171e61b34..f57bcb11a 100755 --- a/tests/test_lane_wiring_contract.sh +++ b/tests/test_lane_wiring_contract.sh @@ -3,8 +3,9 @@ # are selected at all. # # 1. dry-run.yml and release.yml pass no `lanes` (and no shard profile) to -# _test.yml / _lint.yml / _security.yml, whose inputs default to "all": -# those runs execute every job exactly as before lane selection. +# _test.yml / _lint.yml / _security.yml, whose inputs default to "all" +# (and "standard"): those runs execute every job, sharded, exactly as +# before lane selection. Only pr.yml asks for the PR shard profile. # 2. pr.yml feeds the selector's lanes to every reusable workflow, runs the # docs-only contracts job, and hands ci-ok both the selection and the # run's job list (actions: read) -- ci-ok needs `contracts` but never the @@ -62,6 +63,9 @@ for caller in ("dry-run.yml", "release.yml"): require(not re.search(r"(?m)^\s+(lanes|shard_profile):", body), f"{caller} job {job} passes a lane selection to {called.group(1)}; " f"dry runs and releases must run every lane") +m = re.search(r"(?ms)^ shard_profile:\n(.*?)(?=^ \S|^\S|\Z)", text("_test.yml")) +require(m is not None and re.search(r"(?m)^ default: standard$", m.group(1)) is not None, + "_test.yml must declare a shard_profile input defaulting to standard") for callee in REUSABLE: src = text(callee) m = re.search(r"(?ms)^ lanes:\n(.*?)(?=^ \S|^\S|\Z)", src) @@ -83,6 +87,8 @@ for key in ("tier", "full", "lanes"): for job in ("security", "lint", "test"): require("lanes: ${{ needs.changes.outputs.lanes }}" in pr.get(job, ""), f"pr.yml {job} must pass the selected lanes") +require(re.search(r"(?m)^ shard_profile: pr$", pr.get("test", "")) is not None, + "pr.yml test must use the PR shard profile") require("scripts/test.sh --contracts-only" in pr.get("contracts", ""), "pr.yml contracts job must run scripts/test.sh --contracts-only") ci_ok = pr.get("ci-ok", "")