From 5deafdbbb1e39cf4c7159f136e6738f4d77bd094 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 14:52:37 -0700 Subject: [PATCH 1/6] ci: bootstrap opt-in intentional CI development --- .github/intentional-ci.md | 48 +++++++++++++++ .github/tests/intentional-ci.test.cjs | 86 ++++++++++++++++++++++++++ .github/workflows/ci.yml | 77 ++++++++++++++++++------ .github/workflows/configure.yml | 4 +- .github/workflows/format.yml | 5 ++ .github/workflows/intentional-ci.yml | 87 +++++++++++++++++++++++++++ .github/workflows/pip.yml | 2 + .github/workflows/tests-cibw.yml | 3 + .github/workflows/upstream.yml | 4 +- 9 files changed, 295 insertions(+), 21 deletions(-) create mode 100644 .github/intentional-ci.md create mode 100644 .github/tests/intentional-ci.test.cjs create mode 100644 .github/workflows/intentional-ci.yml diff --git a/.github/intentional-ci.md b/.github/intentional-ci.md new file mode 100644 index 0000000000..36f69028bb --- /dev/null +++ b/.github/intentional-ci.md @@ -0,0 +1,48 @@ +# Intentional CI development pilot + +The `ci:manual` 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 `rwgk/intentional-ci-dev` 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. + +On an open PR authored by `rwgk` and labeled `ci:manual`, `rwgk` 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. Extend pilot participation only +by explicitly updating its requester and PR-author checks. + +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. + +Run the listener regression tests locally with: + +```sh +node --test .github/tests/intentional-ci.test.cjs +``` diff --git a/.github/tests/intentional-ci.test.cjs b/.github/tests/intentional-ci.test.cjs new file mode 100644 index 0000000000..ffead1e4e1 --- /dev/null +++ b/.github/tests/intentional-ci.test.cjs @@ -0,0 +1,86 @@ +const assert = require('node:assert/strict'); +const fs = require('node:fs'); +const path = require('node:path'); +const {test} = require('node:test'); + +// Exercise the actual inline github-script without checking out or running PR code. +const workflow = fs.readFileSync(path.join(__dirname, '../workflows/intentional-ci.yml'), 'utf8'); +const block = workflow.match(/ script: \|\n((?: .*\n|\n)+)/)[1]; +const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; +const dispatch = new AsyncFunction('github', 'context', 'core', + block.split('\n').map(line => line.slice(12)).join('\n')); + +async function run({command = '/ci full', pr = {}, status} = {}) { + const requests = []; + const failures = []; + const summaries = []; + const summary = { + addHeading(value) { summaries.push(value); return this; }, + addRaw() { return this; }, + addCodeBlock() { return this; }, + async write() {}, + }; + const github = {rest: { + pulls: {async get() { return {data: { + number: 123, state: 'open', user: {login: 'rwgk'}, + labels: [{name: 'ci:manual'}], + head: {sha: '1'.repeat(40)}, base: {sha: '2'.repeat(40)}, ...pr, + }}; }}, + actions: {async createWorkflowDispatch(request) { + if (status) throw Object.assign(new Error('API failure'), {status}); + requests.push(request); + }}, + }}; + const context = { + repo: {owner: 'pybind', repo: 'pybind11'}, issue: {number: 123}, + payload: {comment: {body: command, id: 456}}, + }; + await dispatch(github, context, {summary, setFailed(message) { failures.push(message); }}); + return {requests, failures, summaries}; +} + +for (const profile of ['full', 'quick']) { + test(`dispatch ${profile} with frozen revisions to the fixed worker`, async () => { + const {requests} = await run({command: `/ci ${profile}`}); + assert.equal(requests.length, 1); + const {inputs, ...destination} = requests[0]; + assert.deepEqual(destination, { + owner: 'pybind', repo: 'pybind11', workflow_id: 'intentional-ci.yml', + ref: 'rwgk/intentional-ci-dev', + }); + assert.deepEqual(JSON.parse(inputs.request), { + pr: 123, profile, head_sha: '1'.repeat(40), base_sha: '2'.repeat(40), comment_id: 456, + }); + }); +} + +for (const command of ['/ci', '/ci all', '/ci full extra', '> /ci full', '/ci full\nplease', '/ci full; echo unsafe']) { + test(`ignore unrecognized command ${JSON.stringify(command)}`, async () => { + assert.deepEqual((await run({command})).requests, []); + }); +} + +for (const pr of [{state: 'closed'}, {user: {login: 'someone-else'}}, {labels: []}]) { + test(`ignore PR outside the pilot: ${JSON.stringify(pr)}`, async () => { + assert.deepEqual((await run({pr})).requests, []); + }); +} + +test('help explains the pilot without dispatching', async () => { + const result = await run({command: '/ci help'}); + assert.deepEqual(result.requests, []); + assert.deepEqual(result.summaries, ['Intentional CI experiment']); +}); + +for (const status of [404, 422]) { + test(`missing or incompatible worker fails clearly (${status})`, async () => { + const result = await run({status}); + assert.deepEqual(result.requests, []); + assert.match(result.failures[0], /rwgk\/intentional-ci-dev/); + assert.deepEqual(result.summaries, []); + }); +} + +test('unexpected API errors propagate', async () => { + await assert.rejects(run({status: 503}), /API failure/); +}); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 60e6fd5c52..9f5bc2379f 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) 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:manual')) strategy: fail-fast: false diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml index bb871c8f1b..a3a0172c8d 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:manual')) strategy: fail-fast: false matrix: diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index beee9ad846..b87b795df7 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:manual')) name: Format runs-on: ubuntu-latest steps: @@ -33,7 +34,11 @@ jobs: run: echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" - uses: pre-commit/action@v3.0.1 + - name: Test intentional CI bootstrap + run: node --test .github/tests/intentional-ci.test.cjs + clang-tidy: + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) # 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..4a1a7909d4 --- /dev/null +++ b/.github/workflows/intentional-ci.yml @@ -0,0 +1,87 @@ +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 && + github.event.comment.user.login == 'rwgk' && + 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 !== 'rwgk' + || !pr.labels.some(label => label.name === 'ci:manual')) return; + + if (match[1] === 'help') { + await core.summary.addHeading('Intentional CI experiment') + .addRaw('Use `/ci full` or `/ci quick` on a PR labeled `ci:manual`. ' + + 'Only rwgk 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: 'rwgk/intentional-ci-dev', + inputs: {request: JSON.stringify(request)}, + }); + } catch (error) { + if (error.status !== 404 && error.status !== 422) throw error; + core.setFailed('Cannot dispatch the experimental worker. Create ' + + 'rwgk/intentional-ci-dev 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 `rwgk/intentional-ci-dev`. ' + + '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..eb6f0b356a 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:manual')) 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:manual')) 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..48dfccd22f 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:manual')) 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:manual')) 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:manual')) 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..27680bafe2 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:manual')) steps: - uses: actions/checkout@v7 From beed2bb7b178b1ea6d16a6ba80bf6a887cfd9a94 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 14:52:37 -0700 Subject: [PATCH 2/6] chore: suppress bootstrap CI during initial review [skip ci] From f4828ef45612398bca2bb696def5997aea3c721b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 15:35:52 -0700 Subject: [PATCH 3/6] ci: open intentional CI experiment to PR authors --- .github/intentional-ci.md | 9 +++++---- .github/tests/intentional-ci.test.cjs | 18 ++++++++++++++---- .github/workflows/intentional-ci.yml | 11 +++++------ 3 files changed, 24 insertions(+), 14 deletions(-) diff --git a/.github/intentional-ci.md b/.github/intentional-ci.md index 36f69028bb..34cacfd72a 100644 --- a/.github/intentional-ci.md +++ b/.github/intentional-ci.md @@ -13,13 +13,14 @@ services and do not honor this GitHub Actions opt-out. ## Experimental commands -After this bootstrap merges, create `rwgk/intentional-ci-dev` in +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. -On an open PR authored by `rwgk` and labeled `ci:manual`, `rwgk` can post: +Any PR author is welcome to join the pilot. On their own open PR labeled +`ci:manual`, they can post: * `/ci full` * `/ci quick` @@ -34,8 +35,8 @@ 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. Extend pilot participation only -by explicitly updating its requester and PR-author checks. +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 diff --git a/.github/tests/intentional-ci.test.cjs b/.github/tests/intentional-ci.test.cjs index ffead1e4e1..d7041dc7db 100644 --- a/.github/tests/intentional-ci.test.cjs +++ b/.github/tests/intentional-ci.test.cjs @@ -10,7 +10,7 @@ const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; const dispatch = new AsyncFunction('github', 'context', 'core', block.split('\n').map(line => line.slice(12)).join('\n')); -async function run({command = '/ci full', pr = {}, status} = {}) { +async function run({command = '/ci full', pr = {}, commenter = 'rwgk', status} = {}) { const requests = []; const failures = []; const summaries = []; @@ -33,7 +33,7 @@ async function run({command = '/ci full', pr = {}, status} = {}) { }}; const context = { repo: {owner: 'pybind', repo: 'pybind11'}, issue: {number: 123}, - payload: {comment: {body: command, id: 456}}, + payload: {comment: {body: command, id: 456, user: {login: commenter}}}, }; await dispatch(github, context, {summary, setFailed(message) { failures.push(message); }}); return {requests, failures, summaries}; @@ -46,7 +46,7 @@ for (const profile of ['full', 'quick']) { const {inputs, ...destination} = requests[0]; assert.deepEqual(destination, { owner: 'pybind', repo: 'pybind11', workflow_id: 'intentional-ci.yml', - ref: 'rwgk/intentional-ci-dev', + ref: 'intentional-ci-experiment', }); assert.deepEqual(JSON.parse(inputs.request), { pr: 123, profile, head_sha: '1'.repeat(40), base_sha: '2'.repeat(40), comment_id: 456, @@ -66,6 +66,16 @@ for (const pr of [{state: 'closed'}, {user: {login: 'someone-else'}}, {labels: [ }); } +test('other PR authors can request CI on their own opted-in PRs', async () => { + const result = await run({commenter: 'contributor', pr: {user: {login: 'contributor'}}}); + assert.equal(result.requests.length, 1); + assert.deepEqual(result.failures, []); +}); + +test('reject requests from someone other than the PR author', async () => { + assert.deepEqual((await run({commenter: 'someone-else'})).requests, []); +}); + test('help explains the pilot without dispatching', async () => { const result = await run({command: '/ci help'}); assert.deepEqual(result.requests, []); @@ -76,7 +86,7 @@ for (const status of [404, 422]) { test(`missing or incompatible worker fails clearly (${status})`, async () => { const result = await run({status}); assert.deepEqual(result.requests, []); - assert.match(result.failures[0], /rwgk\/intentional-ci-dev/); + assert.match(result.failures[0], /intentional-ci-experiment/); assert.deepEqual(result.summaries, []); }); } diff --git a/.github/workflows/intentional-ci.yml b/.github/workflows/intentional-ci.yml index 4a1a7909d4..b93071b607 100644 --- a/.github/workflows/intentional-ci.yml +++ b/.github/workflows/intentional-ci.yml @@ -18,7 +18,6 @@ jobs: github.repository == 'pybind/pybind11' && github.event_name == 'issue_comment' && github.event.issue.pull_request && - github.event.comment.user.login == 'rwgk' && startsWith(github.event.comment.body, '/ci ') runs-on: ubuntu-latest permissions: @@ -35,13 +34,13 @@ jobs: ...context.repo, pull_number: context.issue.number, }); - if (pr.state !== 'open' || pr.user.login !== 'rwgk' + if (pr.state !== 'open' || pr.user.login !== context.payload.comment.user.login || !pr.labels.some(label => label.name === 'ci:manual')) return; if (match[1] === 'help') { await core.summary.addHeading('Intentional CI experiment') .addRaw('Use `/ci full` or `/ci quick` on a PR labeled `ci:manual`. ' - + 'Only rwgk can request runs on their own PRs during this pilot. ' + + '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; @@ -58,13 +57,13 @@ jobs: await github.rest.actions.createWorkflowDispatch({ ...context.repo, workflow_id: 'intentional-ci.yml', - ref: 'rwgk/intentional-ci-dev', + 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 ' - + 'rwgk/intentional-ci-dev from master after this bootstrap merges ' + + 'intentional-ci-experiment from master after this bootstrap merges ' + 'and keep intentional-ci.yml with its workflow_dispatch input. ' + error.message); return; @@ -82,6 +81,6 @@ jobs: with: script: | await core.summary.addHeading('Bootstrap only: no tests executed') - .addRaw('Implement the dispatch worker on `rwgk/intentional-ci-dev`. ' + .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(); From db97ab0875623c570c3b2110698be9d3a5404b4f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 15:35:52 -0700 Subject: [PATCH 4/6] chore: suppress updated bootstrap CI during review [skip ci] From 5b911f3ce94020eb19d674d65a82765eecae3f41 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 15:45:55 -0700 Subject: [PATCH 5/6] ci: use ci:intentional for the intentional CI pilot --- .github/intentional-ci.md | 4 +-- .github/tests/intentional-ci.test.cjs | 4 +-- .github/workflows/ci.yml | 40 +++++++++++++-------------- .github/workflows/configure.yml | 2 +- .github/workflows/format.yml | 4 +-- .github/workflows/intentional-ci.yml | 4 +-- .github/workflows/pip.yml | 4 +-- .github/workflows/tests-cibw.yml | 6 ++-- .github/workflows/upstream.yml | 2 +- 9 files changed, 35 insertions(+), 35 deletions(-) diff --git a/.github/intentional-ci.md b/.github/intentional-ci.md index 34cacfd72a..69fe20f5b1 100644 --- a/.github/intentional-ci.md +++ b/.github/intentional-ci.md @@ -1,6 +1,6 @@ # Intentional CI development pilot -The `ci:manual` label opts a PR out of automatic GitHub Actions test jobs, +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. @@ -20,7 +20,7 @@ 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:manual`, they can post: +`ci:intentional`, they can post: * `/ci full` * `/ci quick` diff --git a/.github/tests/intentional-ci.test.cjs b/.github/tests/intentional-ci.test.cjs index d7041dc7db..0f4d2873c9 100644 --- a/.github/tests/intentional-ci.test.cjs +++ b/.github/tests/intentional-ci.test.cjs @@ -23,7 +23,7 @@ async function run({command = '/ci full', pr = {}, commenter = 'rwgk', status} = const github = {rest: { pulls: {async get() { return {data: { number: 123, state: 'open', user: {login: 'rwgk'}, - labels: [{name: 'ci:manual'}], + labels: [{name: 'ci:intentional'}], head: {sha: '1'.repeat(40)}, base: {sha: '2'.repeat(40)}, ...pr, }}; }}, actions: {async createWorkflowDispatch(request) { @@ -60,7 +60,7 @@ for (const command of ['/ci', '/ci all', '/ci full extra', '> /ci full', '/ci fu }); } -for (const pr of [{state: 'closed'}, {user: {login: 'someone-else'}}, {labels: []}]) { +for (const pr of [{state: 'closed'}, {user: {login: 'someone-else'}}, {labels: []}, {labels: [{name: 'ci:manual'}]}]) { test(`ignore PR outside the pilot: ${JSON.stringify(pr)}`, async () => { assert.deepEqual((await run({pr})).requests, []); }); diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9f5bc2379f..e18f840a38 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,7 +35,7 @@ jobs: standard-small: if: >- (github.event.action != 'ready_for_review') && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -89,7 +89,7 @@ jobs: precompile: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -114,7 +114,7 @@ jobs: standard-large: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -203,7 +203,7 @@ jobs: inplace: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -281,7 +281,7 @@ jobs: name: Manylinux on 🐍 ${{ matrix.python-version }} (${{ matrix.container }}) if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -317,7 +317,7 @@ jobs: clang: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -389,7 +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:manual')) + 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 @@ -460,7 +460,7 @@ jobs: ubuntu-nvhpc: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 @@ -526,7 +526,7 @@ jobs: gcc: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -603,7 +603,7 @@ jobs: icc: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-22.04 timeout-minutes: 90 @@ -713,7 +713,7 @@ jobs: centos: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) runs-on: ubuntu-latest strategy: fail-fast: false @@ -782,7 +782,7 @@ jobs: install-classic: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 @@ -832,7 +832,7 @@ jobs: doxygen: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 @@ -870,7 +870,7 @@ jobs: win32: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -925,7 +925,7 @@ jobs: win32-debug: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -977,7 +977,7 @@ jobs: windows-2022: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false matrix: @@ -1044,7 +1044,7 @@ jobs: mingw: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 @@ -1158,7 +1158,7 @@ jobs: windows_clang: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: matrix: @@ -1233,7 +1233,7 @@ jobs: windows_arm_clang_msvc: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) strategy: fail-fast: false @@ -1303,7 +1303,7 @@ jobs: windows_arm_clang_msys2: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 a3a0172c8d..ba1cb35bc8 100644 --- a/.github/workflows/configure.yml +++ b/.github/workflows/configure.yml @@ -23,7 +23,7 @@ jobs: cmake: if: >- (github.event.pull_request.draft == false) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (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 b87b795df7..bfbbb94171 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -22,7 +22,7 @@ env: jobs: pre-commit: - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: Format runs-on: ubuntu-latest steps: @@ -38,7 +38,7 @@ jobs: run: node --test .github/tests/intentional-ci.test.cjs clang-tidy: - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + 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 index b93071b607..f6a5a08aa2 100644 --- a/.github/workflows/intentional-ci.yml +++ b/.github/workflows/intentional-ci.yml @@ -35,11 +35,11 @@ jobs: pull_number: context.issue.number, }); if (pr.state !== 'open' || pr.user.login !== context.payload.comment.user.login - || !pr.labels.some(label => label.name === 'ci:manual')) return; + || !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:manual`. ' + .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(); diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml index eb6f0b356a..1038fa3aba 100644 --- a/.github/workflows/pip.yml +++ b/.github/workflows/pip.yml @@ -19,7 +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:manual')) + 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 @@ -44,7 +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:manual')) + 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 48dfccd22f..35ee6c568e 100644 --- a/.github/workflows/tests-cibw.yml +++ b/.github/workflows/tests-cibw.yml @@ -17,7 +17,7 @@ concurrency: jobs: build-wasm-emscripten: - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) name: Pyodide wheel runs-on: ubuntu-latest steps: @@ -34,7 +34,7 @@ jobs: only: cp314-pyodide_wasm32 build-ios: - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + 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: @@ -57,7 +57,7 @@ jobs: package-dir: tests build-android: - if: (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + 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 27680bafe2..ea24ffa642 100644 --- a/.github/workflows/upstream.yml +++ b/.github/workflows/upstream.yml @@ -23,7 +23,7 @@ jobs: # Only runs when the 'python dev' label is selected if: >- (contains(github.event.pull_request.labels.*.name, 'python dev')) && - (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:manual')) + (github.event_name != 'pull_request' || !contains(github.event.pull_request.labels.*.name, 'ci:intentional')) steps: - uses: actions/checkout@v7 From c14a5154b7eddb07040d04f8648a0ff1ac44aad6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 25 Oct 2026 15:46:50 -0700 Subject: [PATCH 6/6] ci: remove development-only bootstrap tests --- .github/intentional-ci.md | 6 -- .github/tests/intentional-ci.test.cjs | 96 --------------------------- .github/workflows/format.yml | 3 - 3 files changed, 105 deletions(-) delete mode 100644 .github/tests/intentional-ci.test.cjs diff --git a/.github/intentional-ci.md b/.github/intentional-ci.md index 69fe20f5b1..cab0d597f6 100644 --- a/.github/intentional-ci.md +++ b/.github/intentional-ci.md @@ -41,9 +41,3 @@ 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. - -Run the listener regression tests locally with: - -```sh -node --test .github/tests/intentional-ci.test.cjs -``` diff --git a/.github/tests/intentional-ci.test.cjs b/.github/tests/intentional-ci.test.cjs deleted file mode 100644 index 0f4d2873c9..0000000000 --- a/.github/tests/intentional-ci.test.cjs +++ /dev/null @@ -1,96 +0,0 @@ -const assert = require('node:assert/strict'); -const fs = require('node:fs'); -const path = require('node:path'); -const {test} = require('node:test'); - -// Exercise the actual inline github-script without checking out or running PR code. -const workflow = fs.readFileSync(path.join(__dirname, '../workflows/intentional-ci.yml'), 'utf8'); -const block = workflow.match(/ script: \|\n((?: .*\n|\n)+)/)[1]; -const AsyncFunction = Object.getPrototypeOf(async function () {}).constructor; -const dispatch = new AsyncFunction('github', 'context', 'core', - block.split('\n').map(line => line.slice(12)).join('\n')); - -async function run({command = '/ci full', pr = {}, commenter = 'rwgk', status} = {}) { - const requests = []; - const failures = []; - const summaries = []; - const summary = { - addHeading(value) { summaries.push(value); return this; }, - addRaw() { return this; }, - addCodeBlock() { return this; }, - async write() {}, - }; - const github = {rest: { - pulls: {async get() { return {data: { - number: 123, state: 'open', user: {login: 'rwgk'}, - labels: [{name: 'ci:intentional'}], - head: {sha: '1'.repeat(40)}, base: {sha: '2'.repeat(40)}, ...pr, - }}; }}, - actions: {async createWorkflowDispatch(request) { - if (status) throw Object.assign(new Error('API failure'), {status}); - requests.push(request); - }}, - }}; - const context = { - repo: {owner: 'pybind', repo: 'pybind11'}, issue: {number: 123}, - payload: {comment: {body: command, id: 456, user: {login: commenter}}}, - }; - await dispatch(github, context, {summary, setFailed(message) { failures.push(message); }}); - return {requests, failures, summaries}; -} - -for (const profile of ['full', 'quick']) { - test(`dispatch ${profile} with frozen revisions to the fixed worker`, async () => { - const {requests} = await run({command: `/ci ${profile}`}); - assert.equal(requests.length, 1); - const {inputs, ...destination} = requests[0]; - assert.deepEqual(destination, { - owner: 'pybind', repo: 'pybind11', workflow_id: 'intentional-ci.yml', - ref: 'intentional-ci-experiment', - }); - assert.deepEqual(JSON.parse(inputs.request), { - pr: 123, profile, head_sha: '1'.repeat(40), base_sha: '2'.repeat(40), comment_id: 456, - }); - }); -} - -for (const command of ['/ci', '/ci all', '/ci full extra', '> /ci full', '/ci full\nplease', '/ci full; echo unsafe']) { - test(`ignore unrecognized command ${JSON.stringify(command)}`, async () => { - assert.deepEqual((await run({command})).requests, []); - }); -} - -for (const pr of [{state: 'closed'}, {user: {login: 'someone-else'}}, {labels: []}, {labels: [{name: 'ci:manual'}]}]) { - test(`ignore PR outside the pilot: ${JSON.stringify(pr)}`, async () => { - assert.deepEqual((await run({pr})).requests, []); - }); -} - -test('other PR authors can request CI on their own opted-in PRs', async () => { - const result = await run({commenter: 'contributor', pr: {user: {login: 'contributor'}}}); - assert.equal(result.requests.length, 1); - assert.deepEqual(result.failures, []); -}); - -test('reject requests from someone other than the PR author', async () => { - assert.deepEqual((await run({commenter: 'someone-else'})).requests, []); -}); - -test('help explains the pilot without dispatching', async () => { - const result = await run({command: '/ci help'}); - assert.deepEqual(result.requests, []); - assert.deepEqual(result.summaries, ['Intentional CI experiment']); -}); - -for (const status of [404, 422]) { - test(`missing or incompatible worker fails clearly (${status})`, async () => { - const result = await run({status}); - assert.deepEqual(result.requests, []); - assert.match(result.failures[0], /intentional-ci-experiment/); - assert.deepEqual(result.summaries, []); - }); -} - -test('unexpected API errors propagate', async () => { - await assert.rejects(run({status: 503}), /API failure/); -}); diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index bfbbb94171..76d64ad632 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -34,9 +34,6 @@ jobs: run: echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" - uses: pre-commit/action@v3.0.1 - - name: Test intentional CI bootstrap - run: node --test .github/tests/intentional-ci.test.cjs - 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