diff --git a/.gitignore b/.gitignore
index 5a222d1..52eed31 100644
--- a/.gitignore
+++ b/.gitignore
@@ -3,3 +3,4 @@
coverage.out
action
action.tar.gz
+.vscode
diff --git a/README.md b/README.md
index 9b97eef..f948682 100644
--- a/README.md
+++ b/README.md
@@ -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
@@ -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 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
@@ -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)
+
+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 schedule trigger.
+
+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)
This condition is satisfied when the author of the PR can merge it.
diff --git a/pkg/condition_age.go b/pkg/condition_age.go
new file mode 100644
index 0000000..b27de0b
--- /dev/null
+++ b/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
+}
diff --git a/pkg/condition_age_test.go b/pkg/condition_age_test.go
new file mode 100644
index 0000000..f3e2d3c
--- /dev/null
+++ b/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)
+ }
+ }
+}
diff --git a/pkg/labeler.go b/pkg/labeler.go
index 7fbc931..bbe0a1f 100644
--- a/pkg/labeler.go
+++ b/pkg/labeler.go
@@ -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"`
@@ -209,6 +210,7 @@ func (l *Labeler) findMatches(target *Target, config *LabelerConfigV1) (LabelUpd
set: map[string]bool{},
}
conditions := []Condition{
+ AgeCondition(l),
AuthorCondition(),
AuthorCanMergeCondition(),
BaseBranchCondition(),
diff --git a/pkg/labeler_test.go b/pkg/labeler_test.go
index e6792e6..04296a7 100644
--- a/pkg/labeler_test.go
+++ b/pkg/labeler_test.go
@@ -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"},