-
Notifications
You must be signed in to change notification settings - Fork 416
Add PR size labeling workflow #1255
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
mhucka
wants to merge
7
commits into
quantumlib:main
Choose a base branch
from
mhucka:add-size-labeler
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9659dec
Add PR size labeling workflow
mhucka 613697a
Fix capitalization typo
mhucka 6c19e66
Support > 100 files in size-labeler.sh
mhucka 9aa2f92
Implement performance improvement from Gemini Code Assist
mhucka 234f926
Remove no-longer-needed variable
mhucka a78f913
Fix inconsistent label capitalization
mhucka db63bce
Merge branch 'main' into add-size-labeler
mhucka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,73 @@ | ||
| # Copyright 2026 Google LLC | ||
| # | ||
| # Licensed 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 | ||
| # | ||
| # https://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. | ||
|
|
||
| name: Pull request labeler | ||
| run-name: >- | ||
| Label pull request ${{github.event.pull_request.number}} by ${{github.actor}} | ||
|
|
||
| # This workflow is designed NOT to fail if labeling actions encounter errors; | ||
| # instead, it notes errors as annotations on the workflow's run summary page. If | ||
| # labels don't seem to be getting applied, check there for errors. | ||
|
|
||
| on: | ||
| # Note: do not copy-paste this workflow with `pull_request_target` left as-is. | ||
| # Its use here is a special case where security implications are understood. | ||
| # Workflows should normally use `pull_request` instead. | ||
| pull_request_target: | ||
| types: | ||
| - opened | ||
| - synchronize | ||
|
|
||
| # Allow manual invocation. | ||
| workflow_dispatch: | ||
| inputs: | ||
| pr-number: | ||
| description: 'The PR number of the PR to label:' | ||
| type: string | ||
| required: true | ||
| debug: | ||
| description: 'Run with debugging options' | ||
| type: boolean | ||
| default: true | ||
|
|
||
| # Declare default workflow permissions as read only. | ||
| permissions: read-all | ||
|
|
||
| jobs: | ||
| label-pr-size: | ||
| if: github.repository_owner == 'quantumlib' | ||
| name: Update PR size labels | ||
| runs-on: ubuntu-slim | ||
| timeout-minutes: 5 | ||
| permissions: | ||
| contents: read | ||
| issues: write | ||
| pull-requests: write | ||
| env: | ||
| # Environment variable PR_NUMBER is needed by size-labeler.sh. | ||
| PR_NUMBER: ${{inputs.pr-number || github.event.pull_request.number}} | ||
| # Add xtrace to SHELLOPTS for all Bash scripts when doing debug runs. | ||
| SHELLOPTS: ${{inputs.debug && 'xtrace' || '' }} | ||
| steps: | ||
| - name: Check out a copy of the git repository | ||
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | ||
| with: | ||
| sparse-checkout: | | ||
| ./dev_tools/ci/size-labeler.sh | ||
|
|
||
| - name: Label the PR with a size label | ||
| continue-on-error: true | ||
| env: | ||
| GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} | ||
| run: ./dev_tools/ci/size-labeler.sh |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| # Continuous integration scripts | ||
|
|
||
| The scripts in this directory are used by the workflows in | ||
| [`../../.github/workflows/`](../../.github/workflows/). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,226 @@ | ||
| #!/usr/bin/env bash | ||
| # Copyright 2025 The Cirq Developers | ||
| # | ||
| # Licensed 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 | ||
| # | ||
| # https://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. | ||
|
|
||
| set -euo pipefail -o errtrace | ||
| shopt -s inherit_errexit | ||
|
|
||
| declare -r usage="Usage: ${0##*/} [-h | --help | help] | ||
|
|
||
| Updates the size labels on a pull request based on the number of lines it | ||
| changes. The script requires the following environment variables: | ||
| PR_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN. The script is intended | ||
| for automated execution from GitHub Actions workflow." | ||
|
|
||
| declare -ar LABELS=( | ||
| "Size: XS" | ||
mhucka marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| "size: S" | ||
| "size: M" | ||
| "size: L" | ||
| "size: XL" | ||
| ) | ||
|
|
||
| declare -A LIMITS=( | ||
| ["${LABELS[0]}"]=10 | ||
| ["${LABELS[1]}"]=50 | ||
| ["${LABELS[2]}"]=200 | ||
| ["${LABELS[3]}"]=800 | ||
| ["${LABELS[4]}"]="$((2 ** 63 - 1))" | ||
| ) | ||
|
|
||
| declare -ar IGNORED=( | ||
| "*_pb2.py" | ||
| "*_pb2.pyi" | ||
| "*_pb2_grpc.py" | ||
| ".*.lock" | ||
| "*.bundle.js" | ||
| ) | ||
|
|
||
| function info() { | ||
| echo >&2 "INFO: ${*}" | ||
| } | ||
|
|
||
| function error() { | ||
| echo >&2 "ERROR: ${*}" | ||
| } | ||
|
|
||
| function jq_stdin() { | ||
| local infile | ||
| infile="$(mktemp)" | ||
| readonly infile | ||
| local jq_status=0 | ||
|
|
||
| cat >"${infile}" | ||
| jq_file "$@" "${infile}" || jq_status="${?}" | ||
| rm "${infile}" | ||
| return "${jq_status}" | ||
| } | ||
|
|
||
| function jq_file() { | ||
| # Regardless of the success, store the return code. | ||
| # Prepend each sttderr with command args and send back to stderr. | ||
| jq "${@}" 2> >(awk -v h="stderr from jq ${*}:" '{print h, $0}' 1>&2) && | ||
| rc="${?}" || | ||
| rc="${?}" | ||
| if [[ "${rc}" != "0" ]]; then | ||
| error "The jq program failed: ${*}" | ||
| error "Note the quotes above may be wrong. Here was the (possibly empty) input in ${*: -1}:" | ||
| cat "${@: -1}" # Assumes last argument is input file!! | ||
| fi | ||
| return "${rc}" | ||
| } | ||
mhucka marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
|
||
| function api_call() { | ||
| local -r endpoint="${1// /%20}" # love that our label names have spaces... | ||
| local -r uri="https://api.github.com/repos/${GITHUB_REPOSITORY}" | ||
| local response | ||
| local curl_status=0 | ||
| info "Calling: ${uri}/${endpoint}" | ||
| response="$(curl -sSL \ | ||
| --fail-with-body \ | ||
| --connect-timeout 10 --max-time 20 \ | ||
| -H "Authorization: token ${GITHUB_TOKEN}" \ | ||
| -H "Accept: application/vnd.github.v3.json" \ | ||
| -H "X-GitHub-Api-Version:2022-11-28" \ | ||
| -H "Content-Type: application/json" \ | ||
| "${@:2}" \ | ||
| "${uri}/${endpoint}" | ||
| )" || curl_status="${?}" | ||
| if [[ -n "${response}" ]]; then | ||
| cat <<<"${response}" | ||
| fi | ||
| if (( curl_status )); then | ||
| error "GitHub API call failed (curl exit $curl_status) for ${uri}/${endpoint}" | ||
| error "Response body:" | ||
| cat >&2 <<<"${response}" | ||
| fi | ||
| return "${curl_status}" | ||
| } | ||
|
|
||
| function compute_changes() { | ||
| local -r pr="$1" | ||
| local -r keys_filter='with_entries(select([.key] | inside(["changes", "filename"])))' | ||
|
|
||
| local page=1 | ||
| local total_changes=0 | ||
| while true; do | ||
| local response | ||
| response="$(api_call "pulls/${pr}/files?per_page=100&page=${page}")" | ||
|
|
||
| if [[ "$(jq_stdin '. | length' <<<"${response}")" -eq 0 ]]; then | ||
| break | ||
| fi | ||
|
|
||
| local name changes | ||
| while IFS= read -r name && IFS= read -r changes; do | ||
| for pattern in "${IGNORED[@]}"; do | ||
| # shellcheck disable=SC2053 # Need leave the pattern unquoted. | ||
| if [[ "${name}" == ${pattern} ]]; then | ||
| info "File ${name} ignored" | ||
| continue 2 | ||
| fi | ||
| done | ||
| info "File ${name} +-${changes}" | ||
| total_changes="$((total_changes + changes))" | ||
| done < <(jq_stdin -r '.[] | .filename, .changes' <<<"${response}") | ||
| ((page++)) | ||
| done | ||
| echo "${total_changes}" | ||
| } | ||
|
|
||
| function get_size_label() { | ||
| local -r changes="$1" | ||
| for label in "${LABELS[@]}"; do | ||
| local limit="${LIMITS["${label}"]}" | ||
| if [[ "${changes}" -lt "${limit}" ]]; then | ||
| echo "${label}" | ||
| return | ||
| fi | ||
| done | ||
| } | ||
|
|
||
| function prune_stale_labels() { | ||
| local -r pr="$1" | ||
| local -r size_label="$2" | ||
| local response | ||
| local existing_labels | ||
| response="$(api_call "pulls/${pr}")" | ||
| existing_labels="$(jq_stdin -r '.labels[] | .name' <<<"${response}")" | ||
| readarray -t existing_labels <<<"${existing_labels}" | ||
|
|
||
| local correctly_labeled=false | ||
| for label in "${existing_labels[@]}"; do | ||
| [[ -z "${label}" ]] && continue | ||
| # If the label we want is already present, we can just leave it there. | ||
| if [[ "${label}" == "${size_label}" ]]; then | ||
| info "Label '${label}' is correct, leaving it." | ||
| correctly_labeled=true | ||
| continue | ||
| fi | ||
| # If there is another size label, we need to remove it | ||
| if [[ -v "LIMITS[${label}]" ]]; then | ||
| info "Label '${label}' is stale, removing it." | ||
| api_call "issues/${pr}/labels/${label}" -X DELETE >/dev/null | ||
| continue | ||
| fi | ||
| info "Label '${label}' is unknown, leaving it." | ||
| done | ||
| echo "${correctly_labeled}" | ||
| } | ||
|
|
||
| function main() { | ||
| local moreinfo="(Use --help option for more info.)" | ||
| if (( $# )); then | ||
| case "$1" in | ||
| -h | --help | help) | ||
| echo "$usage" | ||
| exit 0 | ||
| ;; | ||
| *) | ||
| error "Invalid argument '$1'. ${moreinfo}" | ||
| exit 2 | ||
| ;; | ||
| esac | ||
| fi | ||
| local env_var_name | ||
| local env_var_missing=0 | ||
| for env_var_name in PR_NUMBER GITHUB_TOKEN GITHUB_REPOSITORY; do | ||
| if [[ ! -v "${env_var_name}" ]]; then | ||
| env_var_missing=1 | ||
| error "Missing environment variable ${env_var_name}" | ||
| fi | ||
| done | ||
| if (( env_var_missing )); then | ||
| error "${moreinfo}" | ||
| exit 2 | ||
| fi | ||
|
|
||
| local total_changes | ||
| total_changes="$(compute_changes "$PR_NUMBER")" | ||
| info "Lines changed: ${total_changes}" | ||
|
|
||
| local size_label | ||
| size_label="$(get_size_label "$total_changes")" | ||
| info "Appropriate label is '${size_label}'" | ||
|
|
||
| local correctly_labeled | ||
| correctly_labeled="$(prune_stale_labels "$PR_NUMBER" "${size_label}")" | ||
|
|
||
| if [[ "${correctly_labeled}" != true ]]; then | ||
| api_call "issues/$PR_NUMBER/labels" -X POST -d "{\"labels\":[\"${size_label}\"]}" >/dev/null | ||
| info "Added label '${size_label}'" | ||
| fi | ||
| } | ||
|
|
||
| main "$@" | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.