#!/usr/bin/env bash set -e # # ceph-backport.sh - Ceph backporting script # # Credits: This script is based on work done by Loic Dachary # # # This script automates the process of staging a backport starting from a # Backport tracker issue. # # Setup: # # ceph-backport.sh --setup # # Usage and troubleshooting: # # ceph-backport.sh --help # ceph-backport.sh --usage | less # ceph-backport.sh --troubleshooting | less # full_path="$0" SCRIPT_VERSION="16.0.0.6848" active_milestones="" backport_pr_labels="" backport_pr_number="" backport_pr_title="" backport_pr_url="" deprecated_backport_common="$HOME/bin/backport_common.sh" existing_pr_milestone_number="" github_token="" github_token_file="$HOME/.github_token" github_user="" milestone="" non_interactive="" original_issue="" original_issue_url="" original_pr="" original_pr_url="" redmine_key="" redmine_key_file="$HOME/.redmine_key" redmine_login="" redmine_user_id="" setup_ok="" this_script=$(basename "$full_path") if [[ $* == *--debug* ]]; then set -x fi # associative array keyed on "component" strings from PR titles, mapping them to # GitHub PR labels that make sense in backports declare -A comp_hash=( ["auth"]="core" ["bluestore"]="bluestore" ["build/ops"]="build/ops" ["ceph.spec"]="build/ops" ["ceph-volume"]="ceph-volume" ["cephfs"]="cephfs" ["cmake"]="build/ops" ["config"]="config" ["client"]="cephfs" ["common"]="common" ["core"]="core" ["dashboard"]="dashboard" ["deb"]="build/ops" ["doc"]="documentation" ["grafana"]="monitoring" ["mds"]="cephfs" ["messenger"]="core" ["mon"]="core" ["msg"]="core" ["mgr/dashboard"]="dashboard" ["mgr/prometheus"]="monitoring" ["mgr"]="core" ["monitoring"]="monitoring" ["orch"]="orchestrator" ["osd"]="core" ["perf"]="performance" ["prometheus"]="monitoring" ["pybind"]="pybind" ["py3"]="python3" ["python3"]="python3" ["qa"]="tests" ["rbd"]="rbd" ["rgw"]="rgw" ["rpm"]="build/ops" ["tests"]="tests" ["tool"]="tools" ) declare -A flagged_pr_hash=() function abort_due_to_setup_problem { error "problem detected in your setup" info "Run \"${this_script} --setup\" to fix" false } function assert_fail { local message="$1" error "(internal error) $message" info "This could be reported as a bug!" false } function backport_pr_needs_label { local check_label="$1" local label local needs_label="yes" while read -r label ; do if [ "$label" = "$check_label" ] ; then needs_label="" fi done <<< "$backport_pr_labels" echo "$needs_label" } function backport_pr_needs_milestone { if [ "$existing_pr_milestone_number" ] ; then echo "" else echo "yes" fi } function bail_out_github_api { local api_said="$1" local hint="$2" info "GitHub API said:" log bare "$api_said" if [ "$hint" ] ; then info "(hint) $hint" fi abort_due_to_setup_problem } function blindly_set_pr_metadata { local pr_number="$1" local json_blob="$2" 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 } function check_milestones { local milestones_to_check milestones_to_check="$(echo "$1" | tr '\n' ' ' | xargs)" info "Active milestones: $milestones_to_check" for m in $milestones_to_check ; do info "Examining all PRs targeting base branch \"$m\"" vet_prs_for_milestone "$m" done dump_flagged_prs } function check_tracker_status { local -a ok_statuses=("new" "need more info") local ts="$1" local error_msg local tslc="${ts,,}" local tslc_is_ok= for oks in "${ok_statuses[@]}"; do if [ "$tslc" = "$oks" ] ; then debug "Tracker status $ts is OK for backport to proceed" tslc_is_ok="yes" break fi done if [ "$tslc_is_ok" ] ; then true else if [ "$tslc" = "in progress" ] ; then error_msg="backport $redmine_url is already in progress" else error_msg="backport $redmine_url is closed (status: ${ts})" fi if [ "$FORCE" ] || [ "$EXISTING_PR" ] ; then warning "$error_msg" else error "$error_msg" fi fi echo "$tslc_is_ok" } function cherry_pick_phase { local base_branch local default_val local i local merged local number_of_commits local offset local sha1_to_cherry_pick local singular_or_plural_commit local yes_or_no_answer populate_original_issue if [ -z "$original_issue" ] ; then error "Could not find original issue" info "Does ${redmine_url} have a \"Copied from\" relation?" false fi info "Parent issue: ${original_issue_url}" populate_original_pr if [ -z "$original_pr" ]; then error "Could not find original PR" info "Is the \"Pull request ID\" field of ${original_issue_url} populated?" false fi info "Parent issue ostensibly fixed by: ${original_pr_url}" verbose "Examining ${original_pr_url}" remote_api_output=$(curl -u ${github_user}:${github_token} --silent "https://api.github.com/repos/ceph/ceph/pulls/${original_pr}") base_branch=$(echo "${remote_api_output}" | jq -r '.base.label') if [ "$base_branch" = "ceph:master" ] ; then true else if [ "$FORCE" ] ; then warning "base_branch ->$base_branch<- is something other than \"ceph:master\"" info "--force was given, so continuing anyway" else error "${original_pr_url} is targeting ${base_branch}: cowardly refusing to perform automated cherry-pick" info "Out of an abundance of caution, the script only automates cherry-picking of commits from PRs targeting \"ceph:master\"." 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." false fi fi merged=$(echo "${remote_api_output}" | jq -r '.merged') if [ "$merged" = "true" ] ; then true else error "${original_pr_url} is not merged yet" info "Cowardly refusing to perform automated cherry-pick" false fi number_of_commits=$(echo "${remote_api_output}" | jq '.commits') if [ "$number_of_commits" -eq "$number_of_commits" ] 2>/dev/null ; then # \$number_of_commits is set, and is an integer if [ "$number_of_commits" -eq "1" ] ; then singular_or_plural_commit="commit" else singular_or_plural_commit="commits" fi else error "Could not determine the number of commits in ${original_pr_url}" bail_out_github_api "$remote_api_output" fi info "Found $number_of_commits $singular_or_plural_commit in $original_pr_url" set -x git fetch "$upstream_remote" if git show-ref --verify --quiet "refs/heads/$local_branch" ; then if [ "$FORCE" ] ; then if [ "$non_interactive" ] ; then git checkout "$local_branch" git reset --hard "${upstream_remote}/${milestone}" else echo echo "A local branch $local_branch already exists and the --force option was given." echo "If you continue, any local changes in $local_branch will be lost!" echo default_val="y" echo -n "Do you really want to overwrite ${local_branch}? (default: ${default_val}) " yes_or_no_answer="$(get_user_input "$default_val")" [ "$yes_or_no_answer" ] && yes_or_no_answer="${yes_or_no_answer:0:1}" if [ "$yes_or_no_answer" = "y" ] ; then git checkout "$local_branch" git reset --hard "${upstream_remote}/${milestone}" else info "OK, bailing out!" false fi fi else set +x maybe_restore_set_x error "Cannot initialize $local_branch - local branch already exists" false fi else git checkout "${upstream_remote}/${milestone}" -b "$local_branch" fi git fetch "$upstream_remote" "pull/$original_pr/head:pr-$original_pr" set +x maybe_restore_set_x info "Attempting to cherry pick $number_of_commits commits from ${original_pr_url} into local branch $local_branch" offset="$((number_of_commits - 1))" || true for ((i=offset; i>=0; i--)) ; do info "Running \"git cherry-pick -x\" on $(git log --oneline --max-count=1 --no-decorate "pr-${original_pr}~${i}")" sha1_to_cherry_pick=$(git rev-parse --verify "pr-${original_pr}~${i}") set -x if git cherry-pick -x "$sha1_to_cherry_pick" ; then set +x maybe_restore_set_x else set +x maybe_restore_set_x [ "$VERBOSE" ] && git status error "Cherry pick failed" info "Next, manually fix conflicts and complete the current cherry-pick" if [ "$i" -gt "0" ] >/dev/null 2>&1 ; then info "Then, cherry-pick the remaining commits from ${original_pr_url}, i.e.:" for ((j=i-1; j>=0; j--)) ; do info "-> missing commit: $(git log --oneline --max-count=1 --no-decorate "pr-${original_pr}~${j}")" done info "Finally, re-run the script" else info "Then re-run the script" fi false fi done info "Cherry picking completed without conflicts" } function clear_line { log overwrite " \r" } function clip_pr_body { local pr_body="$*" local clipped="" local last_line_was_blank="" local line="" local pr_json_tempfile=$(mktemp) echo "$pr_body" | sed -n '/