Skip to content

Commit 1756d01

Browse files
authored
Merge branch 'master' into mh-consolidate-cmake-configs
2 parents 3941a3f + 6999a9f commit 1756d01

File tree

3 files changed

+302
-0
lines changed

3 files changed

+302
-0
lines changed

.github/workflows/pr-labeler.yaml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
16+
# Label PRs with labels such as size. Note that this workflow is designed not to
17+
# fail if labeling actions encounter errors; instead, it writes error messages
18+
# as annotations on the workflow's run summary page. If labels don't seem to be
19+
# getting applied, check the run summary page for errors.
20+
# ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
21+
22+
name: Pull request labeler
23+
run-name: >-
24+
Label pull request ${{github.event.pull_request.number}} by ${{github.actor}}
25+
26+
on:
27+
# Note: do not copy-paste this workflow with `pull_request_target` left as-is.
28+
# Its use here is a special case where security implications are understood.
29+
# Workflows should normally use `pull_request` instead.
30+
pull_request_target:
31+
types:
32+
- opened
33+
- synchronize
34+
35+
workflow_dispatch:
36+
inputs:
37+
pr-number:
38+
description: 'The PR number of the PR to label:'
39+
type: string
40+
required: true
41+
debug:
42+
description: 'Run with debugging turned on'
43+
type: boolean
44+
default: true
45+
46+
permissions: read-all
47+
48+
jobs:
49+
label-pr-size:
50+
if: github.repository_owner == 'quantumlib'
51+
name: Update PR size labels
52+
runs-on: ubuntu-24.04
53+
timeout-minutes: 5
54+
permissions:
55+
contents: read
56+
issues: write
57+
pull-requests: write
58+
env:
59+
PR_NUMBER: ${{inputs.pr-number || github.event.pull_request.number}}
60+
# The next var is used by Bash. We add 'xtrace' to the options if this run
61+
# is a workflow_dispatch invocation and the user set the 'trace' option.
62+
SHELLOPTS: ${{inputs.debug && 'xtrace' || '' }}
63+
steps:
64+
- name: Check out a copy of the git repository
65+
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
66+
with:
67+
sparse-checkout: |
68+
./dev_tools/ci/size-labeler.sh
69+
70+
- name: Label the PR with a size label
71+
id: label
72+
continue-on-error: true
73+
env:
74+
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
75+
run: ./dev_tools/ci/size-labeler.sh

dev_tools/ci/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Continuous integration scripts
2+
3+
The scripts in this directory are used by the workflows in
4+
[`../../.github/workflows/`](../../.github/workflows/).

dev_tools/ci/size-labeler.sh

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
#!/usr/bin/env bash
2+
# Copyright 2025 The Cirq Developers
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# https://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
set -euo pipefail -o errtrace
17+
shopt -s inherit_errexit
18+
19+
declare -r usage="Usage: ${0##*/} [-h | --help | help]
20+
21+
Updates the size labels on a pull request based on the number of lines it
22+
changes. The script requires the following environment variables:
23+
PR_NUMBER, GITHUB_REPOSITORY, GITHUB_TOKEN. The script is intended
24+
for automated execution from GitHub Actions workflow."
25+
26+
declare -ar LABELS=(
27+
"Size: XS"
28+
"size: S"
29+
"size: M"
30+
"size: L"
31+
"size: XL"
32+
)
33+
34+
declare -A LIMITS=(
35+
["${LABELS[0]}"]=10
36+
["${LABELS[1]}"]=50
37+
["${LABELS[2]}"]=200
38+
["${LABELS[3]}"]=800
39+
["${LABELS[4]}"]="$((2 ** 63 - 1))"
40+
)
41+
42+
declare -ar IGNORED=(
43+
"*_pb2.py"
44+
"*_pb2.pyi"
45+
"*_pb2_grpc.py"
46+
".*.lock"
47+
"*.bundle.js"
48+
)
49+
50+
function info() {
51+
echo >&2 "INFO: ${*}"
52+
}
53+
54+
function error() {
55+
echo >&2 "ERROR: ${*}"
56+
}
57+
58+
function jq_stdin() {
59+
local infile
60+
infile="$(mktemp)"
61+
readonly infile
62+
local jq_status=0
63+
64+
cat >"${infile}"
65+
jq_file "$@" "${infile}" || jq_status="${?}"
66+
rm "${infile}"
67+
return "${jq_status}"
68+
}
69+
70+
function jq_file() {
71+
# Regardless of the success, store the return code.
72+
# Prepend each sttderr with command args and send back to stderr.
73+
jq "${@}" 2> >(awk -v h="stderr from jq ${*}:" '{print h, $0}' 1>&2) &&
74+
rc="${?}" ||
75+
rc="${?}"
76+
if [[ "${rc}" != "0" ]]; then
77+
error "The jq program failed: ${*}"
78+
error "Note the quotes above may be wrong. Here was the (possibly empty) input in ${*: -1}:"
79+
cat "${@: -1}" # Assumes last argument is input file!!
80+
fi
81+
return "${rc}"
82+
}
83+
84+
function api_call() {
85+
local -r endpoint="${1// /%20}" # love that our label names have spaces...
86+
local -r uri="https://api.github.com/repos/${GITHUB_REPOSITORY}"
87+
local response
88+
local curl_status=0
89+
info "Calling: ${uri}/${endpoint}"
90+
response="$(curl -sSL \
91+
--fail-with-body \
92+
--connect-timeout 10 --max-time 20 \
93+
-H "Authorization: token ${GITHUB_TOKEN}" \
94+
-H "Accept: application/vnd.github.v3.json" \
95+
-H "X-GitHub-Api-Version:2022-11-28" \
96+
-H "Content-Type: application/json" \
97+
"${@:2}" \
98+
"${uri}/${endpoint}"
99+
)" || curl_status="${?}"
100+
if [[ -n "${response}" ]]; then
101+
cat <<<"${response}"
102+
fi
103+
if (( curl_status )); then
104+
error "GitHub API call failed (curl exit $curl_status) for ${uri}/${endpoint}"
105+
error "Response body:"
106+
cat >&2 <<<"${response}"
107+
fi
108+
return "${curl_status}"
109+
}
110+
111+
function compute_changes() {
112+
local -r pr="$1"
113+
114+
local response
115+
local change_info
116+
local -r keys_filter='with_entries(select([.key] | inside(["changes", "filename"])))'
117+
response="$(api_call "pulls/${pr}/files")"
118+
change_info="$(jq_stdin "map(${keys_filter})" <<<"${response}")"
119+
120+
local files total_changes
121+
readarray -t files < <(jq_stdin -c '.[]' <<<"${change_info}")
122+
total_changes=0
123+
for file in "${files[@]}"; do
124+
local name changes
125+
name="$(jq_stdin -r '.filename' <<<"${file}")"
126+
for pattern in "${IGNORED[@]}"; do
127+
if [[ "$name" =~ ${pattern} ]]; then
128+
info "File $name ignored"
129+
continue 2
130+
fi
131+
done
132+
changes="$(jq_stdin -r '.changes' <<<"${file}")"
133+
info "File $name +-$changes"
134+
total_changes="$((total_changes + changes))"
135+
done
136+
echo "$total_changes"
137+
}
138+
139+
function get_size_label() {
140+
local -r changes="$1"
141+
for label in "${LABELS[@]}"; do
142+
local limit="${LIMITS["${label}"]}"
143+
if [[ "${changes}" -lt "${limit}" ]]; then
144+
echo "${label}"
145+
return
146+
fi
147+
done
148+
}
149+
150+
function prune_stale_labels() {
151+
local -r pr="$1"
152+
local -r size_label="$2"
153+
local response
154+
local existing_labels
155+
response="$(api_call "pulls/${pr}")"
156+
existing_labels="$(jq_stdin -r '.labels[] | .name' <<<"${response}")"
157+
readarray -t existing_labels <<<"${existing_labels}"
158+
159+
local correctly_labeled=false
160+
for label in "${existing_labels[@]}"; do
161+
[[ -z "${label}" ]] && continue
162+
# If the label we want is already present, we can just leave it there.
163+
if [[ "${label}" == "${size_label}" ]]; then
164+
info "Label '${label}' is correct, leaving it."
165+
correctly_labeled=true
166+
continue
167+
fi
168+
# If there is another size label, we need to remove it
169+
if [[ -v "LIMITS[${label}]" ]]; then
170+
info "Label '${label}' is stale, removing it."
171+
api_call "issues/${pr}/labels/${label}" -X DELETE >/dev/null
172+
continue
173+
fi
174+
info "Label '${label}' is unknown, leaving it."
175+
done
176+
echo "${correctly_labeled}"
177+
}
178+
179+
function main() {
180+
local moreinfo="(Use --help option for more info.)"
181+
if (( $# )); then
182+
case "$1" in
183+
-h | --help | help)
184+
echo "$usage"
185+
exit 0
186+
;;
187+
*)
188+
error "Invalid argument '$1'. ${moreinfo}"
189+
exit 2
190+
;;
191+
esac
192+
fi
193+
local env_var_name
194+
local env_var_missing=0
195+
for env_var_name in PR_NUMBER GITHUB_TOKEN GITHUB_REPOSITORY; do
196+
if [[ ! -v "${env_var_name}" ]]; then
197+
env_var_missing=1
198+
error "Missing environment variable ${env_var_name}"
199+
fi
200+
done
201+
if (( env_var_missing )); then
202+
error "${moreinfo}"
203+
exit 2
204+
fi
205+
206+
local total_changes
207+
total_changes="$(compute_changes "$PR_NUMBER")"
208+
info "Lines changed: ${total_changes}"
209+
210+
local size_label
211+
size_label="$(get_size_label "$total_changes")"
212+
info "Appropriate label is '${size_label}'"
213+
214+
local correctly_labeled
215+
correctly_labeled="$(prune_stale_labels "$PR_NUMBER" "${size_label}")"
216+
217+
if [[ "${correctly_labeled}" != true ]]; then
218+
api_call "issues/$PR_NUMBER/labels" -X POST -d "{\"labels\":[\"${size_label}\"]}" >/dev/null
219+
info "Added label '${size_label}'"
220+
fi
221+
}
222+
223+
main "$@"

0 commit comments

Comments
 (0)