Skip to content

Commit

Permalink
feat: allow glob support for allowed branches, add --ignored-prefixes…
Browse files Browse the repository at this point in the history
… flag
  • Loading branch information
cbrgm committed Jan 17, 2024
1 parent 5078163 commit d8edc0b
Show file tree
Hide file tree
Showing 4 changed files with 43 additions and 14 deletions.
11 changes: 6 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ This GitHub Action deems a branch as stale or abandoned based on the following c
- **No Open Pull Requests**: There are no open pull requests that originate from the branch.
- **Not Base of an Open Pull Request**: The branch is not the base branch for any open pull requests.
- **Not in Ignore List**: The branch is not included in the optional list of branches to ignore.
- **Branch Prefix Match**: If specified, the branch name matches one of the given prefixes.
- **Age**: The branch's last commit is older than the specified number of days (e.g., no commits for 30 days).
- **(No) Branch Prefix Match**: If specified, the branch name does (not) match one of the given prefixes.
- **Latest Commit Age**: The branch's last commit is older than the specified number of days (e.g., no commits for 30 days).

Branches that meet all these criteria are considered as stale or abandoned and eligible for deletion.

Expand All @@ -28,9 +28,10 @@ Branches that meet all these criteria are considered as stale or abandoned and e
- `repository`: **Required** - The target GitHub repository in the format "owner/repo".
- `ignore-branches`: Optional - Comma-separated list of branches to ignore from deletion.
- `allowed-prefixes`: Optional - Comma-separated list of prefixes a branch must match to be considered for deletion.
- `ignored-prefixes`: Optional - Comma-separated list of prefixes a branch must NOT match to be considered for deletion.
- `last-commit-age-days`: Optional - Number of days since the last commit for a branch to be considered abandoned. Defaults to `30` days.
- `dry-run`: Optional - Perform a dry run without actually deleting branches. Defaults to `true`, meaning no branches will be deleted.
- `rate-limit`: Optional - Stop the action if it exceeds 95% of the GitHub API rate limit. Defaults to `true`, ensuring the action is halted before hitting the rate limit.
- `rate-limit`: Optional - Stop the action if it exceeds 95% of the GitHub API rate limit. Defaults to `true`, ensuring the action is halted before hitting the rate limit e.g. exiting with status code `0` instead of failing.

### Workflow Usage

Expand Down Expand Up @@ -75,7 +76,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
ignore-branches: "foobar,release-*"
allowed-prefixes: "feature/,bugfix/"
ignore-prefixes: "feature/,bugfix/"
last-commit-age-days: 60
dry-run: false
rate-limit: true
Expand All @@ -85,7 +86,7 @@ In this advanced example:
* The action is scheduled to run daily.
* It ignores the branch `foobar` and branches starting with `release-`.
* Only branches prefixed with `feature/` or `bugfix/` are considered for deletion.
* Branches prefixed with `feature/` or `bugfix/` are not considered for deletion.
* Branches with no commits in the last `60` days are eligible for deletion.
* The action is not in `dry-run` mode, meaning branches will actually be deleted.
* The `rate-limit` check is enabled to prevent exceeding the GitHub API rate limit.
Expand Down
6 changes: 6 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,10 @@ inputs:
description: 'Comma-separated list of prefixes a branch must match to be deleted'
required: false
default: ""
ignored-prefixes:
description: 'Comma-separated list of prefixes a branch must NOT match to be deleted'
required: false
default: ""
last-commit-age-days:
description: 'Number of days since the last commit for a branch to be considered abandoned'
required: false
Expand All @@ -42,6 +46,8 @@ runs:
- ${{ inputs.ignore-branches }}
- --allowed-prefixes
- ${{ inputs.allowed-prefixes }}
- --ignored-prefixes
- ${{ inputs.ignored-prefixes }}
- --last-commit-age-days=${{ inputs.last-commit-age-days }}
- --dry-run=${{ inputs.dry-run }}
- --rate-limit=${{ inputs.rate-limit }}
Expand Down
24 changes: 19 additions & 5 deletions cmd/cleanup-stale-branches-action/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log"
"os"
"regexp"
"strings"
"time"

Expand All @@ -14,13 +15,14 @@ import (
)

var args struct {
IgnoreBranches string `arg:"--ignore-branches,env:IGNORE_BRANCHES"`
AllowedPrefixes string `arg:"--allowed-prefixes,env:ALLOWED_PREFIXES"`
GithubToken string `arg:"--github-token,required,env:GITHUB_TOKEN"`
DryRun bool `arg:"--dry-run,env:DRY_RUN"`
GithubRepo string `arg:"--github-repo,required,env:GITHUB_REPOSITORY"`
GithubToken string `arg:"--github-token,required,env:GITHUB_TOKEN"`
IgnoreBranches string `arg:"--ignore-branches,env:IGNORE_BRANCHES"`
IgnoredPrefixes string `arg:"--ignored-prefixes,env:IGNORED_PREFIXES"`
LastCommitAgeDays int `arg:"--last-commit-age-days,env:LAST_COMMIT_AGE_DAYS"`
RateLimit bool `arg:"--rate-limit,env:RATE_LIMIT"`
DryRun bool `arg:"--dry-run,env:DRY_RUN"`
}

type GitHubClientWrapper struct {
Expand Down Expand Up @@ -121,6 +123,8 @@ func (g *GitHubClientWrapper) getDeletableBranches(ctx context.Context) ([]strin

ignoreBranches := strings.Split(args.IgnoreBranches, ",")
allowedPrefixes := strings.Split(args.AllowedPrefixes, ",")
ignoredPrefixes := strings.Split(args.IgnoredPrefixes, ",")

deletableBranches := []string{}

opts := &github.BranchListOptions{
Expand Down Expand Up @@ -160,6 +164,11 @@ func (g *GitHubClientWrapper) getDeletableBranches(ctx context.Context) ([]strin
continue
}

if startsWith(ignoredPrefixes, branchName) {
log.Printf("- Skipping `%s`: matches an ignored prefix\n", branchName)
continue
}

commitDate, err := g.getLatestCommitDate(ctx, branchName)
if err != nil {
log.Printf("- Skipping `%s`: %v\n", branchName, err)
Expand Down Expand Up @@ -284,8 +293,13 @@ func (g *GitHubClientWrapper) deleteBranches(ctx context.Context, branches []str
}

func contains(slice []string, item string) bool {
for _, v := range slice {
if v == item {
for _, pattern := range slice {
// Replace * with .*, which is the regex equivalent
regexPattern := "^" + regexp.QuoteMeta(pattern)
regexPattern = strings.ReplaceAll(regexPattern, "\\*", ".*") + "$"

matched, _ := regexp.MatchString(regexPattern, item)
if matched {
return true
}
}
Expand Down
16 changes: 12 additions & 4 deletions cmd/cleanup-stale-branches-action/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,16 +73,24 @@ func TestContains(t *testing.T) {
item string
want bool
}{
{"Present", []string{"a", "b", "c"}, "b", true},
{"NotPresent", []string{"a", "b", "c"}, "d", false},
{"ExactMatchPresent", []string{"a", "b", "c"}, "b", true},
{"ExactMatchNotPresent", []string{"a", "b", "c"}, "d", false},
{"EmptySlice", []string{}, "a", false},
{"EmptyString", []string{"a", "b", ""}, "", true},
{"WildcardSimpleMatch", []string{"foo*", "bar"}, "foobar", true},
{"WildcardComplexMatch", []string{"release-*", "dev"}, "release-v1", true},
{"WildcardNoMatch", []string{"test-*"}, "demo-test", false},
{"WildcardEmptyMatch", []string{"test-*"}, "", false},
{"WildcardOnly", []string{"*"}, "anything", true},
{"MultipleWildcards", []string{"*foo*", "*bar*"}, "myfoobar", true},
{"MiddleWildcard", []string{"fo*ar"}, "foobar", true},
{"NonMatchingMultipleWildcards", []string{"*foo*", "*bar*"}, "baz", false},
{"ExactMatchAmongWildcards", []string{"foo*", "exact", "*bar"}, "exact", true},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := contains(tt.slice, tt.item); got != tt.want {
t.Errorf("contains() = %v, want %v", got, tt.want)
t.Errorf("contains() = %v, want %v for slice %v and item %v", got, tt.want, tt.slice, tt.item)
}
})
}
Expand Down

0 comments on commit d8edc0b

Please sign in to comment.