├── .github └── workflows │ ├── build-and-push-image.yaml │ ├── cleanup.yaml │ └── tests.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── main.py ├── requirements.txt └── src ├── __init__.py ├── actions.py ├── github.py ├── io.py └── requests.py /.github/workflows/build-and-push-image.yaml: -------------------------------------------------------------------------------- 1 | name: Build and publish docker image 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | 8 | # Allow workflow to be manually run from the GitHub UI 9 | workflow_dispatch: 10 | 11 | jobs: 12 | build_and_push: 13 | runs-on: ubuntu-latest 14 | name: Builds the image and publishes to docker hub 15 | steps: 16 | - name: Check out the repo 17 | uses: actions/checkout@v3 18 | 19 | - name: Set up QEMU 20 | uses: docker/setup-qemu-action@v2 21 | 22 | - name: Set up Docker Buildx 23 | uses: docker/setup-buildx-action@v2 24 | 25 | - name: Login to DockerHub 26 | uses: docker/login-action@v2 27 | with: 28 | username: ${{ secrets.DOCKER_USERNAME }} 29 | password: ${{ secrets.DOCKER_TOKEN }} 30 | 31 | - name: Build and push 32 | id: docker_build 33 | uses: docker/build-push-action@v4 34 | with: 35 | push: true 36 | tags: phpdockerio/github-actions-delete-abandoned-branches:v2 37 | -------------------------------------------------------------------------------- /.github/workflows/cleanup.yaml: -------------------------------------------------------------------------------- 1 | name: Repo cleanup (old PRs, branches and issues) 2 | 3 | on: 4 | schedule: 5 | - cron: "0 0 * * *" 6 | 7 | # Allow workflow to be manually run from the GitHub UI 8 | workflow_dispatch: 9 | 10 | jobs: 11 | cleanup-repository: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | 16 | # Mark issues and PRs with no activity as stale after a while, and close them after a while longer 17 | - uses: actions/stale@v3 18 | with: 19 | stale-issue-message: 'Marking issue as stale' 20 | stale-pr-message: 'Marking PR as stale' 21 | stale-issue-label: 'stale' 22 | stale-pr-label: 'stale' 23 | days-before-stale: 30 24 | days-before-close: 7 25 | 26 | - uses: phpdocker-io/github-actions-delete-abandoned-branches@v2 27 | with: 28 | github_token: ${{ github.token }} 29 | last_commit_age_days: 100 30 | dry_run: no 31 | ignore_branches: test_prefix/one,test_prefix/two,test_prefix_ignored/one 32 | -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Quick tests against our own repository 2 | 3 | on: 4 | push: 5 | 6 | # Allow workflow to be manually run from the GitHub UI 7 | workflow_dispatch: 8 | jobs: 9 | check_dry_run_no_branches: 10 | runs-on: ubuntu-latest 11 | name: Runs the action with no ignore branches 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | 16 | - name: Build action container 17 | run: docker build -t action_container . 18 | 19 | - name: "Test: default options" 20 | run: | 21 | docker run --rm -t \ 22 | -e GITHUB_REPOSITORY \ 23 | -e GITHUB_OUTPUT \ 24 | -v "${GITHUB_OUTPUT}:${GITHUB_OUTPUT}" \ 25 | action_container \ 26 | --github-token="${{ github.token }}" 27 | 28 | - name: "Test: ignore branch 'test_prefix/two'" 29 | run: | 30 | docker run --rm -t \ 31 | -e GITHUB_REPOSITORY \ 32 | -e GITHUB_OUTPUT \ 33 | -v "${GITHUB_OUTPUT}:${GITHUB_OUTPUT}" \ 34 | action_container \ 35 | --ignore-branches="test_prefix/two" \ 36 | --last-commit-age-days=9 \ 37 | --dry-run=yes \ 38 | --github-token="${{ github.token }}" 39 | 40 | - name: "Test: allow only`test_prefix/*` except for `test_prefix/two`" 41 | run: | 42 | docker run --rm -t \ 43 | -e GITHUB_REPOSITORY \ 44 | -e GITHUB_OUTPUT \ 45 | -v "${GITHUB_OUTPUT}:${GITHUB_OUTPUT}" \ 46 | action_container \ 47 | --allowed-prefixes=test_prefix/ \ 48 | --ignore-branches=test_prefix/two \ 49 | --last-commit-age-days=9 \ 50 | --dry-run=yes \ 51 | --github-token="${{ github.token }}" 52 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3.11-alpine 2 | 3 | WORKDIR /application 4 | 5 | # Install dependencies 6 | COPY requirements.txt . 7 | RUN pip --no-cache-dir install -r requirements.txt 8 | 9 | # Copy code in 10 | COPY main.py . 11 | COPY src ./src 12 | 13 | ENTRYPOINT ["python3", "/application/main.py"] 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2021 Luis Pabon (PHPDocker.io) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Delete abandoned branches 2 | 3 | Github action to delete abandoned branches. 4 | 5 | ## Warning 6 | 7 | This action WILL delete branches from your repository, so you need to make your due diligence when choosing to use it 8 | and with which settings. I am not responsible for any mishaps that might occur. 9 | 10 | ## Abandoned branches 11 | 12 | A branch must meet all the following criteria to be deemed abandoned and safe to delete: 13 | 14 | * Must NOT be the default branch (eg `master` or `main`, depending on your repository settings) 15 | * Must NOT be a protected branch 16 | * Must NOT have any open pull requests 17 | * Must NOT be the base of an open pull request of another branch. The base of a pull request is the branch you told 18 | GitHub you want to merge your pull request into. 19 | * Must NOT be in an optional list of branches to ignore 20 | * Must match one of the given branch prefixes (optional) 21 | * Must be older than a given amount of days 22 | 23 | ## Inputs 24 | 25 | `* mandatory` 26 | 27 | | Name | Description | Example | 28 | |------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------| 29 | | `github_token`* | **Required.** The github token to use on requests to the github api. You can use the one github actions provide. | `${{ github.token }}` | 30 | | `last_commit_age_days` | How old in days must be the last commit into the branch for the branch to be deleted. **Default:** `60` | `90` | 31 | | `ignore_branches` | Comma-separated list of branches to ignore and never delete. You don't need to add your protected branches here. **Default:** `null` | `foo,bar` | 32 | | `allowed_prefixes` | Comma-separated list of prefixes a branch must match to be deleted. **Default:** `null` | `feature/,bugfix/` | 33 | | `dry_run` | Whether we're actually deleting branches at all. **Possible values:** `yes, no` (case sensitive). **Default:** `yes` | `no` | 34 | | `github_base_url` | The github API's base url. You only need to override this when using Github Enterprise on a different domain. **Default:** `https://api.github.com` | `https://github.mycompany.com/api/v3` | 35 | 36 | ### Note: dry run 37 | 38 | By default, the action will only perform a dry run. It will go in, gather all branches that qualify for deletion and 39 | give you the list on the actions' output, but without actually deleting anything. Make sure you configure your stuff 40 | correctly before setting `dry_run` to `no` 41 | 42 | ## Example 43 | 44 | The following workflow will run on a schedule (daily at 00:00) and will delete all abandoned branches older than 100 45 | days on a github enterprise install. 46 | 47 | ```yaml 48 | name: Delete abandoned branches 49 | 50 | on: 51 | # Run daily at midnight 52 | schedule: 53 | - cron: "0 0 * * *" 54 | 55 | # Allow workflow to be manually run from the GitHub UI 56 | workflow_dispatch: 57 | 58 | jobs: 59 | cleanup_old_branches: 60 | runs-on: ubuntu-latest 61 | name: Satisfy my repo CDO 62 | steps: 63 | - name: Delete those pesky dead branches 64 | uses: phpdocker-io/github-actions-delete-abandoned-branches@v2 65 | id: delete_stuff 66 | with: 67 | github_token: ${{ github.token }} 68 | last_commit_age_days: 100 69 | ignore_branches: next-version,dont-deleteme 70 | github_base_url: https://github.mycompany.com/api/v3 71 | 72 | # Disable dry run and actually get stuff deleted 73 | dry_run: no 74 | 75 | - name: Get output 76 | run: "echo 'Deleted branches: ${{ steps.delete_stuff.outputs.deleted_branches }}'" 77 | ``` 78 | 79 | The following workflow will run on a schedule (daily at 13:00) and will delete all abandoned branches older than 7 days 80 | that are prefixed with `feature/` and `deleteme/`, leaving all the rest. 81 | 82 | ```yaml 83 | name: Delete abandoned branches 84 | 85 | on: 86 | # Run daily at midnight 87 | schedule: 88 | - cron: "0 13 * * *" 89 | 90 | # Allow workflow to be manually run from the GitHub UI 91 | workflow_dispatch: 92 | 93 | jobs: 94 | cleanup_old_branches: 95 | runs-on: ubuntu-latest 96 | name: Satisfy my repo CDO 97 | steps: 98 | - name: Delete those pesky dead branches 99 | uses: phpdocker-io/github-actions-delete-abandoned-branches@v2 100 | id: delete_stuff 101 | with: 102 | github_token: ${{ github.token }} 103 | last_commit_age_days: 7 104 | allowed_prefixes: feature/,deleteme/ 105 | ignore_branches: next-version,dont-deleteme 106 | 107 | # Disable dry run and actually get stuff deleted 108 | dry_run: no 109 | 110 | - name: Get output 111 | run: "echo 'Deleted branches: ${{ steps.delete_stuff.outputs.deleted_branches }}'" 112 | ``` 113 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # action.yml 2 | name: 'Delete abandoned branches' 3 | description: | 4 | Deletes old branches from your repo as long as they aren't part of an open pull request, the default branch, or protected. 5 | author: Luis Pabon (PHPDocker.io) 6 | branding: 7 | icon: git-branch 8 | color: orange 9 | 10 | inputs: 11 | ignore_branches: 12 | description: "Comma-separated list of branches to ignore and never delete. You don't need to add your protected branches here." 13 | required: false 14 | default: "" 15 | last_commit_age_days: 16 | description: "How old in days must be the last commit into the branch for the branch to be deleted." 17 | required: false 18 | default: "60" 19 | allowed_prefixes: 20 | description: "Comma-separated list of prefixes a branch must match to be deleted." 21 | required: false 22 | default: "" 23 | dry_run: 24 | description: "Whether we're actually deleting branches at all. Defaults to 'yes'. Possible values: yes, no (case sensitive)" 25 | required: true 26 | github_token: 27 | description: "The github token to use on requests to the github api" 28 | required: true 29 | github_base_url: 30 | description: "The API base url to be used in requests to GitHub Enterprise" 31 | required: false 32 | default: "https://api.github.com" 33 | 34 | outputs: 35 | deleted_branches: # id of output 36 | description: 'Branches that have been deleted, if any' 37 | 38 | runs: 39 | using: 'docker' 40 | image: 'docker://phpdockerio/github-actions-delete-abandoned-branches:v2' 41 | args: 42 | - --ignore-branches=${{ inputs.ignore_branches }} 43 | - --last-commit-age-days=${{ inputs.last_commit_age_days }} 44 | - --allowed-prefixes=${{ inputs.allowed_prefixes }} 45 | - --dry-run=${{ inputs.dry_run }} 46 | - --github-token=${{ inputs.github_token }} 47 | - --github-base-url=${{ inputs.github_base_url }} 48 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | from src import actions, io 2 | from src.io import InputParser 3 | 4 | if __name__ == '__main__': 5 | options = InputParser().parse_input() 6 | deleted_branches = actions.run_action(options) 7 | io.format_output({'deleted_branches': deleted_branches}) 8 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | requests==2.* 2 | -------------------------------------------------------------------------------- /src/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/phpdocker-io/github-actions-delete-abandoned-branches/e04764c2f0f714dedb80c42d5174e756568a0452/src/__init__.py -------------------------------------------------------------------------------- /src/actions.py: -------------------------------------------------------------------------------- 1 | from src.github import Github 2 | from src.io import Options 3 | 4 | 5 | def run_action(options: Options) -> list: 6 | print(f"Starting github action to cleanup old branches. Input: {options}") 7 | 8 | github = Github(repo=options.github_repo, token=options.github_token, base_url=options.github_base_url) 9 | 10 | branches = github.get_deletable_branches( 11 | last_commit_age_days=options.last_commit_age_days, 12 | ignore_branches=options.ignore_branches, 13 | allowed_prefixes=options.allowed_prefixes 14 | ) 15 | 16 | print(f"Branches queued for deletion: {branches}") 17 | if options.dry_run is False: 18 | print('This is NOT a dry run, deleting branches') 19 | github.delete_branches(branches=branches) 20 | else: 21 | print('This is a dry run, skipping deletion of branches') 22 | 23 | return branches 24 | -------------------------------------------------------------------------------- /src/github.py: -------------------------------------------------------------------------------- 1 | from datetime import datetime 2 | 3 | from src import requests 4 | 5 | 6 | class Github: 7 | def __init__(self, repo: str, token: str, base_url: str): 8 | self.token = token 9 | self.repo = repo 10 | self.base_url = base_url 11 | 12 | def make_headers(self) -> dict: 13 | return { 14 | 'authorization': f'Bearer {self.token}', 15 | 'content-type': 'application/vnd.github.v3+json', 16 | } 17 | 18 | def get_paginated_branches_url(self, page: int = 0) -> str: 19 | return f'{self.base_url}/repos/{self.repo}/branches?protected=false&per_page=30&page={page}' 20 | 21 | def get_deletable_branches( 22 | self, 23 | last_commit_age_days: int, 24 | ignore_branches: list[str], 25 | allowed_prefixes: list[str] 26 | ) -> list[str]: 27 | # Default branch might not be protected 28 | default_branch = self.get_default_branch() 29 | 30 | url = self.get_paginated_branches_url() 31 | headers = self.make_headers() 32 | 33 | response = requests.get(url=url, headers=headers) 34 | if response.status_code != 200: 35 | raise RuntimeError(f'Failed to make request to {url}. {response} {response.json()}') 36 | 37 | deletable_branches = [] 38 | branch: dict 39 | branches: list = response.json() 40 | current_page = 1 41 | 42 | while len(branches) > 0: 43 | for branch in branches: 44 | branch_name = branch.get('name') 45 | 46 | commit_hash = branch.get('commit', {}).get('sha') 47 | commit_url = branch.get('commit', {}).get('url') 48 | 49 | print(f'Analyzing branch `{branch_name}`...') 50 | 51 | # Immediately discard protected branches, default branch, ignored branches and branches not in prefix 52 | if branch_name == default_branch: 53 | print(f'Ignoring `{branch_name}` because it is the default branch') 54 | continue 55 | 56 | # We're already retrieving non-protected branches from the API, but it pays being careful when dealing 57 | # with third party apis 58 | if branch.get('protected') is True: 59 | print(f'Ignoring `{branch_name}` because it is protected') 60 | continue 61 | 62 | if branch_name in ignore_branches: 63 | print(f'Ignoring `{branch_name}` because it is on the list of ignored branches') 64 | continue 65 | 66 | # If allowed_prefixes are provided, only consider branches that match one of the prefixes 67 | if len(allowed_prefixes) > 0: 68 | found_prefix = False 69 | for prefix in allowed_prefixes: 70 | if branch_name.startswith(prefix): 71 | found_prefix = True 72 | if found_prefix is False: 73 | print(f'Ignoring `{branch_name}` because it does not match any provided allowed_prefixes') 74 | continue 75 | 76 | # Move on if commit is in an open pull request 77 | if self.has_open_pulls(commit_hash=commit_hash): 78 | print(f'Ignoring `{branch_name}` because it has open pull requests') 79 | continue 80 | 81 | # Move on if branch is base for a pull request 82 | if self.is_pull_request_base(branch=branch_name): 83 | print(f'Ignoring `{branch_name}` because it is the base for a pull request of another branch') 84 | continue 85 | 86 | # Move on if last commit is newer than last_commit_age_days 87 | if self.is_commit_older_than(commit_url=commit_url, older_than_days=last_commit_age_days) is False: 88 | print(f'Ignoring `{branch_name}` because last commit is newer than {last_commit_age_days} days') 89 | continue 90 | 91 | print(f'Branch `{branch_name}` meets the criteria for deletion') 92 | deletable_branches.append(branch_name) 93 | 94 | # Re-request next page 95 | current_page += 1 96 | 97 | response = requests.get(url=self.get_paginated_branches_url(page=current_page), headers=headers) 98 | if response.status_code != 200: 99 | raise RuntimeError(f'Failed to make request to {url}. {response} {response.json()}') 100 | 101 | branches: list = response.json() 102 | 103 | return deletable_branches 104 | 105 | def delete_branches(self, branches: list[str]) -> None: 106 | for branch in branches: 107 | print(f'Deleting branch `{branch}`...') 108 | url = f'{self.base_url}/repos/{self.repo}/git/refs/heads/{branch.replace("#", "%23")}' 109 | 110 | response = requests.request(method='DELETE', url=url, headers=self.make_headers()) 111 | if response.status_code != 204: 112 | print(f'Failed to delete branch `{branch}`') 113 | raise RuntimeError(f'Failed to make DELETE request to {url}. {response} {response.json()}') 114 | 115 | print(f'Branch `{branch}` DELETED!') 116 | 117 | def get_default_branch(self) -> str: 118 | url = f'{self.base_url}/repos/{self.repo}' 119 | headers = self.make_headers() 120 | 121 | response = requests.get(url=url, headers=headers) 122 | 123 | if response.status_code != 200: 124 | raise RuntimeError('Error: could not determine default branch. This is a big one.') 125 | 126 | return response.json().get('default_branch') 127 | 128 | def has_open_pulls(self, commit_hash: str) -> bool: 129 | """ 130 | Returns true if commit is part of an open pull request or the branch is the base for a pull request 131 | """ 132 | url = f'{self.base_url}/repos/{self.repo}/commits/{commit_hash}/pulls' 133 | headers = self.make_headers() 134 | headers['accept'] = 'application/vnd.github.groot-preview+json' 135 | 136 | response = requests.get(url=url, headers=headers) 137 | if response.status_code != 200: 138 | raise RuntimeError(f'Failed to make request to {url}. {response} {response.json()}') 139 | 140 | pull_request: dict 141 | for pull_request in response.json(): 142 | if pull_request.get('state') == 'open': 143 | return True 144 | 145 | return False 146 | 147 | def is_pull_request_base(self, branch: str) -> bool: 148 | """ 149 | Returns true if the given branch is base for another pull request. 150 | """ 151 | url = f'{self.base_url}/repos/{self.repo}/pulls?base={branch}' 152 | headers = self.make_headers() 153 | headers['accept'] = 'application/vnd.github.groot-preview+json' 154 | 155 | response = requests.get(url=url, headers=headers) 156 | if response.status_code != 200: 157 | raise RuntimeError(f'Failed to make request to {url}. {response} {response.json()}') 158 | 159 | return len(response.json()) > 0 160 | 161 | def is_commit_older_than(self, commit_url: str, older_than_days: int): 162 | response = requests.get(url=commit_url, headers=self.make_headers()) 163 | if response.status_code != 200: 164 | raise RuntimeError(f'Failed to make request to {commit_url}. {response} {response.json()}') 165 | 166 | commit: dict = response.json().get('commit', {}) 167 | committer: dict = commit.get('committer', {}) 168 | author: dict = commit.get('author', {}) 169 | 170 | # Get date of the committer (instead of the author) as the last commit could be old but just applied 171 | # for instance coming from a merge where the committer is bringing in commits from other authors 172 | # Fall back to author's commit date if none found for whatever bizarre reason 173 | commit_date_raw = committer.get('date', author.get('date')) 174 | if commit_date_raw is None: 175 | print(f"Warning: could not determine commit date for {commit_url}. Assuming it's not old enough to delete") 176 | return False 177 | 178 | # Dates are formatted like so: '2021-02-04T10:52:40Z' 179 | commit_date = datetime.strptime(commit_date_raw, "%Y-%m-%dT%H:%M:%SZ") 180 | 181 | delta = datetime.now() - commit_date 182 | print(f'Last commit was on {commit_date_raw} ({delta.days} days ago)') 183 | 184 | return delta.days > older_than_days 185 | -------------------------------------------------------------------------------- /src/io.py: -------------------------------------------------------------------------------- 1 | import argparse 2 | from os import getenv 3 | 4 | DEFAULT_GITHUB_API_URL = 'https://api.github.com' 5 | 6 | 7 | class Options: 8 | def __init__( 9 | self, 10 | ignore_branches: list[str], 11 | last_commit_age_days: int, 12 | allowed_prefixes: list[str], 13 | github_token: str, 14 | github_repo: str, 15 | dry_run: bool = True, 16 | github_base_url: str = DEFAULT_GITHUB_API_URL 17 | ): 18 | self.ignore_branches = ignore_branches 19 | self.last_commit_age_days = last_commit_age_days 20 | self.allowed_prefixes = allowed_prefixes 21 | self.github_token = github_token 22 | self.github_repo = github_repo 23 | self.dry_run = dry_run 24 | self.github_base_url = github_base_url 25 | 26 | 27 | class InputParser: 28 | @staticmethod 29 | def get_args() -> argparse.Namespace: 30 | parser = argparse.ArgumentParser('Github Actions Delete Old Branches') 31 | 32 | parser.add_argument("--ignore-branches", help="Comma-separated list of branches to ignore") 33 | 34 | parser.add_argument( 35 | "--allowed-prefixes", 36 | help="Comma-separated list of prefixes a branch must match to be deleted" 37 | ) 38 | 39 | parser.add_argument("--github-token", required=True) 40 | 41 | parser.add_argument( 42 | "--github-base-url", 43 | default=DEFAULT_GITHUB_API_URL, 44 | help="The API base url to be used in requests to GitHub Enterprise" 45 | ) 46 | 47 | parser.add_argument( 48 | "--last-commit-age-days", 49 | help="How old in days must be the last commit into the branch for the branch to be deleted", 50 | default=60, 51 | type=int, 52 | ) 53 | 54 | parser.add_argument( 55 | "--dry-run", 56 | choices=["yes", "no"], 57 | default="yes", 58 | help="Whether to delete branches at all. Defaults to 'yes'. Possible values: yes, no (case sensitive)" 59 | ) 60 | 61 | return parser.parse_args() 62 | 63 | def parse_input(self) -> Options: 64 | args = self.get_args() 65 | 66 | branches_raw: str = "" if args.ignore_branches is None else args.ignore_branches 67 | ignore_branches = branches_raw.split(',') 68 | if ignore_branches == ['']: 69 | ignore_branches = [] 70 | 71 | allowed_prefixes_raw: str = "" if args.allowed_prefixes is None else args.allowed_prefixes 72 | allowed_prefixes = allowed_prefixes_raw.split(',') 73 | if allowed_prefixes == ['']: 74 | allowed_prefixes = [] 75 | 76 | # Dry run can only be either `true` or `false`, as strings due to github actions input limitations 77 | dry_run = False if args.dry_run == 'no' else True 78 | 79 | return Options( 80 | ignore_branches=ignore_branches, 81 | last_commit_age_days=args.last_commit_age_days, 82 | allowed_prefixes=allowed_prefixes, 83 | dry_run=dry_run, 84 | github_token=args.github_token, 85 | github_repo=getenv('GITHUB_REPOSITORY'), 86 | github_base_url=args.github_base_url 87 | ) 88 | 89 | 90 | def format_output(output_strings: dict) -> None: 91 | file_path = getenv('GITHUB_OUTPUT') 92 | 93 | with open(file_path, "a") as gh_output: 94 | for name, value in output_strings.items(): 95 | gh_output.write(f'{name}={value}\n') 96 | -------------------------------------------------------------------------------- /src/requests.py: -------------------------------------------------------------------------------- 1 | import requests 2 | from requests.models import Response 3 | 4 | 5 | def get(url: str, force_debug: bool = False, headers: dict = None) -> Response: 6 | return request(method='get', url=url, headers=headers, force_debug=force_debug) 7 | 8 | 9 | def request(method: str, url: str, json: dict = None, headers: dict = None, force_debug: bool = False) -> Response: 10 | try: 11 | response = requests.request(method=method, url=url, json=json, headers=headers) 12 | if force_debug: 13 | debug_request(url, method, response, json, headers) 14 | 15 | return response 16 | except Exception as ex: 17 | debug_request(url, method, None, json, headers) 18 | raise ex 19 | 20 | 21 | def debug_request( 22 | url: str, 23 | method: str, 24 | response: Response = None, 25 | payload: dict = None, 26 | headers: dict = None, 27 | ) -> None: 28 | print('#########################') 29 | print(f'Debugging request to {url}') 30 | print(f'Method: {method}') 31 | print(f'Payload: {payload}') 32 | print(f'Headers: {headers}') 33 | if response is not None: 34 | print(f'Response: {response}') 35 | print(response.json()) 36 | print('#########################') 37 | --------------------------------------------------------------------------------