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