]> git.proxmox.com Git - ceph.git/blame - ceph/src/script/ceph-backport.sh
bump version to 19.2.0-pve1
[ceph.git] / ceph / src / script / ceph-backport.sh
CommitLineData
f67539c2
TL
1#!/usr/bin/env bash
2set -e
11fdf7f2 3#
9f95a23c
TL
4# ceph-backport.sh - Ceph backporting script
5#
6# Credits: This script is based on work done by Loic Dachary
7#
8#
9# This script automates the process of staging a backport starting from a
10# Backport tracker issue.
11#
12# Setup:
13#
14# ceph-backport.sh --setup
15#
16# Usage and troubleshooting:
17#
18# ceph-backport.sh --help
19# ceph-backport.sh --usage | less
20# ceph-backport.sh --troubleshooting | less
21#
22
23full_path="$0"
24
f67539c2 25SCRIPT_VERSION="16.0.0.6848"
9f95a23c
TL
26active_milestones=""
27backport_pr_labels=""
28backport_pr_number=""
29backport_pr_title=""
30backport_pr_url=""
31deprecated_backport_common="$HOME/bin/backport_common.sh"
32existing_pr_milestone_number=""
33github_token=""
34github_token_file="$HOME/.github_token"
35github_user=""
36milestone=""
37non_interactive=""
38original_issue=""
39original_issue_url=""
40original_pr=""
41original_pr_url=""
42redmine_key=""
43redmine_key_file="$HOME/.redmine_key"
44redmine_login=""
45redmine_user_id=""
46setup_ok=""
47this_script=$(basename "$full_path")
48
49if [[ $* == *--debug* ]]; then
50 set -x
51fi
52
53# associative array keyed on "component" strings from PR titles, mapping them to
54# GitHub PR labels that make sense in backports
55declare -A comp_hash=(
56["auth"]="core"
57["bluestore"]="bluestore"
58["build/ops"]="build/ops"
59["ceph.spec"]="build/ops"
60["ceph-volume"]="ceph-volume"
1e59de90 61["cephadm"]="cephadm"
9f95a23c
TL
62["cephfs"]="cephfs"
63["cmake"]="build/ops"
64["config"]="config"
65["client"]="cephfs"
66["common"]="common"
67["core"]="core"
68["dashboard"]="dashboard"
69["deb"]="build/ops"
70["doc"]="documentation"
71["grafana"]="monitoring"
72["mds"]="cephfs"
73["messenger"]="core"
74["mon"]="core"
75["msg"]="core"
1e59de90 76["mgr/cephadm"]="cephadm"
9f95a23c
TL
77["mgr/dashboard"]="dashboard"
78["mgr/prometheus"]="monitoring"
79["mgr"]="core"
80["monitoring"]="monitoring"
81["orch"]="orchestrator"
82["osd"]="core"
83["perf"]="performance"
84["prometheus"]="monitoring"
85["pybind"]="pybind"
86["py3"]="python3"
87["python3"]="python3"
88["qa"]="tests"
89["rbd"]="rbd"
90["rgw"]="rgw"
91["rpm"]="build/ops"
92["tests"]="tests"
93["tool"]="tools"
94)
95
96declare -A flagged_pr_hash=()
97
f51cf556
TL
98function run {
99 printf '%s\n' "$*" >&2
100 "$@"
101}
102
9f95a23c
TL
103function abort_due_to_setup_problem {
104 error "problem detected in your setup"
105 info "Run \"${this_script} --setup\" to fix"
106 false
107}
108
109function assert_fail {
110 local message="$1"
111 error "(internal error) $message"
112 info "This could be reported as a bug!"
113 false
114}
115
116function backport_pr_needs_label {
117 local check_label="$1"
118 local label
119 local needs_label="yes"
120 while read -r label ; do
121 if [ "$label" = "$check_label" ] ; then
122 needs_label=""
123 fi
124 done <<< "$backport_pr_labels"
125 echo "$needs_label"
126}
127
128function backport_pr_needs_milestone {
129 if [ "$existing_pr_milestone_number" ] ; then
130 echo ""
131 else
132 echo "yes"
133 fi
134}
135
136function bail_out_github_api {
137 local api_said="$1"
138 local hint="$2"
139 info "GitHub API said:"
140 log bare "$api_said"
141 if [ "$hint" ] ; then
142 info "(hint) $hint"
143 fi
144 abort_due_to_setup_problem
145}
146
147function blindly_set_pr_metadata {
148 local pr_number="$1"
149 local json_blob="$2"
150 curl -u ${github_user}:${github_token} --silent --data-binary "$json_blob" "https://api.github.com/repos/ceph/ceph/issues/${pr_number}" >/dev/null 2>&1 || true
151}
152
153function check_milestones {
154 local milestones_to_check
155 milestones_to_check="$(echo "$1" | tr '\n' ' ' | xargs)"
156 info "Active milestones: $milestones_to_check"
157 for m in $milestones_to_check ; do
158 info "Examining all PRs targeting base branch \"$m\""
159 vet_prs_for_milestone "$m"
160 done
161 dump_flagged_prs
162}
163
164function check_tracker_status {
165 local -a ok_statuses=("new" "need more info")
166 local ts="$1"
167 local error_msg
168 local tslc="${ts,,}"
169 local tslc_is_ok=
170 for oks in "${ok_statuses[@]}"; do
171 if [ "$tslc" = "$oks" ] ; then
172 debug "Tracker status $ts is OK for backport to proceed"
173 tslc_is_ok="yes"
174 break
175 fi
176 done
177 if [ "$tslc_is_ok" ] ; then
178 true
179 else
180 if [ "$tslc" = "in progress" ] ; then
181 error_msg="backport $redmine_url is already in progress"
182 else
183 error_msg="backport $redmine_url is closed (status: ${ts})"
184 fi
185 if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
186 warning "$error_msg"
187 else
188 error "$error_msg"
189 fi
190 fi
191 echo "$tslc_is_ok"
192}
193
194function cherry_pick_phase {
195 local base_branch
196 local default_val
197 local i
198 local merged
199 local number_of_commits
200 local offset
201 local sha1_to_cherry_pick
202 local singular_or_plural_commit
203 local yes_or_no_answer
204 populate_original_issue
205 if [ -z "$original_issue" ] ; then
206 error "Could not find original issue"
207 info "Does ${redmine_url} have a \"Copied from\" relation?"
208 false
209 fi
210 info "Parent issue: ${original_issue_url}"
211
212 populate_original_pr
213 if [ -z "$original_pr" ]; then
214 error "Could not find original PR"
215 info "Is the \"Pull request ID\" field of ${original_issue_url} populated?"
216 false
217 fi
218 info "Parent issue ostensibly fixed by: ${original_pr_url}"
219
220 verbose "Examining ${original_pr_url}"
221 remote_api_output=$(curl -u ${github_user}:${github_token} --silent "https://api.github.com/repos/ceph/ceph/pulls/${original_pr}")
222 base_branch=$(echo "${remote_api_output}" | jq -r '.base.label')
1e59de90 223 if [ "$base_branch" = "ceph:master" -o "$base_branch" = "ceph:main" ] ; then
9f95a23c
TL
224 true
225 else
226 if [ "$FORCE" ] ; then
1e59de90 227 warning "base_branch ->$base_branch<- is something other than \"ceph:master\" or \"ceph:main\""
9f95a23c
TL
228 info "--force was given, so continuing anyway"
229 else
230 error "${original_pr_url} is targeting ${base_branch}: cowardly refusing to perform automated cherry-pick"
1e59de90 231 info "Out of an abundance of caution, the script only automates cherry-picking of commits from PRs targeting \"ceph:master\" or \"ceph:main\"."
9f95a23c
TL
232 info "You can still use the script to stage the backport, though. Just prepare the local branch \"${local_branch}\" manually and re-run the script."
233 false
234 fi
235 fi
f51cf556
TL
236 base_sha="$(printf '%s' "${remote_api_output}" | jq -r .base.sha)"
237 head_sha="$(printf '%s' "${remote_api_output}" | jq -r .head.sha)"
9f95a23c
TL
238 merged=$(echo "${remote_api_output}" | jq -r '.merged')
239 if [ "$merged" = "true" ] ; then
f51cf556
TL
240 # Use the merge commit in case the branch HEAD changes after merge.
241 merge_commit_sha=$(printf '%s' "$remote_api_output" | jq -r '.merge_commit_sha')
242 if [ -z "$merge_commit_sha" ]; then
243 error "Could not determine the merge commit of ${original_pr_url}"
244 bail_out_github_api "$remote_api_output"
245 false
246 fi
247 cherry_pick_sha="${merge_commit_sha}^..${merge_commit_sha}^2"
9f95a23c 248 else
f51cf556
TL
249 if [ "$FORCE" ] ; then
250 warning "${original_pr_url} is not merged yet"
251 info "--force was given, so continuing anyway"
252 cherry_pick_sha="${base_sha}..${head_sha}"
9f95a23c 253 else
f51cf556
TL
254 error "${original_pr_url} is not merged yet"
255 info "Cowardly refusing to perform automated cherry-pick"
256 false
9f95a23c 257 fi
9f95a23c 258 fi
9f95a23c
TL
259
260 set -x
261 git fetch "$upstream_remote"
262
263 if git show-ref --verify --quiet "refs/heads/$local_branch" ; then
264 if [ "$FORCE" ] ; then
265 if [ "$non_interactive" ] ; then
266 git checkout "$local_branch"
267 git reset --hard "${upstream_remote}/${milestone}"
268 else
269 echo
270 echo "A local branch $local_branch already exists and the --force option was given."
271 echo "If you continue, any local changes in $local_branch will be lost!"
272 echo
273 default_val="y"
274 echo -n "Do you really want to overwrite ${local_branch}? (default: ${default_val}) "
275 yes_or_no_answer="$(get_user_input "$default_val")"
276 [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
277 if [ "$yes_or_no_answer" = "y" ] ; then
278 git checkout "$local_branch"
279 git reset --hard "${upstream_remote}/${milestone}"
280 else
281 info "OK, bailing out!"
282 false
283 fi
284 fi
285 else
286 set +x
287 maybe_restore_set_x
288 error "Cannot initialize $local_branch - local branch already exists"
289 false
290 fi
291 else
292 git checkout "${upstream_remote}/${milestone}" -b "$local_branch"
293 fi
294
295 git fetch "$upstream_remote" "pull/$original_pr/head:pr-$original_pr"
296
297 set +x
298 maybe_restore_set_x
f51cf556
TL
299 info "Attempting to cherry pick ${original_pr_url} into local branch $local_branch"
300 if ! run git cherry-pick -x "$cherry_pick_sha"; then
301 [ "$VERBOSE" ] && git status
302 error "Cherry pick failed due to conflicts?"
303 info "Manually fix conflicts and complete the current cherry-pick:"
304 info " git cherry-pick --continue"
305 info "Finally, re-run this script"
306 false
307 fi
308
9f95a23c
TL
309 info "Cherry picking completed without conflicts"
310}
311
312function clear_line {
313 log overwrite " \r"
314}
315
316function clip_pr_body {
317 local pr_body="$*"
318 local clipped=""
319 local last_line_was_blank=""
320 local line=""
321 local pr_json_tempfile=$(mktemp)
322 echo "$pr_body" | sed -n '/<!--.*/q;p' > "$pr_json_tempfile"
323 while IFS= read -r line; do
324 if [ "$(trim_whitespace "$line")" ] ; then
325 last_line_was_blank=""
326 clipped="${clipped}${line}\n"
327 else
328 if [ "$last_line_was_blank" ] ; then
329 true
330 else
331 clipped="${clipped}\n"
332 fi
333 fi
334 done < "$pr_json_tempfile"
335 rm "$pr_json_tempfile"
336 echo "$clipped"
337}
338
339function debug {
340 log debug "$@"
341}
342
343function display_version_message_and_exit {
f67539c2 344 echo "$this_script: Ceph backporting script, version $SCRIPT_VERSION"
9f95a23c
TL
345 exit 0
346}
347
348function dump_flagged_prs {
349 local url=
350 clear_line
351 if [ "${#flagged_pr_hash[@]}" -eq "0" ] ; then
352 info "All backport PRs appear to have milestone set correctly"
353 else
354 warning "Some backport PRs had problematic milestone settings"
355 log bare "==========="
356 log bare "Flagged PRs"
357 log bare "-----------"
358 for url in "${!flagged_pr_hash[@]}" ; do
359 log bare "$url - ${flagged_pr_hash[$url]}"
360 done
361 log bare "==========="
362 fi
363}
364
365function eol {
366 local mtt="$1"
367 error "$mtt is EOL"
368 false
369}
370
371function error {
372 log error "$@"
373}
374
375function existing_pr_routine {
376 local base_branch
377 local clipped_pr_body
378 local new_pr_body
379 local new_pr_title
380 local pr_body
381 local pr_json_tempfile
382 local remote_api_output
383 local update_pr_body
384 remote_api_output="$(curl -u ${github_user}:${github_token} --silent "https://api.github.com/repos/ceph/ceph/pulls/${backport_pr_number}")"
385 backport_pr_title="$(echo "$remote_api_output" | jq -r '.title')"
386 if [ "$backport_pr_title" = "null" ] ; then
387 error "could not get PR title of existing PR ${backport_pr_number}"
388 bail_out_github_api "$remote_api_output"
389 fi
390 existing_pr_milestone_number="$(echo "$remote_api_output" | jq -r '.milestone.number')"
391 if [ "$existing_pr_milestone_number" = "null" ] ; then
392 existing_pr_milestone_number=""
393 fi
394 backport_pr_labels="$(echo "$remote_api_output" | jq -r '.labels[].name')"
395 pr_body="$(echo "$remote_api_output" | jq -r '.body')"
396 if [ "$pr_body" = "null" ] ; then
397 error "could not get PR body of existing PR ${backport_pr_number}"
398 bail_out_github_api "$remote_api_output"
399 fi
400 base_branch=$(echo "${remote_api_output}" | jq -r '.base.label')
401 base_branch="${base_branch#ceph:}"
402 if [ -z "$(is_active_milestone "$base_branch")" ] ; then
403 error "existing PR $backport_pr_url is targeting $base_branch which is not an active milestone"
404 info "Cowardly refusing to work on a backport to $base_branch"
405 false
406 fi
407 clipped_pr_body="$(clip_pr_body "$pr_body")"
408 verbose_en "Clipped body of existing PR ${backport_pr_number}:\n${clipped_pr_body}"
409 if [[ "$backport_pr_title" =~ ^${milestone}: ]] ; then
410 verbose "Existing backport PR ${backport_pr_number} title has ${milestone} prepended"
411 else
412 warning "Existing backport PR ${backport_pr_number} title does NOT have ${milestone} prepended"
413 new_pr_title="${milestone}: $backport_pr_title"
414 if [[ "$new_pr_title" =~ \" ]] ; then
415 new_pr_title="${new_pr_title//\"/\\\"}"
416 fi
417 verbose "New PR title: ${new_pr_title}"
418 fi
419 redmine_url_without_scheme="${redmine_url//http?:\/\//}"
420 verbose "Redmine URL without scheme: $redmine_url_without_scheme"
421 if [[ "$clipped_pr_body" =~ $redmine_url_without_scheme ]] ; then
422 info "Existing backport PR ${backport_pr_number} already mentions $redmine_url"
423 if [ "$FORCE" ] ; then
424 warning "--force was given, so updating the PR body anyway"
425 update_pr_body="yes"
426 fi
427 else
428 warning "Existing backport PR ${backport_pr_number} does NOT mention $redmine_url - adding it"
429 update_pr_body="yes"
430 fi
431 if [ "$update_pr_body" ] ; then
432 new_pr_body="backport tracker: ${redmine_url}"
433 if [ "${original_pr_url}" ] ; then
434 new_pr_body="${new_pr_body}
435possibly a backport of ${original_pr_url}"
436 fi
437 if [ "${original_issue_url}" ] ; then
438 new_pr_body="${new_pr_body}
439parent tracker: ${original_issue_url}"
440 fi
441 new_pr_body="${new_pr_body}
442
443---
444
445original PR body:
446
447$clipped_pr_body
448
449---
450
451updated using ceph-backport.sh version ${SCRIPT_VERSION}"
452 fi
453 maybe_update_pr_title_body "${new_pr_title}" "${new_pr_body}"
454}
455
456function failed_mandatory_var_check {
457 local varname="$1"
458 local error="$2"
459 verbose "$varname $error"
460 setup_ok=""
461}
462
463function flag_pr {
464 local pr_num="$1"
465 local pr_url="$2"
466 local flag_reason="$3"
467 warning "flagging PR#${pr_num} because $flag_reason"
468 flagged_pr_hash["${pr_url}"]="$flag_reason"
469}
470
471function from_file {
472 local what="$1"
473 xargs 2>/dev/null < "$HOME/.${what}" || true
474}
475
476function get_user_input {
477 local default_val="$1"
478 local user_input=
479 read -r user_input
480 if [ "$user_input" ] ; then
481 echo "$user_input"
482 else
483 echo "$default_val"
484 fi
485}
486
487# takes a string and a substring - returns position of substring within string,
488# or -1 if not found
489# NOTE: position of first character in string is 0
490function grep_for_substr {
491 local str="$1"
492 local look_for_in_str="$2"
493 str="${str,,}"
494 munged="${str%%${look_for_in_str}*}"
495 if [ "$munged" = "$str" ] ; then
496 echo "-1"
497 else
498 echo "${#munged}"
499 fi
500}
501
502# takes PR title, attempts to guess component
503function guess_component {
504 local comp=
505 local pos="0"
506 local pr_title="$1"
507 local winning_comp=
508 local winning_comp_pos="9999"
509 for comp in "${!comp_hash[@]}" ; do
510 pos=$(grep_for_substr "$pr_title" "$comp")
511 # echo "$comp: $pos"
512 [ "$pos" = "-1" ] && continue
513 if [ "$pos" -lt "$winning_comp_pos" ] ; then
514 winning_comp_pos="$pos"
515 winning_comp="$comp"
516 fi
517 done
518 [ "$winning_comp" ] && echo "${comp_hash["$winning_comp"]}" || echo ""
519}
520
521function info {
522 log info "$@"
523}
524
525function init_endpoints {
526 verbose "Initializing remote API endpoints"
527 redmine_endpoint="${redmine_endpoint:-"https://tracker.ceph.com"}"
528 github_endpoint="${github_endpoint:-"https://github.com/ceph/ceph"}"
529}
530
531function init_fork_remote {
532 [ "$github_user" ] || assert_fail "github_user not set"
533 [ "$EXPLICIT_FORK" ] && info "Using explicit fork ->$EXPLICIT_FORK<- instead of personal fork."
534 fork_remote="${fork_remote:-$(maybe_deduce_remote fork)}"
535}
536
537function init_github_token {
538 github_token="$(from_file github_token)"
539 if [ "$github_token" ] ; then
540 true
541 else
542 warning "$github_token_file not populated: initiating interactive setup routine"
543 INTERACTIVE_SETUP_ROUTINE="yes"
544 fi
545}
546
547function init_redmine_key {
548 redmine_key="$(from_file redmine_key)"
549 if [ "$redmine_key" ] ; then
550 true
551 else
552 warning "$redmine_key_file not populated: initiating interactive setup routine"
553 INTERACTIVE_SETUP_ROUTINE="yes"
554 fi
555}
556
557function init_upstream_remote {
558 upstream_remote="${upstream_remote:-$(maybe_deduce_remote upstream)}"
559}
560
561function interactive_setup_routine {
562 local default_val
563 local original_github_token
564 local original_redmine_key
565 local total_steps
566 local yes_or_no_answer
567 original_github_token="$github_token"
568 original_redmine_key="$redmine_key"
569 total_steps="4"
570 if [ -e "$deprecated_backport_common" ] ; then
571 github_token=""
572 redmine_key=""
573 # shellcheck disable=SC1090
574 source "$deprecated_backport_common" 2>/dev/null || true
575 total_steps="$((total_steps+1))"
576 fi
577 echo
578 echo "Welcome to the ${this_script} interactive setup routine!"
579 echo
580 echo "---------------------------------------------------------------------"
581 echo "Setup step 1 of $total_steps - GitHub token"
582 echo "---------------------------------------------------------------------"
583 echo "For information on how to generate a GitHub personal access token"
584 echo "to use with this script, go to https://github.com/settings/tokens"
585 echo "then click on \"Generate new token\" and make sure the token has"
586 echo "\"Full control of private repositories\" scope."
eafe8130 587 echo
9f95a23c
TL
588 echo "For more details, see:"
589 echo "https://help.github.com/en/articles/creating-a-personal-access-token-for-the-command-line"
eafe8130 590 echo
9f95a23c
TL
591 echo -n "What is your GitHub token? "
592 default_val="$github_token"
593 [ "$github_token" ] && echo "(default: ${default_val})"
594 github_token="$(get_user_input "$default_val")"
595 if [ "$github_token" ] ; then
596 true
597 else
598 error "You must provide a valid GitHub personal access token"
599 abort_due_to_setup_problem
600 fi
601 [ "$github_token" ] || assert_fail "github_token not set, even after completing Step 1 of interactive setup"
eafe8130 602 echo
9f95a23c
TL
603 echo "---------------------------------------------------------------------"
604 echo "Setup step 2 of $total_steps - GitHub user"
605 echo "---------------------------------------------------------------------"
606 echo "The script will now attempt to determine your GitHub user (login)"
607 echo "from the GitHub token provided in the previous step. If this is"
608 echo "successful, there is a good chance that your GitHub token is OK."
609 echo
610 echo "Communicating with the GitHub API..."
611 set_github_user_from_github_token
612 [ "$github_user" ] || abort_due_to_setup_problem
613 echo
614 echo -n "Is the GitHub username (login) \"$github_user\" correct? "
615 default_val="y"
616 [ "$github_token" ] && echo "(default: ${default_val})"
617 yes_or_no_answer="$(get_user_input "$default_val")"
618 [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
619 if [ "$yes_or_no_answer" = "y" ] ; then
620 if [ "$github_token" = "$original_github_token" ] ; then
621 true
622 else
623 debug "GitHub personal access token changed"
624 echo "$github_token" > "$github_token_file"
625 chmod 0600 "$github_token_file"
626 info "Wrote GitHub personal access token to $github_token_file"
627 fi
628 else
629 error "GitHub user does not look right"
630 abort_due_to_setup_problem
631 fi
632 [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1 and 2 of interactive setup"
633 [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1 and 2 of interactive setup"
634 echo
635 echo "---------------------------------------------------------------------"
636 echo "Setup step 3 of $total_steps - remote repos"
637 echo "---------------------------------------------------------------------"
638 echo "Searching \"git remote -v\" for remote repos"
639 echo
640 init_upstream_remote
641 init_fork_remote
642 vet_remotes
643 echo "Upstream remote is \"$upstream_remote\""
644 echo "Fork remote is \"$fork_remote\""
645 [ "$setup_ok" ] || abort_due_to_setup_problem
646 [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1-3 of interactive setup"
647 [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1-3 of interactive setup"
648 [ "$upstream_remote" ] || assert_fail "upstream_remote not set, even after completing Steps 1-3 of interactive setup"
649 [ "$fork_remote" ] || assert_fail "fork_remote not set, even after completing Steps 1-3 of interactive setup"
650 echo
651 echo "---------------------------------------------------------------------"
652 echo "Setup step 4 of $total_steps - Redmine key"
653 echo "---------------------------------------------------------------------"
654 echo "To generate a Redmine API access key, go to https://tracker.ceph.com"
655 echo "After signing in, click: \"My account\""
656 echo "Now, find \"API access key\"."
657 echo "Once you know the API access key, enter it below."
658 echo
659 echo -n "What is your Redmine key? "
660 default_val="$redmine_key"
661 [ "$redmine_key" ] && echo "(default: ${default_val})"
662 redmine_key="$(get_user_input "$default_val")"
663 if [ "$redmine_key" ] ; then
664 set_redmine_user_from_redmine_key
665 if [ "$setup_ok" ] ; then
666 true
667 else
668 info "You must provide a valid Redmine API access key"
669 abort_due_to_setup_problem
670 fi
671 if [ "$redmine_key" = "$original_redmine_key" ] ; then
672 true
673 else
674 debug "Redmine API access key changed"
675 echo "$redmine_key" > "$redmine_key_file"
676 chmod 0600 "$redmine_key_file"
677 info "Wrote Redmine API access key to $redmine_key_file"
678 fi
679 else
680 error "You must provide a valid Redmine API access key"
681 abort_due_to_setup_problem
682 fi
683 [ "$github_token" ] || assert_fail "github_token not set, even after completing Steps 1-4 of interactive setup"
684 [ "$github_user" ] || assert_fail "github_user not set, even after completing Steps 1-4 of interactive setup"
685 [ "$upstream_remote" ] || assert_fail "upstream_remote not set, even after completing Steps 1-4 of interactive setup"
686 [ "$fork_remote" ] || assert_fail "fork_remote not set, even after completing Steps 1-4 of interactive setup"
687 [ "$redmine_key" ] || assert_fail "redmine_key not set, even after completing Steps 1-4 of interactive setup"
688 [ "$redmine_user_id" ] || assert_fail "redmine_user_id not set, even after completing Steps 1-4 of interactive setup"
689 [ "$redmine_login" ] || assert_fail "redmine_login not set, even after completing Steps 1-4 of interactive setup"
690 if [ "$total_steps" -gt "4" ] ; then
691 echo
692 echo "---------------------------------------------------------------------"
693 echo "Step 5 of $total_steps - delete deprecated $deprecated_backport_common file"
694 echo "---------------------------------------------------------------------"
695 fi
696 maybe_delete_deprecated_backport_common
697 vet_setup --interactive
698}
699
700function is_active_milestone {
701 local is_active=
702 local milestone_under_test="$1"
703 for m in $active_milestones ; do
704 if [ "$milestone_under_test" = "$m" ] ; then
705 verbose "Milestone $m is active"
706 is_active="yes"
707 break
708 fi
709 done
710 echo "$is_active"
711}
712
713function log {
714 local level="$1"
715 local trailing_newline="yes"
716 local in_hex=""
717 shift
718 local msg="$*"
719 prefix="${this_script}: "
720 verbose_only=
721 case $level in
722 bare)
723 prefix=
724 ;;
725 debug)
726 prefix="${prefix}DEBUG: "
727 verbose_only="yes"
728 ;;
729 err*)
730 prefix="${prefix}ERROR: "
731 ;;
732 hex)
733 in_hex="yes"
734 ;;
735 info)
736 :
737 ;;
738 overwrite)
739 trailing_newline=
740 prefix=
741 ;;
742 verbose)
743 verbose_only="yes"
744 ;;
745 verbose_en)
746 verbose_only="yes"
747 trailing_newline=
748 ;;
749 warn|warning)
750 prefix="${prefix}WARNING: "
751 ;;
752 esac
753 if [ "$in_hex" ] ; then
754 print_in_hex "$msg"
755 elif [ "$verbose_only" ] && [ -z "$VERBOSE" ] ; then
756 true
757 else
758 msg="${prefix}${msg}"
759 if [ "$trailing_newline" ] ; then
760 echo "${msg}" >&2
761 else
762 echo -en "${msg}" >&2
763 fi
764 fi
765}
766
767function maybe_deduce_remote {
768 local remote_type="$1"
769 local remote=""
770 local url_component=""
771 if [ "$remote_type" = "upstream" ] ; then
772 url_component="ceph"
773 elif [ "$remote_type" = "fork" ] ; then
774 if [ "$EXPLICIT_FORK" ] ; then
775 url_component="$EXPLICIT_FORK"
776 else
777 url_component="$github_user"
778 fi
779 else
780 assert_fail "bad remote_type ->$remote_type<- in maybe_deduce_remote"
781 fi
1e59de90 782 remote=$(git remote -v | grep --extended-regexp --ignore-case '(://|@)github.com(/|:|:/)'${url_component}'/ceph(\s|\.|\/)' | head -n1 | cut -f 1)
9f95a23c
TL
783 echo "$remote"
784}
785
786function maybe_delete_deprecated_backport_common {
787 local default_val
788 local user_inp
789 if [ -e "$deprecated_backport_common" ] ; then
790 echo "You still have a $deprecated_backport_common file,"
791 echo "which was used to store configuration parameters in version"
792 echo "15.0.0.6270 and earlier versions of ${this_script}."
793 echo
794 echo "Since $deprecated_backport_common has been deprecated in favor"
795 echo "of the interactive setup routine, which has been completed"
796 echo "successfully, the file should be deleted now."
797 echo
798 echo -n "Delete it now? (default: y) "
799 default_val="y"
800 user_inp="$(get_user_input "$default_val")"
801 user_inp="$(echo "$user_inp" | tr '[:upper:]' '[:lower:]' | xargs)"
802 if [ "$user_inp" ] ; then
803 user_inp="${user_inp:0:1}"
804 if [ "$user_inp" = "y" ] ; then
805 set -x
806 rm -f "$deprecated_backport_common"
807 set +x
808 maybe_restore_set_x
809 fi
810 fi
811 if [ -e "$deprecated_backport_common" ] ; then
812 error "$deprecated_backport_common still exists. Bailing out!"
813 false
814 fi
815 fi
816}
817
818function maybe_restore_set_x {
819 if [ "$DEBUG" ] ; then
820 set -x
821 fi
822}
823
824function maybe_update_pr_milestone_labels {
825 local component
826 local data_binary
827 local data_binary
828 local label
829 local needs_milestone
830 if [ "$EXPLICIT_COMPONENT" ] ; then
831 debug "Component given on command line: using it"
832 component="$EXPLICIT_COMPONENT"
833 else
834 debug "Attempting to guess component"
835 component=$(guess_component "$backport_pr_title")
836 fi
837 data_binary="{"
838 needs_milestone="$(backport_pr_needs_milestone)"
839 if [ "$needs_milestone" ] ; then
840 debug "Attempting to set ${milestone} milestone in ${backport_pr_url}"
841 data_binary="${data_binary}\"milestone\":${milestone_number}"
842 else
843 info "Backport PR ${backport_pr_url} already has ${milestone} milestone"
844 fi
845 if [ "$(backport_pr_needs_label "$component")" ] ; then
846 debug "Attempting to add ${component} label to ${backport_pr_url}"
847 if [ "$needs_milestone" ] ; then
848 data_binary="${data_binary},"
849 fi
850 data_binary="${data_binary}\"labels\":[\"${component}\""
851 while read -r label ; do
852 if [ "$label" ] ; then
853 data_binary="${data_binary},\"${label}\""
854 fi
855 done <<< "$backport_pr_labels"
856 data_binary="${data_binary}]}"
857 else
858 info "Backport PR ${backport_pr_url} already has label ${component}"
859 data_binary="${data_binary}}"
860 fi
861 if [ "$data_binary" = "{}" ] ; then
862 true
863 else
864 blindly_set_pr_metadata "$backport_pr_number" "$data_binary"
865 fi
866}
867
868function maybe_update_pr_title_body {
869 local new_title="$1"
870 local new_body="$2"
871 local data_binary
872 if [ "$new_title" ] && [ "$new_body" ] ; then
873 data_binary="{\"title\":\"${new_title}\", \"body\":\"$(munge_body "${new_body}")\"}"
874 elif [ "$new_title" ] ; then
875 data_binary="{\"title\":\"${new_title}\"}"
876 backport_pr_title="${new_title}"
877 elif [ "$new_body" ] ; then
878 data_binary="{\"body\":\"$(munge_body "${new_body}")\"}"
879 #log hex "${data_binary}"
880 #echo -n "${data_binary}"
881 fi
882 if [ "$data_binary" ] ; then
883 blindly_set_pr_metadata "${backport_pr_number}" "$data_binary"
884 fi
885}
886
887function milestone_number_from_remote_api {
888 local mtt="$1" # milestone to try
889 local mn="" # milestone number
890 local milestones
9f95a23c
TL
891 remote_api_output=$(curl -u ${github_user}:${github_token} --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones")
892 mn=$(echo "$remote_api_output" | jq --arg milestone "$mtt" '.[] | select(.title==$milestone) | .number')
893 if [ "$mn" -gt "0" ] >/dev/null 2>&1 ; then
894 echo "$mn"
895 else
896 error "Could not determine milestone number of ->$milestone<-"
897 verbose_en "GitHub API said:\n${remote_api_output}\n"
898 remote_api_output=$(curl -u ${github_user}:${github_token} --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones")
899 milestones=$(echo "$remote_api_output" | jq '.[].title')
900 info "Valid values are ${milestones}"
901 info "(This probably means the Release field of ${redmine_url} is populated with"
902 info "an unexpected value - i.e. it does not match any of the GitHub milestones.)"
903 false
904 fi
905}
906
907function munge_body {
908 echo "$new_body" | tr '\r' '\n' | sed 's/$/\\n/' | tr -d '\n'
909}
910
911function number_to_url {
912 local number_type="$1"
913 local number="$2"
914 if [ "$number_type" = "github" ] ; then
915 echo "${github_endpoint}/pull/${number}"
916 elif [ "$number_type" = "redmine" ] ; then
917 echo "${redmine_endpoint}/issues/${number}"
918 else
919 assert_fail "internal error in number_to_url: bad type ->$number_type<-"
920 fi
921}
922
923function populate_original_issue {
924 if [ -z "$original_issue" ] ; then
925 original_issue=$(curl --silent "${redmine_url}.json?include=relations" |
926 jq '.issue.relations[] | select(.relation_type | contains("copied_to")) | .issue_id')
927 original_issue_url="$(number_to_url "redmine" "${original_issue}")"
928 fi
929}
930
931function populate_original_pr {
932 if [ "$original_issue" ] ; then
933 if [ -z "$original_pr" ] ; then
934 original_pr=$(curl --silent "${original_issue_url}.json" |
935 jq -r '.issue.custom_fields[] | select(.id | contains(21)) | .value')
936 original_pr_url="$(number_to_url "github" "${original_pr}")"
937 fi
938 fi
939}
940
941function print_in_hex {
942 local str="$1"
943 local c
944
945 for (( i=0; i < ${#str}; i++ ))
946 do
947 c=${str:$i:1}
948 if [[ $c == ' ' ]]
949 then
950 printf "[%s] 0x%X\n" " " \'\ \' >&2
951 else
952 printf "[%s] 0x%X\n" "$c" \'"$c"\' >&2
953 fi
954 done
955}
956
957function set_github_user_from_github_token {
958 local quiet="$1"
959 local api_error
960 local curl_opts
961 setup_ok=""
962 [ "$github_token" ] || assert_fail "set_github_user_from_github_token: git_token not set"
963 curl_opts="--silent -u :${github_token} https://api.github.com/user"
964 [ "$quiet" ] || set -x
965 remote_api_output="$(curl $curl_opts)"
966 set +x
967 github_user=$(echo "${remote_api_output}" | jq -r .login 2>/dev/null | grep -v null || true)
968 api_error=$(echo "${remote_api_output}" | jq -r .message 2>/dev/null | grep -v null || true)
969 if [ "$api_error" ] ; then
970 info "GitHub API said: ->$api_error<-"
971 info "If you can't figure out what's wrong by examining the curl command and its output, above,"
972 info "please also study https://developer.github.com/v3/users/#get-the-authenticated-user"
973 github_user=""
974 else
975 [ "$github_user" ] || assert_fail "set_github_user_from_github_token: failed to set github_user"
976 info "my GitHub username is $github_user"
977 setup_ok="yes"
978 fi
979}
980
981function set_redmine_user_from_redmine_key {
982 [ "$redmine_key" ] || assert_fail "set_redmine_user_from_redmine_key was called, but redmine_key not set"
983 local api_key_from_api
984 remote_api_output="$(curl --silent "https://tracker.ceph.com/users/current.json?key=$redmine_key")"
985 redmine_login="$(echo "$remote_api_output" | jq -r '.user.login')"
986 redmine_user_id="$(echo "$remote_api_output" | jq -r '.user.id')"
987 api_key_from_api="$(echo "$remote_api_output" | jq -r '.user.api_key')"
988 if [ "$redmine_login" ] && [ "$redmine_user_id" ] && [ "$api_key_from_api" = "$redmine_key" ] ; then
989 [ "$redmine_user_id" ] || assert_fail "set_redmine_user_from_redmine_key: failed to set redmine_user_id"
990 [ "$redmine_login" ] || assert_fail "set_redmine_user_from_redmine_key: failed to set redmine_login"
991 info "my Redmine username is $redmine_login (ID $redmine_user_id)"
992 setup_ok="yes"
993 else
994 error "Redmine API access key $redmine_key is invalid"
995 redmine_login=""
996 redmine_user_id=""
997 setup_ok=""
998 fi
eafe8130
TL
999}
1000
9f95a23c
TL
1001function tracker_component_is_in_desired_state {
1002 local comp="$1"
1003 local val_is="$2"
1004 local val_should_be="$3"
1005 local in_desired_state
1006 if [ "$val_is" = "$val_should_be" ] ; then
1007 debug "Tracker $comp is in the desired state"
1008 in_desired_state="yes"
1009 fi
1010 echo "$in_desired_state"
1011}
1012
1013function tracker_component_was_updated {
1014 local comp="$1"
1015 local val_old="$2"
1016 local val_new="$3"
1017 local was_updated
1018 if [ "$val_old" = "$val_new" ] ; then
1019 true
1020 else
1021 debug "Tracker $comp was updated!"
1022 was_updated="yes"
1023 fi
1024 echo "$was_updated"
1025}
1026
1027function trim_whitespace {
1028 local var="$*"
1029 # remove leading whitespace characters
1030 var="${var#"${var%%[![:space:]]*}"}"
1031 # remove trailing whitespace characters
1032 var="${var%"${var##*[![:space:]]}"}"
1033 echo -n "$var"
1034}
1035
1036function troubleshooting_advice {
1037 cat <<EOM
1038Troubleshooting notes
1039---------------------
1040
1041If the script inexplicably fails with:
1042
1043 error: a cherry-pick or revert is already in progress
1044 hint: try "git cherry-pick (--continue | --quit | --abort)"
1045 fatal: cherry-pick failed
1046
1047This is because HEAD is not where git expects it to be:
1048
1049 $ git cherry-pick --abort
1050 warning: You seem to have moved HEAD. Not rewinding, check your HEAD!
1051
1052This can be fixed by issuing the command:
1053
1054 $ git cherry-pick --quit
1055
1056EOM
1057}
1058
1059# to update known milestones, consult:
1060# curl --verbose -X GET https://api.github.com/repos/ceph/ceph/milestones
1061function try_known_milestones {
1062 local mtt=$1 # milestone to try
1063 local mn="" # milestone number
1064 case $mtt in
1065 cuttlefish) eol "$mtt" ;;
1066 dumpling) eol "$mtt" ;;
1067 emperor) eol "$mtt" ;;
1068 firefly) eol "$mtt" ;;
1069 giant) eol "$mtt" ;;
1070 hammer) eol "$mtt" ;;
1071 infernalis) eol "$mtt" ;;
1072 jewel) mn="8" ;;
1073 kraken) eol "$mtt" ;;
1074 luminous) mn="10" ;;
1075 mimic) mn="11" ;;
1076 nautilus) mn="12" ;;
f67539c2 1077 octopus) mn="13" ;;
20effc67 1078 pacific) mn="14" ;;
1e59de90 1079 quincy) mn="15" ;;
f51cf556 1080 reef) mn="16" ;;
9f95a23c
TL
1081 esac
1082 echo "$mn"
1083}
1084
1085function update_version_number_and_exit {
1086 set -x
1087 local raw_version
1088 local munge_first_hyphen
1089 # munge_first_hyphen will look like this: 15.0.0.5774-g4c2f2eda969
1090 local script_version_number
1091 raw_version="$(git describe --long --match 'v*' | sed 's/^v//')" # example: "15.0.0-5774-g4c2f2eda969"
1092 munge_first_hyphen="${raw_version/-/.}" # example: "15.0.0.5774-g4c2f2eda969"
1093 script_version_number="${munge_first_hyphen%-*}" # example: "15.0.0.5774"
1094 sed -i -e "s/^SCRIPT_VERSION=.*/SCRIPT_VERSION=\"${script_version_number}\"/" "$full_path"
eafe8130 1095 exit 0
9f95a23c
TL
1096}
1097
1098function usage {
1099 cat <<EOM >&2
1100Setup:
1101
1102 ${this_script} --setup
1103
1104Documentation:
1105
1106 ${this_script} --help
1107 ${this_script} --usage | less
1108 ${this_script} --troubleshooting | less
eafe8130 1109
9f95a23c
TL
1110Usage:
1111 ${this_script} BACKPORT_TRACKER_ISSUE_NUMBER
eafe8130 1112
9f95a23c
TL
1113Options (not needed in normal operation):
1114 --cherry-pick-only (stop after cherry-pick phase)
1115 --component/-c COMPONENT
1116 (explicitly set the component label; if omitted, the
1117 script will try to guess the component)
1118 --debug (turns on "set -x")
1119 --existing-pr BACKPORT_PR_ID
1120 (use this when the backport PR is already open)
1121 --force (exercise caution!)
1122 --fork EXPLICIT_FORK (use EXPLICIT_FORK instead of personal GitHub fork)
1123 --milestones (vet all backport PRs for correct milestone setting)
1124 --setup/-s (run the interactive setup routine - NOTE: this can
1125 be done any number of times)
1126 --setup-report (check the setup and print a report)
1127 --update-version (this option exists as a convenience for the script
1128 maintainer only: not intended for day-to-day usage)
1129 --verbose/-v (produce more output than normal)
1130 --version (display version number and exit)
11fdf7f2 1131
9f95a23c
TL
1132Example:
1133 ${this_script} 31459
1134 (if cherry-pick conflicts are present, finish cherry-picking phase manually
1135 and then run the script again with the same argument)
1136
1137CAVEAT: The script must be run from inside a local git clone.
1138EOM
11fdf7f2
TL
1139}
1140
9f95a23c
TL
1141function usage_advice {
1142 cat <<EOM
1143Usage advice
1144------------
11fdf7f2 1145
9f95a23c
TL
1146Once you have completed --setup, you can run the script with the ID of
1147a Backport tracker issue. For example, to stage the backport
1148https://tracker.ceph.com/issues/41502, run:
1149
1150 ${this_script} 41502
1151
1e59de90 1152Provided the commits in the corresponding main PR cherry-pick cleanly, the
9f95a23c
TL
1153script will automatically perform all steps required to stage the backport:
1154
1155Cherry-pick phase:
1156
11571. fetching the latest commits from the upstream remote
11582. creating a wip branch for the backport
11593. figuring out which upstream PR contains the commits to cherry-pick
11604. cherry-picking the commits
1161
1162PR phase:
1163
11645. pushing the wip branch to your fork
11656. opening the backport PR with compliant title and description describing
1166 the backport
11677. (optionally) setting the milestone and label in the PR
11688. updating the Backport tracker issue
1169
1170When run with --cherry-pick-only, the script will stop after the cherry-pick
1171phase.
1172
1173If any of the commits do not cherry-pick cleanly, the script will abort in
1174step 4. In this case, you can either finish the cherry-picking manually
1175or abort the cherry-pick. In any case, when and if the local wip branch is
1176ready (all commits cherry-picked), if you run the script again, like so:
1177
1178 ${this_script} 41502
1179
1180the script will detect that the wip branch already exists and skip over
1181steps 1-4, starting from step 5 ("PR phase"). In other words, if the wip branch
1182already exists for any reason, the script will assume that the cherry-pick
1183phase (steps 1-4) is complete.
1184
1185As this implies, you can do steps 1-4 manually. Provided the wip branch name
1186is in the format wip-\$TRACKER_ID-\$STABLE_RELEASE (e.g. "wip-41502-mimic"),
1187the script will detect the wip branch and start from step 5.
1188
1189For details on all the options the script takes, run:
1190
1191 ${this_script} --help
1192
1193For more information on Ceph backporting, see:
1194
1e59de90 1195 https://github.com/ceph/ceph/tree/main/SubmittingPatches-backports.rst
9f95a23c
TL
1196
1197EOM
1198}
1199
1200function verbose {
1201 log verbose "$@"
11fdf7f2
TL
1202}
1203
9f95a23c
TL
1204function verbose_en {
1205 log verbose_en "$@"
1206}
1207
1208function vet_pr_milestone {
1209 local pr_number="$1"
1210 local pr_title="$2"
1211 local pr_url="$3"
1212 local milestone_stanza="$4"
1213 local milestone_title_should_be="$5"
1214 local milestone_number_should_be
1215 local milestone_number_is=
1216 local milestone_title_is=
1217 milestone_number_should_be="$(try_known_milestones "$milestone_title_should_be")"
1218 log overwrite "Vetting milestone of PR#${pr_number}\r"
1219 if [ "$milestone_stanza" = "null" ] ; then
1220 blindly_set_pr_metadata "$pr_number" "{\"milestone\": $milestone_number_should_be}"
1221 warning "$pr_url: set milestone to \"$milestone_title_should_be\""
1222 flag_pr "$pr_number" "$pr_url" "milestone not set"
1223 else
1224 milestone_title_is=$(echo "$milestone_stanza" | jq -r '.title')
1225 milestone_number_is=$(echo "$milestone_stanza" | jq -r '.number')
1226 if [ "$milestone_number_is" -eq "$milestone_number_should_be" ] ; then
1227 true
1228 else
1229 blindly_set_pr_metadata "$pr_number" "{\"milestone\": $milestone_number_should_be}"
1230 warning "$pr_url: changed milestone from \"$milestone_title_is\" to \"$milestone_title_should_be\""
1231 flag_pr "$pr_number" "$pr_url" "milestone set to wrong value \"$milestone_title_is\""
1232 fi
1233 fi
1234}
1235
1236function vet_prs_for_milestone {
1237 local milestone_title="$1"
1238 local pages_of_output=
1239 local pr_number=
1240 local pr_title=
1241 local pr_url=
1242 # determine last page (i.e., total number of pages)
1243 remote_api_output="$(curl -u ${github_user}:${github_token} --silent --head "https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}" | grep -E '^Link' || true)"
1244 if [ "$remote_api_output" ] ; then
1245 # Link: <https://api.github.com/repositories/2310495/pulls?base=luminous&page=2>; rel="next", <https://api.github.com/repositories/2310495/pulls?base=luminous&page=2>; rel="last"
1246 # shellcheck disable=SC2001
1247 pages_of_output="$(echo "$remote_api_output" | sed 's/^.*&page\=\([0-9]\+\)>; rel=\"last\".*$/\1/g')"
1248 else
1249 pages_of_output="1"
1250 fi
1251 verbose "GitHub has $pages_of_output pages of pull request data for \"base:${milestone_title}\""
1252 for ((page=1; page<=pages_of_output; page++)) ; do
1253 verbose "Fetching PRs (page $page of ${pages_of_output})"
1254 remote_api_output="$(curl -u ${github_user}:${github_token} --silent -X GET "https://api.github.com/repos/ceph/ceph/pulls?base=${milestone_title}&page=${page}")"
1255 prs_in_page="$(echo "$remote_api_output" | jq -r '. | length')"
1256 verbose "Page $page of remote API output contains information on $prs_in_page PRs"
1257 for ((i=0; i<prs_in_page; i++)) ; do
1258 pr_number="$(echo "$remote_api_output" | jq -r ".[${i}].number")"
1259 pr_title="$(echo "$remote_api_output" | jq -r ".[${i}].title")"
1260 pr_url="$(number_to_url "github" "${pr_number}")"
1261 milestone_stanza="$(echo "$remote_api_output" | jq -r ".[${i}].milestone")"
1262 vet_pr_milestone "$pr_number" "$pr_title" "$pr_url" "$milestone_stanza" "$milestone_title"
1263 done
1264 clear_line
1265 done
1266}
1267
1268function vet_remotes {
1269 if [ "$upstream_remote" ] ; then
1270 verbose "Upstream remote is $upstream_remote"
1271 else
1272 error "Cannot auto-determine upstream remote"
1273 "(Could not find any upstream remote in \"git remote -v\")"
1274 false
1275 fi
1276 if [ "$fork_remote" ] ; then
1277 verbose "Fork remote is $fork_remote"
1278 else
1279 error "Cannot auto-determine fork remote"
1280 if [ "$EXPLICIT_FORK" ] ; then
1281 info "(Could not find $EXPLICIT_FORK fork of ceph/ceph in \"git remote -v\")"
1282 else
1283 info "(Could not find GitHub user ${github_user}'s fork of ceph/ceph in \"git remote -v\")"
1284 fi
1285 setup_ok=""
1286 fi
1287}
1288
1289function vet_setup {
1290 local argument="$1"
1291 local not_set="!!! NOT SET !!!"
1292 local invalid="!!! INVALID !!!"
1293 local redmine_endpoint_display
1294 local redmine_user_id_display
1295 local github_endpoint_display
1296 local github_user_display
1297 local upstream_remote_display
1298 local fork_remote_display
1299 local redmine_key_display
1300 local github_token_display
1301 debug "Entering vet_setup with argument $argument"
1302 if [ "$argument" = "--report" ] || [ "$argument" = "--normal-operation" ] ; then
1303 [ "$github_token" ] && [ "$setup_ok" ] && set_github_user_from_github_token quiet
1304 init_upstream_remote
1305 [ "$github_token" ] && [ "$setup_ok" ] && init_fork_remote
1306 vet_remotes
1307 [ "$redmine_key" ] && set_redmine_user_from_redmine_key
1308 fi
1309 if [ "$github_token" ] ; then
1310 if [ "$setup_ok" ] ; then
1311 github_token_display="(OK; value not shown)"
1312 else
1313 github_token_display="$invalid"
1314 fi
1315 else
1316 github_token_display="$not_set"
1317 fi
1318 if [ "$redmine_key" ] ; then
1319 if [ "$setup_ok" ] ; then
1320 redmine_key_display="(OK; value not shown)"
1321 else
1322 redmine_key_display="$invalid"
1323 fi
1324 else
1325 redmine_key_display="$not_set"
1326 fi
1327 redmine_endpoint_display="${redmine_endpoint:-$not_set}"
1328 redmine_user_id_display="${redmine_user_id:-$not_set}"
1329 github_endpoint_display="${github_endpoint:-$not_set}"
1330 github_user_display="${github_user:-$not_set}"
1331 upstream_remote_display="${upstream_remote:-$not_set}"
1332 fork_remote_display="${fork_remote:-$not_set}"
1333 test "$redmine_endpoint" || failed_mandatory_var_check redmine_endpoint "not set"
1334 test "$redmine_user_id" || failed_mandatory_var_check redmine_user_id "could not be determined"
1335 test "$redmine_key" || failed_mandatory_var_check redmine_key "not set"
1336 test "$github_endpoint" || failed_mandatory_var_check github_endpoint "not set"
1337 test "$github_user" || failed_mandatory_var_check github_user "could not be determined"
1338 test "$github_token" || failed_mandatory_var_check github_token "not set"
1339 test "$upstream_remote" || failed_mandatory_var_check upstream_remote "could not be determined"
1340 test "$fork_remote" || failed_mandatory_var_check fork_remote "could not be determined"
1341 if [ "$argument" = "--report" ] || [ "$argument" == "--interactive" ] ; then
1342 read -r -d '' setup_summary <<EOM || true > /dev/null 2>&1
1343redmine_endpoint $redmine_endpoint
1344redmine_user_id $redmine_user_id_display
1345redmine_key $redmine_key_display
1346github_endpoint $github_endpoint
1347github_user $github_user_display
1348github_token $github_token_display
1349upstream_remote $upstream_remote_display
1350fork_remote $fork_remote_display
1351EOM
1352 log bare
1353 log bare "============================================="
1354 log bare " ${this_script} setup report"
1355 log bare "============================================="
1356 log bare "variable name value"
1357 log bare "---------------------------------------------"
1358 log bare "$setup_summary"
1359 log bare "---------------------------------------------"
1360 else
1361 verbose "redmine_endpoint $redmine_endpoint_display"
1362 verbose "redmine_user_id $redmine_user_id_display"
1363 verbose "redmine_key $redmine_key_display"
1364 verbose "github_endpoint $github_endpoint_display"
1365 verbose "github_user $github_user_display"
1366 verbose "github_token $github_token_display"
1367 verbose "upstream_remote $upstream_remote_display"
1368 verbose "fork_remote $fork_remote_display"
1369 fi
1370 if [ "$argument" = "--report" ] || [ "$argument" = "--interactive" ] ; then
1371 if [ "$setup_ok" ] ; then
1372 info "setup is OK"
1373 else
1374 info "setup is NOT OK"
1375 fi
1376 log bare "=============================================="
1377 log bare
1378 fi
1379}
1380
1381function warning {
1382 log warning "$@"
1383}
1384
1385
1386#
1387# are we in a local git clone?
1388#
1389
1390if git status >/dev/null 2>&1 ; then
1391 debug "In a local git clone. Good."
1392else
1393 error "This script must be run from inside a local git clone"
1394 abort_due_to_setup_problem
1395fi
1396
f67539c2
TL
1397#
1398# do we have jq available?
1399#
1400
1401if type jq >/dev/null 2>&1 ; then
1402 debug "jq is available. Good."
1403else
1404 error "This script uses jq, but it does not seem to be installed"
1405 abort_due_to_setup_problem
1406fi
1407
1408#
1409# is jq available?
1410#
1411
1412if command -v jq >/dev/null ; then
1413 debug "jq is available. Good."
1414else
1415 error "This script needs \"jq\" in order to work, and it is not available"
1416 abort_due_to_setup_problem
1417fi
1418
9f95a23c
TL
1419
1420#
1421# process command-line arguments
1422#
1423
1424munged_options=$(getopt -o c:dhsv --long "cherry-pick-only,component:,debug,existing-pr:,force,fork:,help,milestones,prepare,setup,setup-report,troubleshooting,update-version,usage,verbose,version" -n "$this_script" -- "$@")
1425eval set -- "$munged_options"
11fdf7f2 1426
9f95a23c
TL
1427ADVICE=""
1428CHECK_MILESTONES=""
1429CHERRY_PICK_ONLY=""
1430CHERRY_PICK_PHASE="yes"
1431DEBUG=""
1432EXISTING_PR=""
1433EXPLICIT_COMPONENT=""
1434EXPLICIT_FORK=""
1435FORCE=""
1436HELP=""
1437INTERACTIVE_SETUP_ROUTINE=""
1438ISSUE=""
1439PR_PHASE="yes"
1440SETUP_OPTION=""
1441TRACKER_PHASE="yes"
1442TROUBLESHOOTING_ADVICE=""
1443USAGE_ADVICE=""
1444VERBOSE=""
1445while true ; do
1446 case "$1" in
1447 --cherry-pick-only) CHERRY_PICK_PHASE="yes" ; PR_PHASE="" ; TRACKER_PHASE="" ; shift ;;
1448 --component|-c) shift ; EXPLICIT_COMPONENT="$1" ; shift ;;
1449 --debug|-d) DEBUG="$1" ; shift ;;
1450 --existing-pr) shift ; EXISTING_PR="$1" ; CHERRY_PICK_PHASE="" ; PR_PHASE="" ; shift ;;
1451 --force) FORCE="$1" ; shift ;;
1452 --fork) shift ; EXPLICIT_FORK="$1" ; shift ;;
1453 --help|-h) ADVICE="1" ; HELP="$1" ; shift ;;
1454 --milestones) CHECK_MILESTONES="$1" ; shift ;;
1455 --prepare) CHERRY_PICK_PHASE="yes" ; PR_PHASE="" ; TRACKER_PHASE="" ; shift ;;
1456 --setup*|-s) SETUP_OPTION="$1" ; shift ;;
1457 --troubleshooting) ADVICE="$1" ; TROUBLESHOOTING_ADVICE="$1" ; shift ;;
1458 --update-version) update_version_number_and_exit ;;
1459 --usage) ADVICE="$1" ; USAGE_ADVICE="$1" ; shift ;;
1460 --verbose|-v) VERBOSE="$1" ; shift ;;
1461 --version) display_version_message_and_exit ;;
1462 --) shift ; ISSUE="$1" ; break ;;
1463 *) echo "Internal error" ; false ;;
1464 esac
1465done
1466
1467if [ "$ADVICE" ] ; then
1468 [ "$HELP" ] && usage
1469 [ "$USAGE_ADVICE" ] && usage_advice
1470 [ "$TROUBLESHOOTING_ADVICE" ] && troubleshooting_advice
1471 exit 0
11fdf7f2 1472fi
11fdf7f2 1473
9f95a23c
TL
1474if [ "$SETUP_OPTION" ] || [ "$CHECK_MILESTONES" ] ; then
1475 ISSUE="0"
1476fi
11fdf7f2 1477
9f95a23c
TL
1478if [[ $ISSUE =~ ^[0-9]+$ ]] ; then
1479 issue=$ISSUE
11fdf7f2 1480else
9f95a23c
TL
1481 error "Invalid or missing argument"
1482 usage
1483 false
11fdf7f2 1484fi
11fdf7f2 1485
9f95a23c
TL
1486if [ "$DEBUG" ]; then
1487 set -x
1488 VERBOSE="--verbose"
11fdf7f2
TL
1489fi
1490
9f95a23c
TL
1491if [ "$VERBOSE" ]; then
1492 info "Verbose mode ON"
1493 VERBOSE="--verbose"
1494fi
11fdf7f2 1495
11fdf7f2 1496
9f95a23c
TL
1497#
1498# make sure setup has been completed
1499#
11fdf7f2 1500
9f95a23c
TL
1501init_endpoints
1502init_github_token
1503init_redmine_key
1504setup_ok="OK"
1505if [ "$SETUP_OPTION" ] ; then
1506 vet_setup --report
1507 maybe_delete_deprecated_backport_common
1508 if [ "$setup_ok" ] ; then
1509 exit 0
1510 else
1511 default_val="y"
1512 echo -n "Run the interactive setup routine now? (default: ${default_val}) "
1513 yes_or_no_answer="$(get_user_input "$default_val")"
1514 [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}"
1515 if [ "$yes_or_no_answer" = "y" ] ; then
1516 INTERACTIVE_SETUP_ROUTINE="yes"
1517 else
1518 if [ "$FORCE" ] ; then
1519 warning "--force was given; proceeding with broken setup"
1520 else
1521 info "Bailing out!"
1522 exit 1
1523 fi
1524 fi
1525 fi
1526fi
1527if [ "$INTERACTIVE_SETUP_ROUTINE" ] ; then
1528 interactive_setup_routine
1529else
1530 vet_setup --normal-operation
1531 maybe_delete_deprecated_backport_common
1532fi
1533if [ "$INTERACTIVE_SETUP_ROUTINE" ] || [ "$SETUP_OPTION" ] ; then
1534 echo
1535 if [ "$setup_ok" ] ; then
1536 if [ "$ISSUE" ] && [ "$ISSUE" != "0" ] ; then
1537 true
1538 else
1539 exit 0
1540 fi
1541 else
1542 exit 1
1543 fi
1544fi
1545vet_remotes
1546[ "$setup_ok" ] || abort_due_to_setup_problem
1547
1548#
1549# query remote GitHub API for active milestones
1550#
1551
1552verbose "Querying GitHub API for active milestones"
1553remote_api_output="$(curl -u ${github_user}:${github_token} --silent -X GET "https://api.github.com/repos/ceph/ceph/milestones")"
1554active_milestones="$(echo "$remote_api_output" | jq -r '.[] | .title')"
1555if [ "$active_milestones" = "null" ] ; then
1556 error "Could not determine the active milestones"
1557 bail_out_github_api "$remote_api_output"
1558fi
1559
1560if [ "$CHECK_MILESTONES" ] ; then
1561 check_milestones "$active_milestones"
1562 exit 0
1563fi
1564
1565#
1566# query remote Redmine API for information about the Backport tracker issue
1567#
1568
1569redmine_url="$(number_to_url "redmine" "${issue}")"
1570debug "Considering Redmine issue: $redmine_url - is it in the Backport tracker?"
1571
1572remote_api_output="$(curl --silent "${redmine_url}.json")"
1573tracker="$(echo "$remote_api_output" | jq -r '.issue.tracker.name')"
1574if [ "$tracker" = "Backport" ]; then
1575 debug "Yes, $redmine_url is a Backport issue"
1576else
1577 error "Issue $redmine_url is not a Backport"
1578 info "(This script only works with Backport tracker issues.)"
1579 false
1580fi
1581
1582debug "Looking up release/milestone of $redmine_url"
1583milestone="$(echo "$remote_api_output" | jq -r '.issue.custom_fields[0].value')"
1584if [ "$milestone" ] ; then
1585 debug "Release/milestone: $milestone"
1586else
1587 error "could not obtain release/milestone from ${redmine_url}"
1588 false
1589fi
1590
1591debug "Looking up status of $redmine_url"
1592tracker_status_id="$(echo "$remote_api_output" | jq -r '.issue.status.id')"
1593tracker_status_name="$(echo "$remote_api_output" | jq -r '.issue.status.name')"
1594if [ "$tracker_status_name" ] ; then
1595 debug "Tracker status: $tracker_status_name"
1596 if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
1597 test "$(check_tracker_status "$tracker_status_name")" || true
1598 else
1599 test "$(check_tracker_status "$tracker_status_name")"
1600 fi
1601else
1602 error "could not obtain status from ${redmine_url}"
1603 false
1604fi
1605
1606tracker_title="$(echo "$remote_api_output" | jq -r '.issue.subject')"
1607debug "Title of $redmine_url is ->$tracker_title<-"
1608
1609tracker_description="$(echo "$remote_api_output" | jq -r '.issue.description')"
1610debug "Description of $redmine_url is ->$tracker_description<-"
1611
1612tracker_assignee_id="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.id')"
1613tracker_assignee_name="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.name')"
1614if [ "$tracker_assignee_id" = "null" ] || [ "$tracker_assignee_id" = "$redmine_user_id" ] ; then
1615 true
1616else
1617 error_msg_1="$redmine_url is assigned to someone else: $tracker_assignee_name (ID $tracker_assignee_id)"
1618 error_msg_2="(my ID is $redmine_user_id)"
1619 if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
1620 warning "$error_msg_1"
1621 info "$error_msg_2"
1622 info "--force and/or --existing-pr given: continuing execution"
1623 else
1624 error "$error_msg_1"
1625 info "$error_msg_2"
1626 info "Cowardly refusing to continue"
1627 false
1628 fi
1629fi
1630
1631if [ -z "$(is_active_milestone "$milestone")" ] ; then
1632 error "$redmine_url is a backport to $milestone which is not an active milestone"
1633 info "Cowardly refusing to work on a backport to an inactive release"
1634 false
1635fi
1636
1637milestone_number=$(try_known_milestones "$milestone")
1638if [ "$milestone_number" -gt "0" ] >/dev/null 2>&1 ; then
f67539c2 1639 debug "Milestone ->$milestone<- is known to have number ->$milestone_number<-: skipping remote API call"
9f95a23c 1640else
f67539c2 1641 warning "Milestone ->$milestone<- is unknown to the script: falling back to GitHub API"
9f95a23c
TL
1642 milestone_number=$(milestone_number_from_remote_api "$milestone")
1643fi
f67539c2 1644target_branch="$milestone"
9f95a23c
TL
1645info "milestone/release is $milestone"
1646debug "milestone number is $milestone_number"
1647
1648if [ "$CHERRY_PICK_PHASE" ] ; then
1649 local_branch=wip-${issue}-${target_branch}
1650 if git show-ref --verify --quiet "refs/heads/$local_branch" ; then
1651 if [ "$FORCE" ] ; then
1652 warning "local branch $local_branch already exists"
1653 info "--force was given: will clobber $local_branch and attempt automated cherry-pick"
1654 cherry_pick_phase
1655 elif [ "$CHERRY_PICK_ONLY" ] ; then
1656 error "local branch $local_branch already exists"
1657 info "Cowardly refusing to clobber $local_branch as it might contain valuable data"
1658 info "(hint) run with --force to clobber it and attempt the cherry-pick"
1659 false
1660 fi
1661 if [ "$FORCE" ] || [ "$CHERRY_PICK_ONLY" ] ; then
1662 true
1663 else
1664 info "local branch $local_branch already exists: skipping cherry-pick phase"
1665 fi
1666 else
1667 info "$local_branch does not exist: will create it and attempt automated cherry-pick"
1668 cherry_pick_phase
1669 fi
1670fi
1671
1672if [ "$PR_PHASE" ] ; then
1673 current_branch=$(git rev-parse --abbrev-ref HEAD)
1674 if [ "$current_branch" = "$local_branch" ] ; then
1675 true
1676 else
1677 set -x
1678 git checkout "$local_branch"
1679 set +x
1680 maybe_restore_set_x
1681 fi
1682
1683 set -x
1684 git push -u "$fork_remote" "$local_branch"
1685 set +x
1686 maybe_restore_set_x
1687
1688 original_issue=""
1689 original_pr=""
1690 original_pr_url=""
1691
1692 debug "Generating backport PR description"
1693 populate_original_issue
1694 populate_original_pr
1695 desc="backport tracker: ${redmine_url}"
1696 if [ "$original_pr" ] || [ "$original_issue" ] ; then
1697 desc="${desc}\n\n---\n"
1698 [ "$original_pr" ] && desc="${desc}\nbackport of $(number_to_url "github" "${original_pr}")"
1699 [ "$original_issue" ] && desc="${desc}\nparent tracker: $(number_to_url "redmine" "${original_issue}")"
1700 fi
1e59de90 1701 desc="${desc}\n\nthis backport was staged using ceph-backport.sh version ${SCRIPT_VERSION}\nfind the latest version at ${github_endpoint}/blob/main/src/script/ceph-backport.sh"
9f95a23c
TL
1702
1703 debug "Generating backport PR title"
1704 if [ "$original_pr" ] ; then
1705 backport_pr_title="${milestone}: $(curl --silent https://api.github.com/repos/ceph/ceph/pulls/${original_pr} | jq -r '.title')"
1706 else
1707 if [[ $tracker_title =~ ^${milestone}: ]] ; then
1708 backport_pr_title="${tracker_title}"
1709 else
1710 backport_pr_title="${milestone}: ${tracker_title}"
1711 fi
1712 fi
1713 if [[ "$backport_pr_title" =~ \" ]] ; then
1714 backport_pr_title="${backport_pr_title//\"/\\\"}"
1715 fi
1716
1717 debug "Opening backport PR"
1718 if [ "$EXPLICIT_FORK" ] ; then
1719 source_repo="$EXPLICIT_FORK"
1720 else
1721 source_repo="$github_user"
1722 fi
1723 remote_api_output=$(curl -u ${github_user}:${github_token} --silent --data-binary "{\"title\":\"${backport_pr_title}\",\"head\":\"${source_repo}:${local_branch}\",\"base\":\"${target_branch}\",\"body\":\"${desc}\"}" "https://api.github.com/repos/ceph/ceph/pulls")
1724 backport_pr_number=$(echo "$remote_api_output" | jq -r .number)
1725 if [ -z "$backport_pr_number" ] || [ "$backport_pr_number" = "null" ] ; then
1726 error "failed to open backport PR"
1727 bail_out_github_api "$remote_api_output"
1728 fi
1729 backport_pr_url="$(number_to_url "github" "$backport_pr_number")"
1730 info "Opened backport PR ${backport_pr_url}"
1731fi
1732
1733if [ "$EXISTING_PR" ] ; then
1734 populate_original_issue
1735 populate_original_pr
1736 backport_pr_number="$EXISTING_PR"
1737 backport_pr_url="$(number_to_url "github" "$backport_pr_number")"
1738 existing_pr_routine
1739fi
1740
1741if [ "$PR_PHASE" ] || [ "$EXISTING_PR" ] ; then
1742 maybe_update_pr_milestone_labels
1743 pgrep firefox >/dev/null && firefox "${backport_pr_url}"
1744fi
1745
1746if [ "$TRACKER_PHASE" ] ; then
1747 debug "Considering Backport tracker issue ${redmine_url}"
1748 status_should_be=2 # In Progress
1749 desc_should_be="${backport_pr_url}"
1750 assignee_should_be="${redmine_user_id}"
1751 if [ "$EXISTING_PR" ] ; then
1752 data_binary="{\"issue\":{\"description\":\"${desc_should_be}\",\"status_id\":${status_should_be}}}"
1753 else
1754 data_binary="{\"issue\":{\"description\":\"${desc_should_be}\",\"status_id\":${status_should_be},\"assigned_to_id\":${assignee_should_be}}}"
1755 fi
1756 remote_api_status_code="$(curl --write-out '%{http_code}' --output /dev/null --silent -X PUT --header "Content-type: application/json" --data-binary "${data_binary}" "${redmine_url}.json?key=$redmine_key")"
1757 if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then
1758 true
1759 else
1760 if [ "${remote_api_status_code:0:1}" = "2" ] ; then
1761 true
1762 elif [ "${remote_api_status_code:0:1}" = "4" ] ; then
1763 warning "remote API ${redmine_endpoint} returned status ${remote_api_status_code}"
1764 info "This merely indicates that you cannot modify issue fields at ${redmine_endpoint}"
1765 info "and does not limit your ability to do backports."
1766 else
1767 error "Remote API ${redmine_endpoint} returned unexpected response code ${remote_api_status_code}"
1768 fi
1769 fi
1770 # check if anything actually changed on the Redmine issue
1771 remote_api_output=$(curl --silent "${redmine_url}.json?include=journals")
1772 status_is="$(echo "$remote_api_output" | jq -r '.issue.status.id')"
1773 desc_is="$(echo "$remote_api_output" | jq -r '.issue.description')"
1774 assignee_is="$(echo "$remote_api_output" | jq -r '.issue.assigned_to.id')"
1775 tracker_was_updated=""
1776 tracker_is_in_desired_state="yes"
1777 [ "$(tracker_component_was_updated "status" "$tracker_status_id" "$status_is")" ] && tracker_was_updated="yes"
1778 [ "$(tracker_component_was_updated "desc" "$tracker_description" "$desc_is")" ] && tracker_was_updated="yes"
1779 if [ "$EXISTING_PR" ] ; then
1780 true
1781 else
1782 [ "$(tracker_component_was_updated "assignee" "$tracker_assignee_id" "$assignee_is")" ] && tracker_was_updated="yes"
1783 fi
1784 [ "$(tracker_component_is_in_desired_state "status" "$status_is" "$status_should_be")" ] || tracker_is_in_desired_state=""
1785 [ "$(tracker_component_is_in_desired_state "desc" "$desc_is" "$desc_should_be")" ] || tracker_is_in_desired_state=""
1786 if [ "$EXISTING_PR" ] ; then
1787 true
1788 else
1789 [ "$(tracker_component_is_in_desired_state "assignee" "$assignee_is" "$assignee_should_be")" ] || tracker_is_in_desired_state=""
1790 fi
1791 if [ "$tracker_is_in_desired_state" ] ; then
1792 [ "$tracker_was_updated" ] && info "Backport tracker ${redmine_url} was updated"
1793 info "Backport tracker ${redmine_url} is in the desired state"
1794 pgrep firefox >/dev/null && firefox "${redmine_url}"
1795 exit 0
1796 fi
1797 if [ "$tracker_was_updated" ] ; then
1798 warning "backport tracker ${redmine_url} was updated, but is not in the desired state. Please check it."
1799 pgrep firefox >/dev/null && firefox "${redmine_url}"
1800 exit 1
1801 else
1802 data_binary="{\"issue\":{\"notes\":\"please link this Backport tracker issue with GitHub PR ${desc_should_be}\nceph-backport.sh version ${SCRIPT_VERSION}\"}}"
1803 remote_api_status_code=$(curl --write-out '%{http_code}' --output /dev/null --silent -X PUT --header "Content-type: application/json" --data-binary "${data_binary}" "${redmine_url}.json?key=$redmine_key")
1804 if [ "${remote_api_status_code:0:1}" = "2" ] ; then
1805 info "Comment added to ${redmine_url}"
1806 fi
1807 exit 0
1808 fi
1809fi