Last active
July 15, 2024 07:54
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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