Skip to content

Commit

Permalink
Support labels based on PR / Issue age (#113)
Browse files Browse the repository at this point in the history
Signed-off-by: Galo Navarro <anglorvaroa@gmail.com>
  • Loading branch information
srvaroa committed Nov 21, 2023
1 parent 50a2c3d commit 7440435
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@
coverage.out
action
action.tar.gz
.vscode
27 changes: 26 additions & 1 deletion README.md
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
@@ -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
@@ -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
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
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

0 comments on commit 7440435

Please sign in to comment.