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