diff --git a/.github/intentional-ci.md b/.github/intentional-ci.md new file mode 100644 index 0000000000..cab0d597f6 --- /dev/null +++ b/.github/intentional-ci.md @@ -0,0 +1,43 @@ +# Intentional CI development pilot + +The `ci:intentional` label opts a PR out of automatic GitHub Actions test jobs, +independently of draft or ready-for-review state. Other PRs retain their current +behavior. Scheduled, branch-push, release, and existing manual runs retain their +current conditions. Skipped workflow entries can still appear in GitHub. + +Apply the label before pushing updates. Adding it does not cancel existing runs; +automatic labeling after PR creation can be too late to suppress the first run. +Existing stack branches may need rebasing onto the bootstrap before their +PR workflows contain the opt-out guards. AppVeyor and Read the Docs are separate +services and do not honor this GitHub Actions opt-out. + +## Experimental commands + +After this bootstrap merges, create `intentional-ci-experiment` in +`pybind/pybind11` from `master`. Implement the worker there, retaining +`.github/workflows/intentional-ci.yml` and its `workflow_dispatch` string input +named `request`. The bootstrap worker only reports that no tests were executed; +it does not implement either test profile or publish a CI result for the PR. + +Any PR author is welcome to join the pilot. On their own open PR labeled +`ci:intentional`, they can post: + +* `/ci full` +* `/ci quick` +* `/ci help` (usage appears in the listener's Actions job summary) + +The listener runs trusted code from `master`, fetches the current PR state, and +sends the PR number, profile, head SHA, base SHA, and requesting comment ID to +the fixed development branch. Each command requests one captured revision. +Draft status and commit messages do not select the profile. A missing worker +branch produces an actionable failure in the listener; no PR tests run. + +Keep experimental workflow/check names, concurrency groups, and cache keys +separate from ordinary CI. Test the captured revisions with read-only +permissions and no secrets; keep result publishing in a separate trusted job. +The listener itself never checks out PR code. It accepts requests only from +the PR author; the label is the explicit opt-in to the evolving experiment. + +Implementation changes can stay on the development branch while the listener +and ordinary workflows on `master` remain stable. Submit the completed design +and any proposed project-wide rollout together for review. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60e6fd5c52..e18f840a38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,9 @@ jobs: # versions of default compilers and Python versions in GitHub Actions. # It is in two parts: one that always runs, and one that runs on non-draft standard-small: - if: github.event.action != 'ready_for_review' + if: >- + (github.event.action != 'ready_for_review') && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -85,7 +87,9 @@ jobs: # Build the test suite against the precompiled pybind11 static library; a # definition missing from the -inl.h split shows up here as a link error. precompile: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -108,7 +112,9 @@ jobs: cmake-args: -DPYBIND11_TEST_PRECOMPILE=ON standard-large: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -195,7 +201,9 @@ jobs: # This checks inplace builds with C++11 inplace: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -271,7 +279,9 @@ jobs: manylinux: name: Manylinux on 🐍 ${{ matrix.python-version }} (${{ matrix.container }}) - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -305,7 +315,9 @@ jobs: # Testing on clang using the excellent silkeh clang docker images clang: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -377,6 +389,7 @@ jobs: # Testing NVCC; forces sources to behave like .cu files cuda: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest name: "🐍 3.10 • CUDA 12.2 • Ubuntu 22.04" container: nvidia/cuda:12.2.0-devel-ubuntu22.04 @@ -445,7 +458,9 @@ jobs: # Testing on Ubuntu + NVHPC (previous PGI) compilers, which seems to require more workarounds ubuntu-nvhpc: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-24.04 name: "🐍 3 • NVHPC 26.5 • C++17 • x64" timeout-minutes: 90 @@ -509,7 +524,9 @@ jobs: # Testing on GCC using the GCC docker images (only recent images supported) gcc: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -584,7 +601,9 @@ jobs: # Testing on ICC using the oneAPI apt repo icc: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-22.04 timeout-minutes: 90 @@ -692,7 +711,9 @@ jobs: # Testing on CentOS (manylinux uses a centos base). centos: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -759,7 +780,9 @@ jobs: # This tests an "install" with the CMake tools install-classic: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: "🐍 3.11 • Debian • x86 • Install" # Ubuntu 26.04's Docker 29.4.2 breaks i386 networking: # https://github.com/actions/runner-images/issues/14790 @@ -807,7 +830,9 @@ jobs: # This verifies that the documentation is not horribly broken, and does a # basic validation check on the SDist. doxygen: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: "Documentation build test" runs-on: ubuntu-latest timeout-minutes: 90 @@ -843,7 +868,9 @@ jobs: diff -rq $installed ./pybind11 win32: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -896,7 +923,9 @@ jobs: run: cmake --build build -t pytest win32-debug: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -946,7 +975,9 @@ jobs: windows-2022: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -1011,7 +1042,9 @@ jobs: run: cmake --build build_partial --target pytest mingw: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: "🐍 3 • windows-latest • ${{ matrix.sys }}" runs-on: windows-latest timeout-minutes: 90 @@ -1123,7 +1156,9 @@ jobs: run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cross_module_rtti windows_clang: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: matrix: @@ -1196,7 +1231,9 @@ jobs: # Clang with MSVC/Windows SDK toolchain + python.org CPython (Windows ARM) windows_arm_clang_msvc: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false @@ -1264,7 +1301,9 @@ jobs: # Clang in MSYS2/MinGW-w64 CLANGARM64 toolchain + MSYS2 Python (Windows ARM) windows_arm_clang_msys2: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index bb871c8f1b..ba1cb35bc8 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -21,7 +21,9 @@ jobs: # This tests various versions of CMake in various combinations, to make sure # the configure step passes. cmake: - if: github.event.pull_request.draft == false + if: >- + (github.event.pull_request.draft == false) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index beee9ad846..76d64ad632 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -22,6 +22,7 @@ env: jobs: pre-commit: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: Format runs-on: ubuntu-latest steps: @@ -34,6 +35,7 @@ jobs: - uses: pre-commit/action@v3.0.1 clang-tidy: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) # When making changes here, please also review the "Clang-Tidy" section # in .github/CONTRIBUTING.md and update as needed. name: Clang-Tidy diff --git a/.github/workflows/intentional-ci.yml b/.github/workflows/intentional-ci.yml new file mode 100644 index 0000000000..f6a5a08aa2 --- /dev/null +++ b/.github/workflows/intentional-ci.yml @@ -0,0 +1,86 @@ +name: Intentional CI experiment + +on: + issue_comment: + types: [created] + workflow_dispatch: + inputs: + request: + description: JSON with the PR, profile, head SHA, base SHA, and comment ID + required: true + type: string + +permissions: {} + +jobs: + dispatch: + if: >- + github.repository == 'pybind/pybind11' && + github.event_name == 'issue_comment' && + github.event.issue.pull_request && + startsWith(github.event.comment.body, '/ci ') + runs-on: ubuntu-latest + permissions: + actions: write + pull-requests: read + steps: + - uses: actions/github-script@v9 + with: + script: | + const match = /^\/ci (full|quick|help)$/.exec(context.payload.comment.body.trim()); + if (!match) return; + + const {data: pr} = await github.rest.pulls.get({ + ...context.repo, + pull_number: context.issue.number, + }); + if (pr.state !== 'open' || pr.user.login !== context.payload.comment.user.login + || !pr.labels.some(label => label.name === 'ci:intentional')) return; + + if (match[1] === 'help') { + await core.summary.addHeading('Intentional CI experiment') + .addRaw('Use `/ci full` or `/ci quick` on a PR labeled `ci:intentional`. ' + + 'PR authors can request runs on their own PRs during this pilot. ' + + 'The development worker must be installed first; see ' + + '`.github/intentional-ci.md`.').write(); + return; + } + + const request = { + pr: pr.number, + profile: match[1], + head_sha: pr.head.sha, + base_sha: pr.base.sha, + comment_id: context.payload.comment.id, + }; + try { + await github.rest.actions.createWorkflowDispatch({ + ...context.repo, + workflow_id: 'intentional-ci.yml', + ref: 'intentional-ci-experiment', + inputs: {request: JSON.stringify(request)}, + }); + } catch (error) { + if (error.status !== 404 && error.status !== 422) throw error; + core.setFailed('Cannot dispatch the experimental worker. Create ' + + 'intentional-ci-experiment from master after this bootstrap merges ' + + 'and keep intentional-ci.yml with its workflow_dispatch input. ' + + error.message); + return; + } + await core.summary.addHeading('Experimental CI requested') + .addCodeBlock(JSON.stringify(request, null, 2), 'json').write(); + + bootstrap: + if: github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + steps: + - uses: actions/github-script@v9 + env: + REQUEST: ${{ inputs.request }} + with: + script: | + await core.summary.addHeading('Bootstrap only: no tests executed') + .addRaw('Implement the dispatch worker on `intentional-ci-experiment`. ' + + 'This placeholder does not publish a CI result for the PR.') + .addCodeBlock(process.env.REQUEST, 'json').write(); diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index 329247da5c..1038fa3aba 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,6 +19,7 @@ jobs: # This builds the sdists and wheels and makes sure the files are exactly as # expected. test-packaging: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: 🐍 3.9 • 📦 tests • windows-latest runs-on: windows-latest @@ -43,6 +44,7 @@ jobs: # This runs the packaging tests and also builds and saves the packages as # artifacts. packaging: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: 🐍 3.9 • 📦 & 📦 tests • ubuntu-24.04 runs-on: ubuntu-24.04 diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml index a845a4c157..35ee6c568e 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -17,6 +17,7 @@ concurrency: jobs: build-wasm-emscripten: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: Pyodide wheel runs-on: ubuntu-latest steps: @@ -33,6 +34,7 @@ jobs: only: cp314-pyodide_wasm32 build-ios: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: iOS wheel ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }} strategy: @@ -55,6 +57,7 @@ jobs: package-dir: tests build-android: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: Android wheel ${{ matrix.runs-on }} runs-on: ${{ matrix.runs-on }} strategy: diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml index 7ad8c99014..ea24ffa642 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -21,7 +21,9 @@ jobs: name: "🐍 3.15 latest • ubuntu-latest • x64" runs-on: ubuntu-latest # Only runs when the 'python dev' label is selected - if: "contains(github.event.pull_request.labels.*.name, 'python dev')" + if: >- + (contains(github.event.pull_request.labels.*.name, 'python dev')) && + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) steps: - uses: actions/checkout@v7