From 4e97b8e5af333e87915f8c50b3abf08b9cdeceeb Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Tue, 15 Sep 2026 10:44:35 +0200 Subject: [PATCH 1/2] 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 74f575ec2f74222fc04663c0173a33a9c72fa381 Mon Sep 17 00:00:00 2001 From: Anders Fugmann Date: Tue, 15 Sep 2026 11:19:29 +0200 Subject: [PATCH 2/2] Add external contribution label selector Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../label-external-contributions.yml | 62 --------- misc/scripts/label-external-contributions.sh | 127 ++++++++++++++++++ 2 files changed, 127 insertions(+), 62 deletions(-) delete mode 100644 .github/workflows/label-external-contributions.yml create mode 100755 misc/scripts/label-external-contributions.sh diff --git a/.github/workflows/label-external-contributions.yml b/.github/workflows/label-external-contributions.yml deleted file mode 100644 index 4b396242083b..000000000000 --- a/.github/workflows/label-external-contributions.yml +++ /dev/null @@ -1,62 +0,0 @@ -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], - }); diff --git a/misc/scripts/label-external-contributions.sh b/misc/scripts/label-external-contributions.sh new file mode 100755 index 000000000000..d987b4835774 --- /dev/null +++ b/misc/scripts/label-external-contributions.sh @@ -0,0 +1,127 @@ +#!/usr/bin/env bash + +set -euo pipefail + +repository="github/codeql" +label="external-contribution" + +for command in gh jq fzf; do + if ! command -v "$command" >/dev/null; then + echo "Required command not found: $command" >&2 + exit 1 + fi +done + +if ! gh auth status >/dev/null 2>&1; then + echo "Authenticate with GitHub CLI before running this script." >&2 + exit 1 +fi + +if [[ ! -r /dev/tty || ! -w /dev/tty ]]; then + echo "This script requires an interactive terminal." >&2 + exit 1 +fi + +pulls="$( + gh api --method GET --paginate "repos/$repository/pulls" \ + -f state=open \ + -f sort=created \ + -f direction=desc \ + -f per_page=100 +)" + +rows="" +while IFS=$'\t' read -r number created author status title; do + if [[ "$status" == "unlabelled" ]]; then + events="$( + gh api --method GET --paginate \ + "repos/$repository/issues/$number/events" \ + -f per_page=100 + )" + if jq -s -e --arg label "$label" \ + 'any(.[][]; .event == "unlabeled" and .label.name == $label)' \ + >/dev/null <<<"$events"; then + status="removed" + fi + fi + + rows+="$number"$'\t'"$created"$'\t'"$author"$'\t'"$status"$'\t'"$title"$'\n' +done < <( + jq -r --arg label "$label" ' + .[] + | select( + .draft == false + and .user.type == "User" + and .author_association != "MEMBER" + and .author_association != "OWNER" + ) + | [ + (.number | tostring), + .created_at[0:10], + .user.login, + (if any(.labels[]?; .name == $label) + then "labelled" + else "unlabelled" + end), + (.title + | gsub("[\u0000-\u001f\u007f\u202a-\u202e\u2066-\u2069]"; " ")) + ] + | @tsv + ' <<<"$pulls" +) + +if [[ -z "$rows" ]]; then + echo "No external contributions found." + exit 0 +fi + +set +e +selection="$( + printf '%s' "$rows" | + fzf --multi \ + --no-mouse \ + --delimiter=$'\t' \ + --with-nth=2,3,4,5 \ + --header="Select unlabelled PRs with TAB, then press ENTER" \ + --prompt="External contributions> " +)" +fzf_status=$? +set -e + +case "$fzf_status" in + 0) ;; + 1 | 130) + echo "No pull requests selected." + exit 0 + ;; + *) + echo "fzf failed with exit code $fzf_status." >&2 + exit "$fzf_status" + ;; +esac + +selected_numbers="$( + awk -F $'\t' '$4 == "unlabelled" { print $1 }' <<<"$selection" +)" +if [[ -z "$selected_numbers" ]]; then + echo "No unlabelled pull requests selected." + exit 0 +fi + +selected_count="$(wc -l <<<"$selected_numbers")" +printf 'Apply label "%s" to %s pull request(s)? [y/N] ' \ + "$label" "$selected_count" >/dev/tty +read -r confirmation &2 + exit 1 + fi + + gh pr edit "$number" --repo "$repository" --add-label "$label" +done <<<"$selected_numbers"