From 055de06030272acb4fb15034816139d58ed0a7f4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Mon, 9 Nov 2026 14:10:08 +0800 Subject: [PATCH 1/2] CI: migrate the dialect labeler away from pull_request_target Fixes #2818 Signed-off-by: Vincent --- .github/workflows/labeler.yml | 35 ++++++++++++--- .github/workflows/labeler/label_dialects.js | 9 ++-- .../workflows/labeler/pull_requests_of_run.js | 37 +++++++++++++++ .github/workflows/pull_request_signal.yml | 45 +++++++++++++++++++ 4 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 .github/workflows/labeler/pull_requests_of_run.js create mode 100644 .github/workflows/pull_request_signal.yml diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 143012d9b0..b76e8c0899 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -17,28 +17,46 @@ name: labeler -# pull_request_target runs on the base branch, so the script below is never taken from the PR head. +# `pull_request_target` is disabled by ASF infra policy. The pull request path +# runs from the default branch when `pull_request_signal.yml` starts, so it +# never runs pull request code. See that file for details. +# +# `issues` and `issue_comment` already run from the default branch and are +# unchanged. +concurrency: + group: ${{ github.repository }}-${{ github.event.workflow_run.head_repository.full_name || github.event.issue.number }}-${{ github.event.workflow_run.head_branch || github.event_name }}-${{ github.workflow }} + cancel-in-progress: true + on: - pull_request_target: - types: [opened, edited, reopened, synchronize] + workflow_run: + workflows: ["Pull request signal"] + types: [requested] issues: types: [opened, edited] issue_comment: types: [created] +permissions: {} + jobs: label-dialects: name: Label dialects # Bot edits and comments arrive in bursts and never carry a dialect. if: >- github.event.sender.type != 'Bot' && - (github.event_name != 'issue_comment' || contains(github.event.comment.body, 'dialect')) + ( + (github.event_name == 'workflow_run' && github.event.workflow_run.event == 'pull_request') + || github.event_name == 'issues' + || (github.event_name == 'issue_comment' && contains(github.event.comment.body, 'dialect')) + ) runs-on: ubuntu-latest + # The token of a `workflow_run` workflow can update PRs from forks. permissions: contents: read issues: write pull-requests: write steps: + # This checks out the default branch for `workflow_run`, never the pull request. - uses: actions/checkout@v4 with: sparse-checkout: .github/workflows/labeler @@ -47,4 +65,11 @@ jobs: with: script: | const label = require('./.github/workflows/labeler/label_dialects.js'); - await label({ github, context }); + if (context.eventName === 'workflow_run') { + const pulls = await require('./.github/workflows/labeler/pull_requests_of_run.js')({ github, context }); + for (const pull of pulls) { + await label({ github, context, pull }); + } + } else { + await label({ github, context }); + } diff --git a/.github/workflows/labeler/label_dialects.js b/.github/workflows/labeler/label_dialects.js index ef752ef2dd..4b98babe15 100644 --- a/.github/workflows/labeler/label_dialects.js +++ b/.github/workflows/labeler/label_dialects.js @@ -89,21 +89,22 @@ function trustedComment(comment, issue) { return comment.user.id === issue.user.id || TRUSTED_ASSOCIATIONS.includes(comment.author_association); } -async function run({ github, context }) { +async function run({ github, context, pull }) { const { owner, repo } = context.repo; const { payload } = context; - const issue = payload.pull_request ?? payload.issue; + const issue = pull ?? payload.pull_request ?? payload.issue; + if (!issue) return; const issue_number = issue.number; let labels = []; let comment; - if (context.eventName === "issue_comment") { + if (!pull && context.eventName === "issue_comment") { if (!trustedComment(payload.comment, issue)) return; labels = declaredDialects(payload.comment.body) ?? []; } else { const resolved = resolve(issue); labels = resolved.labels; - if (!resolved.answered && payload.pull_request && payload.action === "opened") { + if (!resolved.answered && (pull || (payload.pull_request && payload.action === "opened"))) { const files = await github.paginate(github.rest.pulls.listFiles, { owner, repo, diff --git a/.github/workflows/labeler/pull_requests_of_run.js b/.github/workflows/labeler/pull_requests_of_run.js new file mode 100644 index 0000000000..6ed1f7a0e9 --- /dev/null +++ b/.github/workflows/labeler/pull_requests_of_run.js @@ -0,0 +1,37 @@ +// Licensed to the Apache Software Foundation (ASF) under one +// or more contributor license agreements. See the NOTICE file +// distributed with this work for additional information +// regarding copyright ownership. The ASF licenses this file +// to you under the Apache License, Version 2.0 (the +// "License"); you may not use this file except in compliance +// with the License. You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, +// software distributed under the License is distributed on an +// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +// KIND, either express or implied. See the License for the +// specific language governing permissions and limitations +// under the License. + +// Used by the dialect labeler when it runs on `workflow_run`. +// +// `workflow_run.pull_requests` is empty for pull requests from forks, so find +// the open pull requests of the run's head branch. They are the current ones, +// which can be newer than the run's commit when pushes follow each other. +// The same branch can target several bases, so return every match. +module.exports = async ({ github, context }) => { + const run = context.payload.workflow_run; + // The head repository is null when the fork was deleted. + if (!run.head_repository) { + return []; + } + const { data } = await github.rest.pulls.list({ + owner: context.repo.owner, + repo: context.repo.repo, + state: "open", + head: `${run.head_repository.owner.login}:${run.head_branch}`, + }); + return data; +}; diff --git a/.github/workflows/pull_request_signal.yml b/.github/workflows/pull_request_signal.yml new file mode 100644 index 0000000000..b891fe4743 --- /dev/null +++ b/.github/workflows/pull_request_signal.yml @@ -0,0 +1,45 @@ +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +# Signal for workflows that need a write token on a pull request. +# +# ASF infra policy disables `pull_request_target` +# (https://infra.apache.org/github-actions-policy.html), and GitHub is adding a +# default policy that blocks it. `labeler.yml` runs from the default branch on +# the `requested` event of this workflow and never checks out or runs pull +# request code. +# +# GitHub sends `workflow_run` `requested` at once for every run, even one that +# waits for first-time contributor approval. A pull request runs its own copy of +# this file, so changing or deleting it only skips dialect labels for that pull +# request. GitHub also skips `pull_request` workflows while a pull request has +# merge conflicts, so such a pull request gets its labels after a push that +# resolves them. +name: Pull request signal + +on: + pull_request: + types: [opened, edited, reopened, synchronize] + +permissions: {} + +jobs: + signal: + name: Signal pull request update + runs-on: ubuntu-latest + steps: + - run: echo "This run triggers the workflow_run workflows that handle the pull request." From 17bc5a52d18359ecac120b1bce6c9651e75ef5d6 Mon Sep 17 00:00:00 2001 From: Vincent Date: Tue, 10 Nov 2026 09:45:02 +0800 Subject: [PATCH 2/2] CI: give issue and issue_comment labeler runs unique concurrency groups A second comment on the same pull request was sharing one concurrency group and cancelling an in-progress issues/issue_comment labeling run, so dialects declared in the cancelled comment were never applied. Main has no concurrency block, so this restores that behavior. Use github.run_id for those events and keep the pull-request workflow_run group keyed by head repository and branch. Signed-off-by: Vincent --- .github/workflows/labeler.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index b76e8c0899..45230beb5d 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -21,10 +21,11 @@ name: labeler # runs from the default branch when `pull_request_signal.yml` starts, so it # never runs pull request code. See that file for details. # -# `issues` and `issue_comment` already run from the default branch and are -# unchanged. +# `issues` and `issue_comment` already run from the default branch. +# Their concurrency group uses `github.run_id` so a later comment on the +# same issue does not cancel an in-progress labeling run. concurrency: - group: ${{ github.repository }}-${{ github.event.workflow_run.head_repository.full_name || github.event.issue.number }}-${{ github.event.workflow_run.head_branch || github.event_name }}-${{ github.workflow }} + group: ${{ github.repository }}-${{ github.event_name == 'workflow_run' && format('{0}-{1}', github.event.workflow_run.head_repository.full_name, github.event.workflow_run.head_branch) || github.run_id }}-${{ github.workflow }} cancel-in-progress: true on: