From 3415562686bb652e64104d76b7ab8d5c2def370c Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Tue, 15 Sep 2026 11:34:38 +0200 Subject: [PATCH 1/4] Label external contributions Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../label-external-contributions.yml | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 .github/workflows/label-external-contributions.yml diff --git a/.github/workflows/label-external-contributions.yml b/.github/workflows/label-external-contributions.yml new file mode 100644 index 000000000000..4b396242083b --- /dev/null +++ b/.github/workflows/label-external-contributions.yml @@ -0,0 +1,62 @@ +name: Label external contributions + +on: + pull_request_target: + types: + - opened + - reopened + - ready_for_review + +permissions: {} + +jobs: + label: + if: >- + github.event.pull_request.draft == false && + github.event.pull_request.user.type == 'User' && + github.event.pull_request.author_association != 'MEMBER' && + github.event.pull_request.author_association != 'OWNER' + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + issues: write + concurrency: + group: label-external-contribution-${{ github.event.pull_request.number }} + cancel-in-progress: false + + steps: + - name: Add external contribution label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + github-token: ${{ github.token }} + script: | + const label = 'external-contribution'; + const pullRequest = context.payload.pull_request; + + if (pullRequest.labels.some(({ name }) => name === label)) { + return; + } + + if (context.payload.action !== 'opened') { + const events = await github.paginate( + github.rest.issues.listEvents, + { + ...context.repo, + issue_number: pullRequest.number, + per_page: 100, + }, + ); + if ( + events.some(({ event, label: eventLabel }) => + event === 'unlabeled' && eventLabel?.name === label + ) + ) { + return; + } + } + + await github.rest.issues.addLabels({ + ...context.repo, + issue_number: pullRequest.number, + labels: [label], + }); From 9ac2ff973bc10dbbcf2ec0aac4d6d303b9c05ac8 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Tue, 15 Sep 2026 11:46:53 +0200 Subject: [PATCH 2/4] Check label history for opened pull requests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../label-external-contributions.yml | 30 +++++++++---------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/.github/workflows/label-external-contributions.yml b/.github/workflows/label-external-contributions.yml index 4b396242083b..6b281c0ab104 100644 --- a/.github/workflows/label-external-contributions.yml +++ b/.github/workflows/label-external-contributions.yml @@ -37,22 +37,20 @@ jobs: return; } - if (context.payload.action !== 'opened') { - const events = await github.paginate( - github.rest.issues.listEvents, - { - ...context.repo, - issue_number: pullRequest.number, - per_page: 100, - }, - ); - if ( - events.some(({ event, label: eventLabel }) => - event === 'unlabeled' && eventLabel?.name === label - ) - ) { - return; - } + const events = await github.paginate( + github.rest.issues.listEvents, + { + ...context.repo, + issue_number: pullRequest.number, + per_page: 100, + }, + ); + if ( + events.some(({ event, label: eventLabel }) => + event === 'unlabeled' && eventLabel?.name === label + ) + ) { + return; } await github.rest.issues.addLabels({ From e6722a00ed054f621b5ae17fb676535c6e7f04ed Mon Sep 17 00:00:00 2001 From: Anders Peter Fugmann Date: Tue, 15 Sep 2026 12:59:49 +0200 Subject: [PATCH 3/4] Update .github/workflows/label-external-contributions.yml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Óscar San José --- .github/workflows/label-external-contributions.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/label-external-contributions.yml b/.github/workflows/label-external-contributions.yml index 6b281c0ab104..6db5dcd68501 100644 --- a/.github/workflows/label-external-contributions.yml +++ b/.github/workflows/label-external-contributions.yml @@ -11,6 +11,8 @@ permissions: {} jobs: label: + # Label non-draft PRs from personal accounts that are not owning (github) organization members or owners, + # including outside collaborators and prior contributors. GitHub organization members are excluded, and bot accounts too. if: >- github.event.pull_request.draft == false && github.event.pull_request.user.type == 'User' && From be6d5b769ed1e9eb712ab1ce12b9002c0381f729 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Tue, 15 Sep 2026 14:50:33 +0200 Subject: [PATCH 4/4] Address peer review: Replace 'pull_request_target' with a schedule for the external review labeler workflow Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../label-external-contributions.yml | 118 ++++++++++-------- 1 file changed, 68 insertions(+), 50 deletions(-) diff --git a/.github/workflows/label-external-contributions.yml b/.github/workflows/label-external-contributions.yml index 6db5dcd68501..2843f6d2c5d4 100644 --- a/.github/workflows/label-external-contributions.yml +++ b/.github/workflows/label-external-contributions.yml @@ -1,62 +1,80 @@ name: Label external contributions on: - pull_request_target: - types: - - opened - - reopened - - ready_for_review + schedule: + - cron: "7,22,37,52 * * * *" + workflow_dispatch: permissions: {} +concurrency: + group: label-external-contributions + cancel-in-progress: false + jobs: label: - # Label non-draft PRs from personal accounts that are not owning (github) organization members or owners, - # including outside collaborators and prior contributors. GitHub organization members are excluded, and bot accounts too. - if: >- - github.event.pull_request.draft == false && - github.event.pull_request.user.type == 'User' && - github.event.pull_request.author_association != 'MEMBER' && - github.event.pull_request.author_association != 'OWNER' + if: github.ref_name == github.event.repository.default_branch runs-on: ubuntu-latest - timeout-minutes: 5 + timeout-minutes: 10 permissions: - issues: write - concurrency: - group: label-external-contribution-${{ github.event.pull_request.number }} - cancel-in-progress: false + pull-requests: write steps: - - name: Add external contribution label - uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 - with: - github-token: ${{ github.token }} - script: | - const label = 'external-contribution'; - const pullRequest = context.payload.pull_request; - - if (pullRequest.labels.some(({ name }) => name === label)) { - return; - } - - const events = await github.paginate( - github.rest.issues.listEvents, - { - ...context.repo, - issue_number: pullRequest.number, - per_page: 100, - }, - ); - if ( - events.some(({ event, label: eventLabel }) => - event === 'unlabeled' && eventLabel?.name === label - ) - ) { - return; - } - - await github.rest.issues.addLabels({ - ...context.repo, - issue_number: pullRequest.number, - labels: [label], - }); + - name: Label external contributions + env: + GH_TOKEN: ${{ github.token }} + REPO: ${{ github.repository }} + run: | + set -euo pipefail + + label="external-contribution" + updated_cutoff=$(date -u -d "1 hour ago" "+%Y-%m-%dT%H:%M:%SZ") + + while IFS= read -r pr_number; do + if [[ ! "$pr_number" =~ ^[1-9][0-9]*$ ]]; then + echo "Skipping malformed pull request number." + continue + fi + + pr_json=$(gh api "repos/$REPO/pulls/$pr_number") + if ! jq -e \ + --arg repo "$REPO" \ + --arg label "$label" \ + '.state == "open" and + .draft == false and + .base.repo.full_name == $repo and + (.head.repo.full_name | type == "string") and + .head.repo.full_name != $repo and + .user.type == "User" and + .author_association != "MEMBER" and + .author_association != "OWNER" and + (any(.labels[]?; .name == $label) | not)' \ + >/dev/null <<<"$pr_json"; then + continue + fi + + events=$(gh api --paginate \ + "repos/$REPO/issues/$pr_number/events?per_page=100" | + jq -cs 'add') + + if jq -e --arg label "$label" \ + 'any(.[]; .event == "labeled" and .label.name == $label)' \ + >/dev/null <<<"$events"; then + continue + fi + + jq -n --arg label "$label" '{labels: [$label]}' | + gh api --method POST \ + "repos/$REPO/issues/$pr_number/labels" \ + --input - \ + >/dev/null + echo "Labelled pull request #$pr_number." + done < <( + gh api --method GET --paginate "repos/$REPO/issues" \ + -f state=open \ + -f since="$updated_cutoff" \ + -f sort=updated \ + -f direction=desc \ + -f per_page=100 | + jq -r '.[] | select(.pull_request != null) | .number' + )