diff --git a/tools/abandon_patches_on_unmaintained_branch.sh b/tools/abandon_patches_on_unmaintained_branch.sh new file mode 100755 index 0000000000..1646388e62 --- /dev/null +++ b/tools/abandon_patches_on_unmaintained_branch.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This script helps to abandon open patches when a given branch transitions +# to Unmaintained or End of Life and all patches on stable/ branch +# needs to be abandoned in order to be able to delete that branch. + +set -e + +if [[ $# -lt 2 ]]; then + echo "Usage: $(basename $0) [...]" + echo "repo should be e.g. glance" + echo + echo "Example: $(basename $0) yoga \$(list-deliverables --series yoga --is-eol)" + echo + echo " !!! WARNING: please do not run this script without discussing it" + echo " first with release managers!" + exit 1 +fi + +series="$1" +shift +repos="$@" + +function abandon_change { + gitid=$1 + msg=$2 + commit_message=$(ssh review.opendev.org "gerrit query $gitid --current-patch-set --format json" | head -n1 | jq .subject) + echo "Abandoning: $change -- $commit_message" + ssh review.opendev.org gerrit review $gitid --abandon --message \"$msg\" +} + + + +for repo in $repos; do + echo "Processing repository: $repo..." + open_changes=$( + ssh review.opendev.org "gerrit query --current-patch-set --format json \ + status:open branch:unmaintained/${series} project:openstack/${repo}" | \ + jq .currentPatchSet.revision | grep -v null | sed 's/"//g' + ) + + abandon_message=" +unmaintained/$series branch of openstack/$repo transitioned to End of Life +and is about to be deleted. +To be able to do that, all open patches need to be abandoned." + + for change in $open_changes; do + abandon_change $change "$abandon_message" + done +done + diff --git a/tools/delete_unmaintained_branch.py b/tools/delete_unmaintained_branch.py new file mode 100755 index 0000000000..0c016f53ad --- /dev/null +++ b/tools/delete_unmaintained_branch.py @@ -0,0 +1,114 @@ +#!/usr/bin/env python3 +# Copyright 2021 Ericsson Software Technology +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +# This script helps deleting unmaintained branches. Requires 'Delete reference' +# access category for 'refs/for/unmaintained/*' for the user who executes it. + +import argparse +import getpass +import json +import sys + +from oslo_utils import timeutils +import requests + + +GERRIT_URL = 'https://review.opendev.org/' +GITEA_URL = 'https://opendev.org/api/v1' + + +def delete_branch(username, project_name, branch_id): + print(f'!!! WARNING: You are about to delete unmaintained/{branch_id} ' + f'from {project_name} !!!') + gerrit_auth = requests.auth.HTTPBasicAuth( + username, + getpass.getpass('Gerrit password: ')) + url = (f'{GERRIT_URL}a/projects/{project_name.replace("/", "%2F")}/' + f'branches/unmaintained%2F{branch_id}') + response = requests.delete(url, auth=gerrit_auth) + if response.status_code == 204: + print(f'{timeutils.utcnow()} | Branch unmaintained/{branch_id} successfully deleted ' + f'from {project_name}!') + return 0 + elif response.status_code == 401: + print(f'{timeutils.utcnow()} | 401 Unauthorized.') + return 1 + else: + # NOTE(elod.illes): other possible errors from gerrit: + # 404: In case of project or branch is not found + # 409: Branch has open changes + print(f'{timeutils.utcnow()} | Delete failed ({response.status_code}): {response.text}') + return 2 + + +def is_branch_open(project_name, branch_id, quiet): + url = (f'{GITEA_URL}/repos/{project_name.replace("/", "%2F")}/' + f'branches/unmaintained%2F{branch_id}') + response = requests.get(url) + try: + response_details = response.json() + except json.decoder.JSONDecodeError as exc: + print(f'ERROR: JSON decode failed ({exc})') + print(f'ERROR: ({response.status_code}): {response.text}') + print('Is the project name correct, like "openstack/nova"?') + return 4 + if response.status_code == 200: + if not quiet: + print(f'unmaintained/{branch_id} exists in {project_name}.') + return 0 + elif ((response.status_code == 404) and + (response_details['errors'] == [f'branch does not exist [name: unmaintained/{branch_id}]']) and + (response_details['message'] == "The target couldn't be found.")): + if not quiet: + print(f'unmaintained/{branch_id} does not exist in {project_name}.') + return 1 + else: + print(f'ERROR: ({response.status_code}): {response.text}') + return 2 + + +def main(): + parser = argparse.ArgumentParser( + description='Deletes unmaintained/ from .') + subparsers = parser.add_subparsers(required=True, dest='command', + metavar='{delete,check}') + + delete_parser = subparsers.add_parser( + 'delete', help='Delete unmaintained/ from ') + delete_parser.add_argument('username', help='Gerrit Username') + delete_parser.add_argument('project', help='Project to delete from') + delete_parser.add_argument('branch', help='Branch to delete') + + check_parser = subparsers.add_parser( + 'check', help='Check if unmaintained/ exists for ') + check_parser.add_argument('project', help='Project to check') + check_parser.add_argument('branch', help='Branch to check if exists') + check_parser.add_argument( + '-q', '--quiet', action='store_true', + help='Return code only (0 means branch exists)') + + args = parser.parse_args() + if args.command == 'delete': + return delete_branch(args.username, + args.project, + args.branch.replace('unmaintained/', '')) + elif args.command == 'check': + return is_branch_open(args.project, + args.branch.replace('unmaintained/', ''), + args.quiet) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/tools/list_eol_stale_branches.sh b/tools/list_eol_stale_branches.sh index af3b9b3090..7d34920228 100755 --- a/tools/list_eol_stale_branches.sh +++ b/tools/list_eol_stale_branches.sh @@ -61,9 +61,7 @@ export PAGER= setup_temp_space 'list-eol-stale-branches' -branch=$(series_to_branch "$series") - -function no_open_patches { +function no_stable_open_patches { req="${GERRIT_URL}/changes/?q=status:open+project:${repo}+branch:stable/${eom_branch}" patches=$(curl -s ${req} | sed 1d | jq --raw-output '.[] | .change_id') [ -z "${patches}" ] @@ -75,7 +73,19 @@ function no_open_patches { return $no_opens } -function eol_tag_matches_head { +function no_unmaintained_open_patches { + req="${GERRIT_URL}/changes/?q=status:open+project:${repo}+branch:unmaintained/${eom_branch}" + patches=$(curl -s ${req} | sed 1d | jq --raw-output '.[] | .change_id') + [ -z "${patches}" ] + no_opens=$? + if [[ "$no_opens" -eq 1 ]]; then + echo "Patches remained open on stale branch (make sure to abandon them):" + echo "https://review.opendev.org/q/status:open+project:${repo}+branch:unmaintained/${eom_branch}" + fi + return $no_opens +} + +function eol_tag_matches_stable_head { head=$(git log --oneline --decorate -1) [[ "$head" =~ "${eom_branch}-eol" ]] && [[ "$head" =~ "origin/stable/${eom_branch}" ]] matches=$? @@ -100,14 +110,39 @@ function eol_tag_matches_head { return $matches } -function is_eol { +function eol_tag_matches_unmaintained_head { + head=$(git log --oneline --decorate -1) + [[ "$head" =~ "${eom_branch}-eol" ]] && [[ "$head" =~ "origin/unmaintained/${eom_branch}" ]] + matches=$? + if [[ "$matches" -eq 1 ]] ; then + tags=$(git tag) + [[ "$tags" =~ "${eom_branch}-eol" ]] + eol_tag_exists=$? + if [[ "$eol_tag_exists" -eq 0 ]]; then + echo "WARNING !!! unmaintained/${eom_branch} has patches on top of the ${eom_branch}-eol tag." + echo "Please check the branch and ${eom_branch}-eol tag manually." + echo "Do not delete the branch if you are not sure!" + read -p "> If you are sure the branch can be deleted, then press D + Enter: " DELETE + if [ "${DELETE,,}" == "d" ]; then + matches=0 + else + echo "Skipping." + fi + else + echo "No ${eom_branch}-eol tag found! Branch cannot be deleted. Skipping." + fi + fi + return $matches +} + +function is_stable_eol { ${TOOLSDIR}/delete_stable_branch.py check --quiet ${repo} ${eom_branch} if [[ $? -eq 0 ]]; then echo echo "${repo} contains eol stale branch (${eom_branch})" clone_repo ${repo} stable/${eom_branch} cd ${repo} - if no_open_patches && eol_tag_matches_head; then + if no_stable_open_patches && eol_tag_matches_stable_head; then read -p "> Do you want to delete the branch stable/${eom_branch} from ${repo} repository? [y/N]: " YN if [ "${YN,,}" == "y" ]; then if [ -z "$gerrit_username" ]; then @@ -120,14 +155,36 @@ function is_eol { fi } -for eom_branch in "${eom_series[@]}"; do - repos=$(list-deliverables -r --series "${eom_branch}" --is-eol) +function is_unmaintained_eol { + ${TOOLSDIR}/delete_unmaintained_branch.py check --quiet ${repo} ${eom_branch} + if [[ $? -eq 0 ]]; then + echo + echo "${repo} contains eol stale branch (${eom_branch})" + clone_repo ${repo} unmaintained/${eom_branch} + cd ${repo} + if no_unmaintained_open_patches && eol_tag_matches_unmaintained_head; then + read -p "> Do you want to delete the branch unmaintained/${eom_branch} from ${repo} repository? [y/N]: " YN + if [ "${YN,,}" == "y" ]; then + if [ -z "$gerrit_username" ]; then + read -p "Gerrit username: " gerrit_username + fi + ${TOOLSDIR}/delete_unmaintained_branch.py delete ${gerrit_username} ${repo} ${eom_branch} + fi + fi + cd .. + fi +} + +for eom_series in "${eom_series[@]}"; do + eom_branch=$(python -c "from openstack_releases import gitutils; print(gitutils.get_stable_branch_id(\"$eom_series\"))") + repos=$(list-deliverables -r --series "$eom_series" --is-eol) # Show the eol stale branches for each repository. for repo in ${repos}; do cd ${MYTMPDIR} echo echo " --- $repo ($eom_branch) --- " - is_eol "${repo}" "${eom_branch}" + is_stable_eol "${repo}" "${eom_branch}" + is_unmaintained_eol "${repo}" "${eom_branch}" done done