Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support labels based on PR / Issue age #113

Merged
merged 1 commit into from
Nov 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
coverage.out
action
action.tar.gz
.vscode
27 changes: 26 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Action](https://help.github.com/en/categories/automating-your-workflow-with-gith
that can manage multiple labels for both Pull Requests and Issues using
configurable matching rules. Available conditions:

* [Age](#age): label based on the age of a PR or Issue.
* [Author can merge](#author-can-merge): label based on whether the author can merge the PR
* [Authors](#authors): label based on the PR/Issue authors
* [Base branch](#base-branch): label based on the PR's base branch name
Expand Down Expand Up @@ -87,7 +88,7 @@ to control when to run it.

You may combine multiple event triggers.

A final option is to trigger the action periodically using the
<a name="schedule" />A final option is to trigger the action periodically using the
[`schedule`](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#schedule)
trigger. For backwards compatibility reasons this will examine all
active pull requests and update their labels. If you wish to examine
Expand Down Expand Up @@ -301,6 +302,30 @@ alphabetical order. Some important considerations:
You can use tools like [regex101.com](https://regex101.com/?flavor=golang)
to verify your conditions.

### Age (PRs and Issues) <a name="age" />

This condition is satisfied when the age of the PR or Issue are larger than
the given one. The age is calculated from the creation date.

This condition is best used when with a <a href="#schedule">schedule trigger</a>.

Example:

```yaml
age: 1d
```

The syntax for values is based on a number, followed by a suffix:

* s: seconds
* m: minutes
* h: hours
* d: days
* w: weeks
* y: years

For example, `2d` means 2 days, `4w` means 4 weeks, and so on.

### Author can merge (PRs) <a name="author-can-merge" />

This condition is satisfied when the author of the PR can merge it.
Expand Down
61 changes: 61 additions & 0 deletions pkg/condition_age.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package labeler

import (
"fmt"
"strconv"
"strings"
"time"
)

func AgeCondition(l *Labeler) Condition {
return Condition{
GetName: func() string {
return "Age of issue/PR"
},
CanEvaluate: func(target *Target) bool {
return target.ghIssue != nil || target.ghPR != nil
},
Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) {
// Parse the age from the configuration
ageDuration, err := parseExtendedDuration(matcher.Age)
if err != nil {
return false, fmt.Errorf("failed to parse age parameter in configuration: %v", err)
}

// Determine the creation time of the issue or PR
var createdAt time.Time
if target.ghIssue != nil {
createdAt = target.ghIssue.CreatedAt.Time
} else if target.ghPR != nil {
createdAt = target.ghPR.CreatedAt.Time
}

age := time.Since(createdAt)

return age > ageDuration, nil
},
}
}

func parseExtendedDuration(s string) (time.Duration, error) {
multiplier := time.Hour * 24 // default to days

if strings.HasSuffix(s, "w") {
multiplier = time.Hour * 24 * 7 // weeks
s = strings.TrimSuffix(s, "w")
} else if strings.HasSuffix(s, "y") {
multiplier = time.Hour * 24 * 365 // years
s = strings.TrimSuffix(s, "y")
} else if strings.HasSuffix(s, "d") {
s = strings.TrimSuffix(s, "d") // days
} else {
return time.ParseDuration(s) // default to time.ParseDuration for hours, minutes, seconds
}

value, err := strconv.Atoi(s)
if err != nil {
return 0, err
}

return time.Duration(value) * multiplier, nil
}
30 changes: 30 additions & 0 deletions pkg/condition_age_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
package labeler

import (
"testing"
"time"
)

func TestParseExtendedDuration(t *testing.T) {
tests := []struct {
input string
expected time.Duration
}{
{"1s", 1 * time.Second},
{"2m", 2 * time.Minute},
{"3h", 3 * time.Hour},
{"4d", 4 * 24 * time.Hour},
{"5w", 5 * 7 * 24 * time.Hour},
{"6y", 6 * 365 * 24 * time.Hour},
}

for _, test := range tests {
result, err := parseExtendedDuration(test.input)
if err != nil {
t.Errorf("failed to parse duration from %s: %v", test.input, err)
}
if result != test.expected {
t.Errorf("expected %v, got %v", test.expected, result)
}
}
}
2 changes: 2 additions & 0 deletions pkg/labeler.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type SizeConfig struct {
}

type LabelMatcher struct {
Age string
AuthorCanMerge string `yaml:"author-can-merge"`
Authors []string
BaseBranch string `yaml:"base-branch"`
Expand Down Expand Up @@ -209,6 +210,7 @@ func (l *Labeler) findMatches(target *Target, config *LabelerConfigV1) (LabelUpd
set: map[string]bool{},
}
conditions := []Condition{
AgeCondition(l),
AuthorCondition(),
AuthorCanMergeCondition(),
BaseBranchCondition(),
Expand Down
33 changes: 32 additions & 1 deletion pkg/labeler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,38 @@ func TestHandleEvent(t *testing.T) {
initialLabels: []string{},
expectedLabels: []string{"NotADraft"},
},

{
event: "pull_request",
payloads: []string{"create_pr"},
name: "Age of a PR in the future",
config: LabelerConfigV1{
Version: 1,
Labels: []LabelMatcher{
{
Label: "ThisIsOld",
Age: "100000000d",
},
},
},
initialLabels: []string{},
expectedLabels: []string{},
},
{
event: "pull_request",
payloads: []string{"create_pr"},
name: "Age of a PR in the past",
config: LabelerConfigV1{
Version: 1,
Labels: []LabelMatcher{
{
Label: "ThisIsOld",
Age: "10d",
},
},
},
initialLabels: []string{},
expectedLabels: []string{"ThisIsOld"},
},
{
event: "pull_request",
payloads: []string{"create_draft_pr"},
Expand Down
Loading