Skip to content

Commit

Permalink
Merge pull request #6 from cbrgm/support-globbing
Browse files Browse the repository at this point in the history
support globbing
  • Loading branch information
cbrgm authored Jan 17, 2024
2 parents 5078163 + bcc3030 commit fafe963
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 19 deletions.
1 change: 1 addition & 0 deletions .github/workflows/stale-branches.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,5 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
repository: ${{ github.repository }}
dry-run: false

21 changes: 11 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,13 @@

This GitHub Action deems a branch as stale or abandoned based on the following criteria:

- **Not Default Branch**: The branch is not the repository's default branch.
- **Not Protected**: The branch is not a protected branch.
- **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).
- 🚫 **Not Default Branch**: The branch is not the repository's default branch.
- 🛡️ **Not Protected**: The branch is not a protected branch.
- 📭 **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.
- **(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 fafe963

Please sign in to comment.