Skip to content

Instantly share code, notes, and snippets.

@artemptushkin
Last active July 15, 2024 07:54
Show Gist options
  • Save artemptushkin/268f2d058d45521acff8d1cc0a4d4581 to your computer and use it in GitHub Desktop.
Save artemptushkin/268f2d058d45521acff8d1cc0a4d4581 to your computer and use it in GitHub Desktop.
Python script and gitlab job to remove stale branches from a configured projects, see https://dev.to/art_ptushkin/gitlab-python-based-job-to-remove-stale-branches-1i6i
remove-stale-branches:
needs: []
image: python:slim
variables:
THRESHOLD_PERIOD_DAYS: 30
STALE_PROJECT_NAMESPACES: "$CI_PROJECT_PATH"
parallel:
matrix:
- STALE_PROJECT_NAMESPACES: "foo/baz"
STALE_EXCLUSION_PATTERNS: "example/.*"
rules:
- if: "$CI_PIPELINE_SOURCE == 'schedule'"
when: always
script:
- pip install requests
- python3 clean-stale-branches.py --project-namespace=$STALE_PROJECT_NAMESPACES --days-threshold=$THRESHOLD_PERIOD_DAYS --ignore-patterns=$STALE_EXCLUSION_PATTERNS
import os
import requests
from urllib.parse import quote
import re
import argparse
from datetime import datetime, timedelta, timezone
def get_arguments():
parser = argparse.ArgumentParser(description="Remove outdated GitLab branches based on specified criteria.")
parser.add_argument("--days-threshold", type=int, required=True, default=30, help="Number of days to consider a branch stale.")
parser.add_argument("--namespace", required=True, help="Namespace of the GitLab project.")
parser.add_argument("--ignore-patterns", help="Comma-separated string of patterns to exclude branches from deletion.")
return parser.parse_args()
def main():
args = get_arguments()
days_limit = args.days_threshold
project_space = args.namespace
namespace_encoded = quote(project_space, safe='')
access_token = os.environ.get("GITLAB_ACCESS_TOKEN")
if access_token is None:
print("Error: Missing GitLab access token in environment variables.")
exit(1)
request_headers = {"PRIVATE-TOKEN": access_token}
ignore_list = args.ignore_patterns.split(",") if args.ignore_patterns else []
ignore_regex = [re.compile(pattern) for pattern in ignore_list]
group_api_url = f"https://gitlab.com/api/v4/groups/{namespace_encoded}"
project_api_url = f"https://gitlab.com/api/v4/projects/{namespace_encoded}"
group_response = requests.get(group_api_url, headers=request_headers)
if group_response.status_code == 404:
print(f"Identified {namespace_encoded} as a project")
project_info = requests.get(project_api_url, headers=request_headers).json()
handle_project(ignore_regex, project_info['id'], request_headers, days_limit, project_info['name'])
else:
print(f"Identified {namespace_encoded} as a group, processing all contained projects")
group_info = group_response.json()
for project in group_info['projects']:
handle_project(ignore_regex, project['id'], request_headers, days_limit, project['name'])
def handle_project(ignore_regex, project_id, headers, days_limit, project_name):
print("=========")
print(f"Working on project {project_name}")
branches_api_url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/branches?per_page=100"
branches_response = requests.get(branches_api_url, headers=headers)
if branches_response.status_code == 200:
branches_info = branches_response.json()
stale_date = datetime.utcnow() - timedelta(days=days_limit)
stale_date = stale_date.replace(tzinfo=timezone.utc)
stale_branches = [
branch for branch in branches_info
if
(datetime.fromisoformat(branch['commit']['committed_date']).replace(tzinfo=timezone.utc) < stale_date)
and not any(regex.match(branch['name']) for regex in ignore_regex)
and not branch.get('protected', True)
]
print(f"Branches not updated in over {days_limit} days, count:", len(stale_branches))
print(f"Branches to be deleted:\n", [branch['name'] for branch in stale_branches])
for branch in stale_branches:
branch_encoded = quote(branch['name'], safe='')
delete_api_url = f"https://gitlab.com/api/v4/projects/{project_id}/repository/branches/{branch_encoded}"
delete_response = requests.delete(delete_api_url, headers=headers)
if delete_response.status_code == 204:
print(f"Successfully deleted branch '{branch['name']}'.")
else:
print(f"Failed to delete branch '{branch['name']}': Status Code {delete_response.status_code}")
else:
print(f"Error: Unable to fetch branches. Status Code: {branches_response.status_code}")
exit(1)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment