Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 53 additions & 10 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,11 @@ jobs:
fail-fast: false
matrix:
os: [macos-latest, windows-latest]
# Two legs per OS (scripts/ci-test-shard.py): one leg linked ~240
# test binaries and ran them serially for ~26 min, the merge queue's
# critical path. Shard 1 = unit tests + doctests + a third of the
# integration targets; shard 2 = the rest.
shard: [1, 2]
exclude:
# macOS legs run on main, the merge queue and nightly, not per PR push.
- os: ${{ github.event_name == 'pull_request' && 'macos-latest' || '' }}
Expand Down Expand Up @@ -400,11 +405,19 @@ jobs:
# `zip`, and assert the pinned release.
SOCKET_PATCH_GO_E2E_REQUIRED: '1'
SOCKET_PATCH_GO_E2E_VERSION: '1.24'
TEST_SHARD: ${{ matrix.shard }}
# This leg's share of `cargo test --workspace --no-fail-fast`; the
# shards together run exactly that selection (test_ci_test_shard.py).
run: |
cargo test --workspace --no-fail-fast
python3 scripts/ci-test-shard.py "$TEST_SHARD" 2

test-release:
if: github.event.pull_request.draft != true
# Not in the merge queue: each PR's head already ran this, and so did
# every PR ahead of it, so a merge group would re-spend ~30 min (23 of it
# compiling) on the same release-mode suite while the queue waits. It
# still runs on every PR and on main after each merge; `ci-ok` counts a
# skipped job as passing.
if: github.event.pull_request.draft != true && github.event_name != 'merge_group'
runs-on: ubuntu-latest
# Every tests/ target is its own optimized link (~240 test binaries).
# The manifest-less VEX suites share two multi-module binaries
Expand Down Expand Up @@ -1218,13 +1231,27 @@ jobs:
# way): a missing suite fails the leg, and every suite must run at
# least one test on its own. Maven is installed only where a
# selected test needs it (gradle_vendor_395's mixed root).
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_'}
# The hosted suite (~43 real-Gradle builds, ~31 min serially) is
# split into three legs per line so it stops being the merge-queue
# critical path: the agent suites plus `gradle_hosted_[345]`, the
# `gradle_hosted_[b-p]` names, and a catch-all that `--skip`s
# exactly those words, so a new test always lands in some leg
# (test_ci_gradle_prefixes.py keeps the skip list in sync).
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'}
- {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '6.9.4', java: '11', test_filter: '--ignored gradle_vendor_ gradle_multi_project'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'}
- {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '7.6.6', java: '17', test_filter: '--ignored gradle_vendor_ gradle_multi_project'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'}
- {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '8.14.3', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_'}
- {os: ubuntu-latest, suite: 'e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build', jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_agent_ gradle_hosted_3 gradle_hosted_4 gradle_hosted_5'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_b gradle_hosted_c gradle_hosted_d gradle_hosted_e gradle_hosted_f gradle_hosted_l gradle_hosted_m gradle_hosted_n gradle_hosted_o gradle_hosted_p'}
- {os: ubuntu-latest, suite: e2e_redirect_gradle_build, jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_hosted_ --skip gradle_hosted_3 --skip gradle_hosted_4 --skip gradle_hosted_5 --skip gradle_hosted_b --skip gradle_hosted_c --skip gradle_hosted_d --skip gradle_hosted_e --skip gradle_hosted_f --skip gradle_hosted_l --skip gradle_hosted_m --skip gradle_hosted_n --skip gradle_hosted_o --skip gradle_hosted_p'}
- {os: ubuntu-latest, suite: 'e2e_vendor_gradle_build e2e_vendor_jvm_build', jvm_tool: gradle, gradle: '9.8.0', java: '21', test_filter: '--ignored gradle_vendor_ gradle_multi_project'}
- {os: windows-latest, suite: e2e_vendor_jvm_build, jvm_tool: gradle, gradle: '8.14.3', java: '17', test_filter: '--ignored gradle_multi_project'}
# Real-sbt hosted (socket-patch.sbt) + vendored
Expand Down Expand Up @@ -1904,7 +1931,12 @@ jobs:
# ----------------------------------------------------------------------
yarn-classic-matrix:
name: yarn-classic ${{ matrix.release }}
needs: [test, coverage]
# No `needs: [test, coverage]`: nothing here consumes their outputs,
# and waiting on the ~30 min windows `test` leg made this the merge
# queue's critical path.
Comment thread
mikolalysenko marked this conversation as resolved.
# Dropping that `needs` also dropped the draft skip it inherited, so
# gate on draft here directly (push/merge_group/schedule still run).
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest
timeout-minutes: 40
strategy:
Expand Down Expand Up @@ -1940,7 +1972,12 @@ jobs:

yarn-berry-e2e:
name: yarn-berry ${{ matrix.yarn }} (${{ matrix.os }})
needs: [test, coverage]
# No `needs: [test, coverage]`: nothing here consumes their outputs,
# and waiting on the ~30 min windows `test` leg made this the merge
# queue's critical path.
# Dropping that `needs` also dropped the draft skip it inherited, so
# gate on draft here directly (push/merge_group/schedule still run).
if: github.event.pull_request.draft != true
strategy:
fail-fast: false
matrix:
Expand Down Expand Up @@ -2022,7 +2059,8 @@ jobs:
# e2e_safety_cargo_build (its headline test honours the knobs).
cargo-vex-matrix:
name: cargo ${{ matrix.toolchain }} lock-v${{ matrix.lock || 'own' }} (${{ matrix.os }})
needs: [test, coverage, e2e-build]
# e2e-build only (its binaries); see yarn-classic-matrix on test/coverage.
needs: [e2e-build]
runs-on: ${{ matrix.os }}
timeout-minutes: 40
# What .cargo/config.toml's [env] gives processes cargo launches; these
Expand Down Expand Up @@ -2149,7 +2187,12 @@ jobs:
# available; this leg pulls both images and requires them.
cargo-old-toolchains:
name: cargo old toolchains (manifest [patch])
needs: [test, coverage]
# No `needs: [test, coverage]`: nothing here consumes their outputs,
# and waiting on the ~30 min windows `test` leg made this the merge
# queue's critical path.
# Dropping that `needs` also dropped the draft skip it inherited, so
# gate on draft here directly (push/merge_group/schedule still run).
if: github.event.pull_request.draft != true
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
Expand Down
55 changes: 55 additions & 0 deletions .github/workflows/merge-queue-janitor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
name: Merge queue janitor

# Cancels merge-group runs the queue has orphaned. Each queue entry runs CI on
# its own `gh-readonly-queue/main/pr-<n>-<base sha>` ref, so ci.yml's
# concurrency group never lets a newer run cancel an older one. When an entry
# ahead fails or is removed, the queue deletes the refs of every entry behind
# it and rebuilds them on new refs, but the runs on the deleted refs keep going
# (each one is a full ~200-job CI run, macOS legs included). A run whose ref no
# longer exists can never merge, so it is cancelled here. Every rebuild creates
# a new merge group, which triggers this sweep.
#
# Not a required check, and it never fails the merge group: a sweep error is
# only a warning.

on:
merge_group:
types: [checks_requested]
workflow_dispatch:

permissions: {}

concurrency:
group: merge-queue-janitor
cancel-in-progress: false

jobs:
cancel-orphaned-runs:
runs-on: ubuntu-latest
timeout-minutes: 5
permissions:
actions: write
contents: read
steps:
- name: Cancel merge-group runs whose queue ref is gone
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: |
set -uo pipefail
for status in queued in_progress; do
gh api --paginate "repos/$REPO/actions/runs?event=merge_group&status=$status&per_page=100" \
-q '.workflow_runs[] | "\(.id) \(.head_branch)"' || echo "::warning::could not list $status runs"
done | sort -u | while read -r id branch; do
case "$branch" in gh-readonly-queue/*) ;; *) continue ;; esac
# Only a definite 404 means the entry is gone; any other lookup
# error (rate limit, 5xx) leaves the run alone.
if err=$(gh api "repos/$REPO/git/ref/heads/$branch" --silent 2>&1); then
continue
fi
case "$err" in *"HTTP 404"*) ;; *) echo "::warning::ref lookup for $branch failed: $err"; continue ;; esac
echo "Cancelling run $id: $branch is no longer in the queue"
gh api -X POST "repos/$REPO/actions/runs/$id/cancel" --silent \
|| echo "::warning::could not cancel run $id"
done
exit 0
87 changes: 87 additions & 0 deletions scripts/ci-test-shard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
#!/usr/bin/env python3
"""Run one shard of ci.yml's `test` job: `cargo test --workspace` split over
`COUNT` runners so the macOS / Windows legs stop being the merge queue's
critical path (one leg spent ~10 min linking ~240 test binaries and ~15 min
running them).

Shard 1 runs the unit tests (`--lib --bins`, ~4 min of the run on Windows)
and the doctests; the integration-test targets (`cargo metadata`, kind
`test`) are dealt out over the shards by name, with shard 1 taking a smaller
share to balance its unit tests. Every target lands in exactly one shard, so
the union of the shards is the old single `cargo test --workspace` run.

Extra arguments after `SHARD COUNT` go to every `cargo test` invocation.
Each invocation runs with `--no-fail-fast`; the exit status is non-zero if
any of them failed.

python3 scripts/ci-test-shard.py 1 2
"""

import json
import subprocess
import sys

# Shard 1's share of the integration targets, relative to every other
# shard's 1.0 (it also carries the unit tests and doctests).
FIRST_SHARD_WEIGHT = 0.5


def integration_targets(metadata):
"""Sorted, de-duplicated names of the workspace's integration tests."""
members = set(metadata["workspace_members"])
return sorted({t["name"] for p in metadata["packages"] if p["id"] in members
for t in p["targets"] if "test" in t["kind"]})


def partition(names, count):
"""`count` lists covering `names` exactly once, in order, with the first
list weighted by FIRST_SHARD_WEIGHT."""
if count < 1:
raise ValueError("count must be >= 1")
weights = [FIRST_SHARD_WEIGHT if count > 1 else 1.0] + [1.0] * (count - 1)
shards = [[] for _ in range(count)]
load = [0.0] * count
for name in names:
# The least-loaded shard relative to its weight; ties go to the lower index.
i = min(range(count), key=lambda k: ((load[k] + 1) / weights[k], k))
shards[i].append(name)
load[i] += 1
return shards


def invocations(shard, count, names, extra=()):
"""The `cargo test` argument lists shard `shard` (1-based) runs."""
if not 1 <= shard <= count:
raise ValueError(f"shard {shard} not in 1..{count}")
base = ["cargo", "test", "--workspace", "--no-fail-fast", *extra]
mine = partition(names, count)[shard - 1]
runs = []
if shard == 1:
runs.append(base + ["--lib", "--bins"] + [a for n in mine for a in ("--test", n)])
runs.append(base + ["--doc"])
elif mine:
runs.append(base + [a for n in mine for a in ("--test", n)])
return runs


def main(argv):
if len(argv) < 2:
print(__doc__, file=sys.stderr)
return 2
shard, count = int(argv[0]), int(argv[1])
metadata = json.loads(subprocess.run(
["cargo", "metadata", "--no-deps", "--format-version", "1"],
check=True, capture_output=True, text=True).stdout)
names = integration_targets(metadata)
print(f"ci-test-shard: shard {shard}/{count}: "
f"{len(partition(names, count)[shard - 1])} of {len(names)} integration targets", flush=True)
status = 0
for args in invocations(shard, count, names, argv[2:]):
print("+ " + " ".join(args), flush=True)
if subprocess.run(args).returncode != 0:
status = 1
return status


if __name__ == "__main__":
sys.exit(main(sys.argv[1:]))
17 changes: 13 additions & 4 deletions scripts/tests/test_ci_e2e_tiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,8 +122,17 @@ def test_bundle_reads_cargo_json(self):

GRADLE_COMPAT = ROOT / ".github" / "workflows" / "gradle-compatibility.yml"
GRADLE_LINES = {"6.9.4": "11", "7.6.6": "17", "8.14.3": "21", "9.8.0": "21"}
AGENT_HOSTED = ("e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build",
"--ignored gradle_agent_ gradle_hosted_")
# The hosted suite is sharded over three legs per line (test_ci_gradle_prefixes
# HostedShards checks every hosted test runs in exactly one of them).
HOSTED_SHARD_1 = ["gradle_hosted_3", "gradle_hosted_4", "gradle_hosted_5"]
HOSTED_SHARD_2 = ["gradle_hosted_" + c for c in "bcdeflmnop"]
AGENT_HOSTED = (
("e2e_gradle_discovery_build e2e_gradle_agent_build e2e_redirect_gradle_build",
" ".join(["--ignored", "gradle_agent_", *HOSTED_SHARD_1])),
("e2e_redirect_gradle_build", " ".join(["--ignored", *HOSTED_SHARD_2])),
("e2e_redirect_gradle_build",
" ".join(["--ignored", "gradle_hosted_"] + [w for p in HOSTED_SHARD_1 + HOSTED_SHARD_2 for w in ("--skip", p)])),
)
VENDOR = ("e2e_vendor_gradle_build e2e_vendor_jvm_build", "--ignored gradle_vendor_ gradle_multi_project")


Expand Down Expand Up @@ -160,7 +169,7 @@ def test_pr_rows_are_the_lean_table(self):
gradle = [r for r in rows("e2e") if r.get("jvm_tool") == "gradle"]
want = []
for line, java in GRADLE_LINES.items():
for suite, test_filter in (AGENT_HOSTED, VENDOR):
for suite, test_filter in (*AGENT_HOSTED, VENDOR):
row = {"os": "ubuntu-latest", "suite": suite, "jvm_tool": "gradle", "gradle": line,
"java": java, "test_filter": test_filter}
# The allowance lasts only while a suite of the row is unlanded.
Expand All @@ -169,7 +178,7 @@ def test_pr_rows_are_the_lean_table(self):
want.append(row)
want.append({"os": "windows-latest", "suite": "e2e_vendor_jvm_build", "jvm_tool": "gradle",
"gradle": "8.14.3", "java": "17", "test_filter": "--ignored gradle_multi_project"})
self.assertEqual(len(gradle), 9)
self.assertEqual(len(gradle), 17)
self.assertEqual(sorted(map(str, gradle)), sorted(map(str, want)))
self.assertFalse([r for r in rows("e2e-full") if "gradle" in r or "jvm_tool" in r])

Expand Down
Loading
Loading