From 0dc942508e207dc78292ac95ee092ca85a147e54 Mon Sep 17 00:00:00 2001 From: Ayaz Salikhov Date: Tue, 14 Jul 2026 17:09:20 +0100 Subject: [PATCH] ci: Run full matrix only on `Ready to merge` or `Full CI build` labeled PRs (#7689) Co-authored-by: Bart --- .github/scripts/strategy-matrix/generate.py | 62 +++++++------------ .github/scripts/strategy-matrix/linux.json | 31 ++++++++-- .github/scripts/strategy-matrix/macos.json | 5 +- .github/scripts/strategy-matrix/windows.json | 4 +- .github/workflows/on-pr.yml | 55 +++++++++++++--- .../workflows/reusable-strategy-matrix.yml | 7 ++- 6 files changed, 104 insertions(+), 60 deletions(-) diff --git a/.github/scripts/strategy-matrix/generate.py b/.github/scripts/strategy-matrix/generate.py index a269cb25d4..c783f32fb7 100755 --- a/.github/scripts/strategy-matrix/generate.py +++ b/.github/scripts/strategy-matrix/generate.py @@ -25,24 +25,16 @@ def get_cmake_args(build_type: str, extra_args: str) -> str: return " ".join(args) -def runs_on_event(exclude_event_types: list[str], event: str | None) -> bool: - """Whether a config should run for the current event. - - 'exclude_event_types' is a list of GitHub event names (e.g. - ["pull_request"]) on which the config should NOT run; an empty list means - the config runs on every event. When no event is given (event is None), no - filtering is applied. - """ - if event is None: - return True - return event not in exclude_event_types - - # --------------------------------------------------------------------------- # Input types — shapes of the JSON config files # --------------------------------------------------------------------------- +# Every config must declare 'minimal'. Minimal configs form the reduced matrix +# built for pull requests by default; the full matrix adds the rest. Packaging +# configs declare it too, but packaging is gated in the workflow, not by it. + + @dataclasses.dataclass class LinuxConfig: """One entry in linux.json's 'configs' or 'package_configs' arrays.""" @@ -50,13 +42,11 @@ class LinuxConfig: compiler: list[str] build_type: list[str] arch: list[str] + minimal: bool sanitizers: list[str] = dataclasses.field(default_factory=list) suffix: str = "" extra_cmake_args: str = "" image: str = "" # only used by package_configs entries - # List of GitHub event names (e.g. "pull_request") on which this config - # should NOT run. Empty means it runs on every event. - exclude_event_types: list[str] = dataclasses.field(default_factory=list) @dataclasses.dataclass @@ -89,11 +79,9 @@ class PlatformConfig: """One entry in macos.json's or windows.json's 'configs' array.""" build_type: list[str] + minimal: bool build_only: bool = False # if true, skip tests (e.g. macos/Windows Debug) extra_cmake_args: str = "" - # List of GitHub event names (e.g. "pull_request") on which this config - # should NOT run. Empty means it runs on every event. - exclude_event_types: list[str] = dataclasses.field(default_factory=list) def __post_init__(self) -> None: if isinstance(self.build_type, str): @@ -168,20 +156,18 @@ _ARCHS: dict[str, Architecture] = { } -def expand_linux_matrix( - linux: LinuxFile, event: str | None = None -) -> list[MatrixEntry]: +def expand_linux_matrix(linux: LinuxFile, minimal: bool) -> list[MatrixEntry]: """Expand a LinuxFile into a flat list of matrix entries. Each config entry is expanded over the cross-product of its - compiler, build_type, sanitizers, and architecture lists. Configs that - exclude the current event are skipped. + compiler, build_type, sanitizers, and architecture lists. When 'minimal' is + true, only configs flagged as minimal are included. """ entries: list[MatrixEntry] = [] for distro, configs in linux.configs.items(): for cfg in configs: - if not runs_on_event(cfg.exclude_event_types, event): + if minimal and not cfg.minimal: continue # An empty sanitizers list means "one entry with no sanitizer". effective_sanitizers = cfg.sanitizers or [""] @@ -240,19 +226,17 @@ def expand_linux_packaging(linux: LinuxFile) -> list[PackagingEntry]: return entries -def expand_platform_matrix( - pf: PlatformFile, event: str | None = None -) -> list[MatrixEntry]: +def expand_platform_matrix(pf: PlatformFile, minimal: bool) -> list[MatrixEntry]: """Expand a PlatformFile (macOS or Windows) into matrix entries. - Configs that exclude the current event are skipped. + When 'minimal' is true, only configs flagged as minimal are included. """ platform_name, arch = pf.platform.split("/") is_windows = platform_name == "windows" entries: list[MatrixEntry] = [] for cfg in pf.configs: - if not runs_on_event(cfg.exclude_event_types, event): + if minimal and not cfg.minimal: continue for build_type in cfg.build_type: entries.append( @@ -292,12 +276,12 @@ if __name__ == "__main__": action="store_true", ) parser.add_argument( - "-e", - "--event", - help="The GitHub event name that triggered the workflow (e.g. 'push', " - "'pull_request'). Configs are filtered by their 'event_type'. If " - "omitted, no filtering is applied.", - default=None, + "-m", + "--minimal", + help="Emit only the minimal matrix (the configs flagged 'minimal'), " + "used for pull requests by default. If omitted, the full matrix is " + "emitted.", + action="store_true", ) args = parser.parse_args() @@ -308,15 +292,15 @@ if __name__ == "__main__": else: if args.config in ("linux", None): matrix += expand_linux_matrix( - LinuxFile.load(THIS_DIR / "linux.json"), args.event + LinuxFile.load(THIS_DIR / "linux.json"), args.minimal ) if args.config in ("macos", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "macos.json"), args.event + PlatformFile.load(THIS_DIR / "macos.json"), args.minimal ) if args.config in ("windows", None): matrix += expand_platform_matrix( - PlatformFile.load(THIS_DIR / "windows.json"), args.event + PlatformFile.load(THIS_DIR / "windows.json"), args.minimal ) print(f"matrix={json.dumps({'include': [dataclasses.asdict(e) for e in matrix]})}") diff --git a/.github/scripts/strategy-matrix/linux.json b/.github/scripts/strategy-matrix/linux.json index 863b910dda..03ac1c6334 100644 --- a/.github/scripts/strategy-matrix/linux.json +++ b/.github/scripts/strategy-matrix/linux.json @@ -2,16 +2,30 @@ "image_tag": "sha-e29b523", "configs": { "ubuntu": [ + { + "compiler": ["clang"], + "build_type": ["Release"], + "arch": ["amd64"], + "minimal": true + }, + { + "compiler": ["gcc"], + "build_type": ["Release"], + "arch": ["amd64"], + "minimal": false + }, { "compiler": ["gcc", "clang"], "build_type": ["Debug", "Release"], - "arch": ["amd64", "arm64"] + "arch": ["arm64"], + "minimal": false }, { "compiler": ["gcc", "clang"], "build_type": ["Debug", "Release"], "arch": ["amd64"], + "minimal": false, "sanitizers": ["address", "undefinedbehavior"] }, @@ -19,6 +33,7 @@ "compiler": ["gcc"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": true, "suffix": "coverage", "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=500 -Dcoverage=ON -Dcoverage_format=xml -DCODE_COVERAGE_VERBOSE=ON -DCMAKE_C_FLAGS=-O0 -DCMAKE_CXX_FLAGS=-O0" }, @@ -26,6 +41,7 @@ "compiler": ["clang"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": false, "suffix": "voidstar", "extra_cmake_args": "-Dvoidstar=ON" }, @@ -33,6 +49,7 @@ "compiler": ["clang"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "suffix": "reffee", "extra_cmake_args": "-DUNIT_TEST_REFERENCE_FEE=1000" }, @@ -40,9 +57,9 @@ "compiler": ["gcc"], "build_type": ["Debug"], "arch": ["amd64"], + "minimal": false, "suffix": "unity", - "extra_cmake_args": "-Dunity=ON", - "exclude_event_types": ["pull_request"] + "extra_cmake_args": "-Dunity=ON" } ], @@ -50,7 +67,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "minimal": false } ], @@ -58,7 +76,8 @@ { "compiler": ["gcc"], "build_type": ["Release"], - "arch": ["amd64"] + "arch": ["amd64"], + "minimal": false } ] }, @@ -68,6 +87,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "image": "ghcr.io/xrplf/xrpld/packaging-debian:sha-577d745" } ], @@ -77,6 +97,7 @@ "compiler": ["gcc"], "build_type": ["Release"], "arch": ["amd64"], + "minimal": false, "image": "ghcr.io/xrplf/xrpld/packaging-rhel:sha-577d745" } ] diff --git a/.github/scripts/strategy-matrix/macos.json b/.github/scripts/strategy-matrix/macos.json index 2d3cc75c7b..98e0f13141 100644 --- a/.github/scripts/strategy-matrix/macos.json +++ b/.github/scripts/strategy-matrix/macos.json @@ -4,13 +4,14 @@ "configs": [ { "build_type": "Release", - "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5" + "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", + "minimal": true }, { "build_type": "Debug", "extra_cmake_args": "-DCMAKE_POLICY_VERSION_MINIMUM=3.5", "build_only": true, - "exclude_event_types": ["pull_request"] + "minimal": false } ] } diff --git a/.github/scripts/strategy-matrix/windows.json b/.github/scripts/strategy-matrix/windows.json index 370e9f5bc7..6b926e85f5 100644 --- a/.github/scripts/strategy-matrix/windows.json +++ b/.github/scripts/strategy-matrix/windows.json @@ -2,11 +2,11 @@ "platform": "windows/amd64", "runner": ["self-hosted", "Windows", "dev-box-windows-2026"], "configs": [ - { "build_type": "Release" }, + { "build_type": "Release", "minimal": true }, { "build_type": "Debug", "build_only": true, - "exclude_event_types": ["pull_request"] + "minimal": false } ] } diff --git a/.github/workflows/on-pr.yml b/.github/workflows/on-pr.yml index 19fb170b92..bbf6f8c39e 100644 --- a/.github/workflows/on-pr.yml +++ b/.github/workflows/on-pr.yml @@ -1,7 +1,11 @@ -# This workflow runs all workflows to check, build and test the project on -# various Linux flavors, as well as on MacOS and Windows, on every push to a -# user branch. However, it will not run if the pull request is a draft unless it -# has the 'DraftRunCI' label. For commits to PRs that target a release branch, +# This workflow runs workflows to check, build and test the project +# on every meaningful change on pull_request. +# However, it will not run if the PR is a draft +# unless it has the 'DraftRunCI' or 'Full CI build' label. +# +# By default a PR builds only a minimal matrix. +# The full matrix runs once the PR is labeled "Ready to merge" or "Full CI build". +# For commits to PRs that target a release branch, # it also uploads the libxrpl recipe to the Conan remote. name: PR @@ -15,9 +19,24 @@ on: - reopened - synchronize - ready_for_review + # Trigger on label changes so toggling "Ready to merge" or "Full CI build" + # switches between the minimal and full matrix without needing a new push. + - labeled + - unlabeled concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + # Use a per-ref group so a newer run (a push, or a change to a label below) + # supersedes the in-progress one for that ref. Label events we don't act on get + # their own unique group (per run id) instead, keeping them out of the shared + # group so real builds keep running. Keep this list in sync with `should-run`. + group: >- + ${{ github.workflow }}-${{ github.ref }}${{ + ((github.event.action == 'labeled' || github.event.action == 'unlabeled') + && github.event.label.name != 'Ready to merge' + && github.event.label.name != 'DraftRunCI' + && github.event.label.name != 'Full CI build') + && format('-{0}', github.run_id) || '' + }} cancel-in-progress: true defaults: @@ -26,10 +45,21 @@ defaults: jobs: # This job determines whether the rest of the workflow should run. It runs - # when the PR is not a draft (which should also cover merge-group) or - # has the 'DraftRunCI' label. + # when the PR is not a draft (which should also cover merge-group) or has the + # 'DraftRunCI' or 'Full CI build' label. For label events it only runs when the + # label added or removed is one we act on ('Ready to merge', 'DraftRunCI' or + # 'Full CI build'), so unrelated label changes do not trigger a redundant run. should-run: - if: ${{ !github.event.pull_request.draft || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') }} + if: >- + ${{ + ((github.event.action != 'labeled' && github.event.action != 'unlabeled') + || github.event.label.name == 'Ready to merge' + || github.event.label.name == 'DraftRunCI' + || github.event.label.name == 'Full CI build') + && (!github.event.pull_request.draft + || contains(github.event.pull_request.labels.*.name, 'DraftRunCI') + || contains(github.event.pull_request.labels.*.name, 'Full CI build')) + }} runs-on: ubuntu-latest steps: - name: Checkout repository @@ -91,15 +121,17 @@ jobs: # least one of: # * Any of the files checked in the `changes` step were modified # * The PR is NOT a draft and is labeled "Ready to merge" + # * The PR is labeled "Full CI build" (draft or not) # * The workflow is running from the merge queue id: go env: FILES: ${{ steps.changes.outputs.any_changed }} DRAFT: ${{ github.event.pull_request.draft }} READY: ${{ contains(github.event.pull_request.labels.*.name, 'Ready to merge') }} + FULL: ${{ contains(github.event.pull_request.labels.*.name, 'Full CI build') }} MERGE: ${{ github.event_name == 'merge_group' }} run: | - echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}" + echo "go=${{ (env.DRAFT != 'true' && env.READY == 'true') || env.FULL == 'true' || env.FILES == 'true' || env.MERGE == 'true' }}" >>"${GITHUB_OUTPUT}" cat "${GITHUB_OUTPUT}" outputs: go: ${{ steps.go.outputs.go == 'true' }} @@ -142,7 +174,10 @@ jobs: package: needs: [should-run, build-test] - if: ${{ needs.should-run.outputs.go == 'true' }} + # Packaging consumes the debian/rhel release binaries, which are only built + # by the full matrix. Skip it for pull requests that ran only the minimal + # matrix (i.e. not yet labeled "Ready to merge" or "Full CI build"). + if: ${{ needs.should-run.outputs.go == 'true' && (github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'Ready to merge') || contains(github.event.pull_request.labels.*.name, 'Full CI build')) }} uses: ./.github/workflows/reusable-package.yml upload-recipe: diff --git a/.github/workflows/reusable-strategy-matrix.yml b/.github/workflows/reusable-strategy-matrix.yml index 690aa3d423..b6091b99d9 100644 --- a/.github/workflows/reusable-strategy-matrix.yml +++ b/.github/workflows/reusable-strategy-matrix.yml @@ -35,5 +35,8 @@ jobs: id: generate env: GENERATE_CONFIG: ${{ inputs.os != '' && format('--config={0}', inputs.os) || '' }} - GENERATE_EVENT: ${{ github.event_name }} - run: ./generate.py ${GENERATE_CONFIG} --event="${GENERATE_EVENT}" >>"${GITHUB_OUTPUT}" + # Run only the minimal matrix for pull requests that are not yet + # labeled "Ready to merge" or "Full CI build". Any other event (merge + # queue, push, schedule, manual dispatch) runs the full matrix. + GENERATE_MINIMAL: ${{ (github.event_name == 'pull_request' && !contains(github.event.pull_request.labels.*.name, 'Ready to merge') && !contains(github.event.pull_request.labels.*.name, 'Full CI build')) && '--minimal' || '' }} + run: ./generate.py ${GENERATE_CONFIG} ${GENERATE_MINIMAL} >>"${GITHUB_OUTPUT}"