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