Skip to content

Commit fe1ac60

Browse files
authored
Merge pull request #24 from phpdocker-io/refactor-input
Refactor input, limit deletion to certain prefixes, misc uplifts and fixes
2 parents a0af495 + e67250c commit fe1ac60

File tree

9 files changed

+213
-94
lines changed

9 files changed

+213
-94
lines changed

.github/workflows/build-and-push-image.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,4 @@ jobs:
3333
uses: docker/build-push-action@v4
3434
with:
3535
push: true
36-
tags: phpdockerio/github-actions-delete-abandoned-branches:v1
36+
tags: phpdockerio/github-actions-delete-abandoned-branches:v2

.github/workflows/tests.yaml

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,41 @@ jobs:
1111
name: Runs the action with no ignore branches
1212
steps:
1313
- name: Checkout
14-
uses: actions/checkout@v2
14+
uses: actions/checkout@v3
1515

16-
- name: Run our action
17-
uses: ./
18-
id: delete_stuff
19-
with:
20-
github_token: ${{ github.token }}
21-
last_commit_age_days: 9
22-
dry_run: yes
23-
ignore_branches: deletable-but-ignored
16+
- name: Build action container
17+
run: docker build -t action_container .
2418

25-
- name: Get output
26-
run: "echo 'Deleted branches: ${{ steps.delete_stuff.outputs.deleted_branches }}'"
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 }}"

Dockerfile

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
FROM python:3.9-alpine
1+
FROM python:3.11-alpine
22

33
WORKDIR /application
44

README.md

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,21 @@ A branch must meet all the following criteria to be deemed abandoned and safe to
1717
* Must NOT be the base of an open pull request of another branch. The base of a pull request is the branch you told
1818
GitHub you want to merge your pull request into.
1919
* Must NOT be in an optional list of branches to ignore
20+
* Must match one of the given branch prefixes (optional)
2021
* Must be older than a given amount of days
2122

2223
## Inputs
2324

2425
`* mandatory`
2526

26-
| Name | Description | Example |
27-
| ------------- | ------------- | ------------- |
28-
| `ignore_branches` | Comma-separated list of branches to ignore and never delete. You don't need to add your protected branches here. | `foo,bar`
29-
| `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`
30-
| `dry_run`* | Whether we're actually deleting branches at all. Possible values: `yes, no` (case sensitive). Default: `yes` | `no`
31-
| `github_token`* | The github token to use on requests to the github api. You can use the one github actions provide | `${{ github.token }}`
32-
| `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`
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` |
3335

3436
### Note: dry run
3537

@@ -40,7 +42,7 @@ correctly before setting `dry_run` to `no`
4042
## Example
4143

4244
The following workflow will run on a schedule (daily at 00:00) and will delete all abandoned branches older than 100
43-
days:
45+
days on a github enterprise install.
4446

4547
```yaml
4648
name: Delete abandoned branches
@@ -59,7 +61,7 @@ jobs:
5961
name: Satisfy my repo CDO
6062
steps:
6163
- name: Delete those pesky dead branches
62-
uses: phpdocker-io/github-actions-delete-abandoned-branches@v1
64+
uses: phpdocker-io/github-actions-delete-abandoned-branches@v2
6365
id: delete_stuff
6466
with:
6567
github_token: ${{ github.token }}
@@ -72,5 +74,39 @@ jobs:
7274

7375
- name: Get output
7476
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
75106
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 }}'"
76112
```

action.yml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ inputs:
1616
description: "How old in days must be the last commit into the branch for the branch to be deleted."
1717
required: false
1818
default: "60"
19+
allowed_prefixes:
20+
description: "Comma-separated list of prefixes a branch must match to be deleted."
21+
required: false
22+
default: ""
1923
dry_run:
2024
description: "Whether we're actually deleting branches at all. Defaults to 'yes'. Possible values: yes, no (case sensitive)"
2125
required: true
@@ -33,10 +37,11 @@ outputs:
3337

3438
runs:
3539
using: 'docker'
36-
image: 'docker://phpdockerio/github-actions-delete-abandoned-branches:v1'
40+
image: 'docker://phpdockerio/github-actions-delete-abandoned-branches:v2'
3741
args:
38-
- ${{ inputs.ignore_branches }}
39-
- ${{ inputs.last_commit_age_days }}
40-
- ${{ inputs.dry_run }}
41-
- ${{ inputs.github_token }}
42-
- ${{ inputs.github_base_url }}
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 }}"

main.py

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,7 @@
11
from src import actions, io
2+
from src.io import InputParser
23

34
if __name__ == '__main__':
4-
ignore_branches, last_commit_age_days, dry_run, github_token, github_repo, github_base_url = io.parse_input()
5-
6-
deleted_branches = actions.run_action(
7-
ignore_branches=ignore_branches,
8-
last_commit_age_days=last_commit_age_days,
9-
dry_run=dry_run,
10-
github_repo=github_repo,
11-
github_token=github_token,
12-
github_base_url=github_base_url
13-
)
14-
5+
options = InputParser().parse_input()
6+
deleted_branches = actions.run_action(options)
157
io.format_output({'deleted_branches': deleted_branches})

src/actions.py

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,20 @@
11
from src.github import Github
2+
from src.io import Options
23

34

4-
def run_action(
5-
github_repo: str,
6-
ignore_branches: list,
7-
last_commit_age_days: int,
8-
github_token: str,
9-
github_base_url: str,
10-
dry_run: bool = True
11-
) -> list:
12-
input_data = {
13-
'github_repo': github_repo,
14-
'ignore_branches': ignore_branches,
15-
'last_commit_age_days': last_commit_age_days,
16-
'dry_run': dry_run,
17-
'github_base_url': github_base_url
18-
}
5+
def run_action(options: Options) -> list:
6+
print(f"Starting github action to cleanup old branches. Input: {options}")
197

20-
print(f"Starting github action to cleanup old branches. Input: {input_data}")
8+
github = Github(repo=options.github_repo, token=options.github_token, base_url=options.github_base_url)
219

22-
github = Github(github_repo=github_repo, github_token=github_token, github_base_url=github_base_url)
23-
24-
branches = github.get_deletable_branches(last_commit_age_days=last_commit_age_days, ignore_branches=ignore_branches)
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+
)
2515

2616
print(f"Branches queued for deletion: {branches}")
27-
if dry_run is False:
17+
if options.dry_run is False:
2818
print('This is NOT a dry run, deleting branches')
2919
github.delete_branches(branches=branches)
3020
else:

src/github.py

Lines changed: 31 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,22 +2,28 @@
22

33
from src import requests
44

5+
56
class Github:
6-
def __init__(self, github_repo: str, github_token: str, github_base_url: str):
7-
self.github_token = github_token
8-
self.github_repo = github_repo
9-
self.github_base_url = github_base_url
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
1011

1112
def make_headers(self) -> dict:
1213
return {
13-
'authorization': f'Bearer {self.github_token}',
14+
'authorization': f'Bearer {self.token}',
1415
'content-type': 'application/vnd.github.v3+json',
1516
}
1617

1718
def get_paginated_branches_url(self, page: int = 0) -> str:
18-
return f'{self.github_base_url}/repos/{self.github_repo}/branches?protected=false&per_page=30&page={page}'
19-
20-
def get_deletable_branches(self, last_commit_age_days: int, ignore_branches: list) -> list:
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]:
2127
# Default branch might not be protected
2228
default_branch = self.get_default_branch()
2329

@@ -42,7 +48,7 @@ def get_deletable_branches(self, last_commit_age_days: int, ignore_branches: lis
4248

4349
print(f'Analyzing branch `{branch_name}`...')
4450

45-
# Immediately discard protected branches, default branch and ignored branches
51+
# Immediately discard protected branches, default branch, ignored branches and branches not in prefix
4652
if branch_name == default_branch:
4753
print(f'Ignoring `{branch_name}` because it is the default branch')
4854
continue
@@ -57,9 +63,19 @@ def get_deletable_branches(self, last_commit_age_days: int, ignore_branches: lis
5763
print(f'Ignoring `{branch_name}` because it is on the list of ignored branches')
5864
continue
5965

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+
6076
# Move on if commit is in an open pull request
6177
if self.has_open_pulls(commit_hash=commit_hash):
62-
print(f'Ignoring `{branch_name}` because it has open pulls')
78+
print(f'Ignoring `{branch_name}` because it has open pull requests')
6379
continue
6480

6581
# Move on if branch is base for a pull request
@@ -86,10 +102,10 @@ def get_deletable_branches(self, last_commit_age_days: int, ignore_branches: lis
86102

87103
return deletable_branches
88104

89-
def delete_branches(self, branches: list) -> None:
105+
def delete_branches(self, branches: list[str]) -> None:
90106
for branch in branches:
91107
print(f'Deleting branch `{branch}`...')
92-
url = f'{self.github_base_url}/repos/{self.github_repo}/git/refs/heads/{branch.replace("#", "%23")}'
108+
url = f'{self.base_url}/repos/{self.repo}/git/refs/heads/{branch.replace("#", "%23")}'
93109

94110
response = requests.request(method='DELETE', url=url, headers=self.make_headers())
95111
if response.status_code != 204:
@@ -99,7 +115,7 @@ def delete_branches(self, branches: list) -> None:
99115
print(f'Branch `{branch}` DELETED!')
100116

101117
def get_default_branch(self) -> str:
102-
url = f'{self.github_base_url}/repos/{self.github_repo}'
118+
url = f'{self.base_url}/repos/{self.repo}'
103119
headers = self.make_headers()
104120

105121
response = requests.get(url=url, headers=headers)
@@ -113,7 +129,7 @@ def has_open_pulls(self, commit_hash: str) -> bool:
113129
"""
114130
Returns true if commit is part of an open pull request or the branch is the base for a pull request
115131
"""
116-
url = f'{self.github_base_url}/repos/{self.github_repo}/commits/{commit_hash}/pulls'
132+
url = f'{self.base_url}/repos/{self.repo}/commits/{commit_hash}/pulls'
117133
headers = self.make_headers()
118134
headers['accept'] = 'application/vnd.github.groot-preview+json'
119135

@@ -132,7 +148,7 @@ def is_pull_request_base(self, branch: str) -> bool:
132148
"""
133149
Returns true if the given branch is base for another pull request.
134150
"""
135-
url = f'{self.github_base_url}/repos/{self.github_repo}/pulls?base={branch}'
151+
url = f'{self.base_url}/repos/{self.repo}/pulls?base={branch}'
136152
headers = self.make_headers()
137153
headers['accept'] = 'application/vnd.github.groot-preview+json'
138154

0 commit comments

Comments
 (0)