From c22b6465e1eac905080bf81041a8fe7cc3d0dd83 Mon Sep 17 00:00:00 2001 From: Yang Wu <1425326620@qq.com> Date: Sat, 29 Aug 2026 23:05:02 +0800 Subject: [PATCH] fix(ci): run CI when pull requests retarget main Generated-by: Codex --- .asf.yaml | 18 ++++++++++-------- .github/workflows/ci.yml | 9 ++++++++- scripts/ci-workflow-policy.test.mjs | 27 +++++++++++++++++++++++---- 3 files changed, 41 insertions(+), 13 deletions(-) diff --git a/.asf.yaml b/.asf.yaml index bd40ce071f..fa87ccae0a 100644 --- a/.asf.yaml +++ b/.asf.yaml @@ -61,14 +61,16 @@ github: required_approving_review_count: 1 required_status_checks: strict: false - # test is the single unconditional job in .github/workflows/ci.yml. It - # runs the install-free contract checks on every change and installs the - # toolchain only for the validation its own planning step selects, so a - # documentation-only change still reports without paying for a build. - # Renaming the job there, adding a paths filter that stops ci.yml from - # running, or splitting the work back across jobs so this context comes - # from an aggregator that can be skipped, freezes every pull request: - # the check never reports and no committer can override it. + # test is the single required-check authority in + # .github/workflows/ci.yml. It runs for every new or updated pull request + # targeting main, including a pull request retargeted there. Title- and + # body-only edits create a differently named skipped job, so they neither + # cancel nor satisfy this context. The job runs install-free contract + # checks before installing the toolchain only for the validation its own + # planning step selects, so a documentation-only change still reports + # without paying for a build. Renaming the required job, adding a paths + # filter that stops ci.yml from running, or splitting this authority back + # across jobs can leave the check unreported and freeze pull requests. # A required context must report on every pull request, so a lane # behind a paths filter cannot be listed here: the filter would keep # the workflow from starting and the check would stay pending forever. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 31c1978380..e13af462a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,12 +20,15 @@ name: CI on: pull_request: branches: [main] + types: [opened, synchronize, reopened, edited] push: branches: [main] workflow_dispatch: concurrency: - group: ci-${{ github.workflow }}-${{ github.ref }} + # `edited` also covers title and body changes. Isolate those no-op runs so + # they cannot cancel the check for the current pull request revision. + group: ci-${{ github.workflow }}-${{ github.ref }}${{ github.event.action == 'edited' && github.event.changes.base.ref.from == '' && format('-ignored-{0}', github.run_id) || '' }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} permissions: @@ -42,6 +45,10 @@ jobs: # Renaming it would leave that check unreported on every open pull request # until the rename merged, and nothing could merge while it was unreported. test: + # A base-ref edit keeps the protected `CI / test` name. Other edits create + # only a differently named skipped check, so they cannot satisfy it. + name: ${{ github.event_name == 'pull_request' && github.event.action == 'edited' && github.event.changes.base.ref.from == '' && 'ignored-edit' || 'test' }} + if: ${{ github.event_name != 'pull_request' || github.event.action != 'edited' || github.event.changes.base.ref.from != '' }} # Pinned, not `ubuntu-latest`. The two resolve to the same image, but only # the alias makes this required context wait at the tail, and the steps # below already assume this image. `ci-workflow-policy.test.mjs` holds the diff --git a/scripts/ci-workflow-policy.test.mjs b/scripts/ci-workflow-policy.test.mjs index 62c03b362d..a3866266b8 100644 --- a/scripts/ci-workflow-policy.test.mjs +++ b/scripts/ci-workflow-policy.test.mjs @@ -53,19 +53,19 @@ test('GitHub output matches the selections consumed by CI', () => { assert.deepEqual(outputKeys, consumedKeys); }); -test('one unconditional job carries the required context on every pull request', () => { +test('one job remains the only required-check authority', () => { const workflow = readWorkflow('ci.yml'); // `.asf.yaml` requires `test`. A paths filter would stop the workflow and - // leave that check pending forever, and a second job would make the same - // pull request queue for a scarce runner twice to reach one verdict. + // leave that check pending forever, and a second job would create another + // authority. Metadata-only edits may skip this job under a different name; + // the retarget contract below proves that exception cannot impersonate it. assert.doesNotMatch(triggerBlock('ci.yml'), /\bpaths(-ignore)?:/u); const jobsBlock = workflow.slice(workflow.indexOf('\njobs:')); const jobs = [...jobsBlock.matchAll(/^ {2}([a-z0-9_-]+):$/gmu)].map((match) => match[1]); assert.deepEqual(jobs, ['test']); assert.doesNotMatch(jobsBlock, /^ {4}needs:/mu); - assert.doesNotMatch(jobsBlock, /^ {4}if:/mu); }); test('planning runs first and every later step gates on its outputs', () => { @@ -101,6 +101,25 @@ test('core CI validates pull requests and the resulting main branch state', () = assert.match(workflow, /\[\[ "\$BASE_SHA" =~ \^0\+\$ \]\]/u); }); +test('core CI runs on base retargets without letting metadata edits replace the required check', () => { + const workflow = readWorkflow('ci.yml'); + + assert.match(workflow, /types: \[opened, synchronize, reopened, edited\]/u); + assert.match( + workflow, + /group: ci-\$\{\{ github\.workflow \}\}-\$\{\{ github\.ref \}\}\$\{\{ github\.event\.action == 'edited' && github\.event\.changes\.base\.ref\.from == '' && format\('-ignored-\{0\}', github\.run_id\) \|\| '' \}\}/u, + ); + assert.match(workflow, /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/u); + assert.match( + workflow, + /name: \$\{\{ github\.event_name == 'pull_request' && github\.event\.action == 'edited' && github\.event\.changes\.base\.ref\.from == '' && 'ignored-edit' \|\| 'test' \}\}/u, + ); + assert.match( + workflow, + /if: \$\{\{ github\.event_name != 'pull_request' \|\| github\.event\.action != 'edited' \|\| github\.event\.changes\.base\.ref\.from != '' \}\}/u, + ); +}); + test('core CI uses the Windows inventory package-script authority', () => { const workflow = readWorkflow('ci.yml');