Repository: srvaroa/labeler
Pull request #149 (2024-09-02 15:42:40)

Support last-modified
Signed-off-by: Galo Navarro <anglorvaroa@gmail.com>



Patch:
diff --git a/.github/labeler.yml b/.github/labeler.yml
index f4ed978..a2d9951 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -1,6 +1,16 @@
 version: 1
 
 labels:
+    # Type: Recent changes
+    -   label: "@type/new"
+        last-modified:
+            at-most: 1d
+
+    # Type: Old changes
+    -   label: "@type/old"
+        last-modified: 
+            at-least: 30d
+
     # Type: Build-related changes
     -   label: "@type/build"
         title: '^build(?:\(.+\))?\!?:'
diff --git a/README.md b/README.md
index 9ba325c..922baa1 100644
--- a/README.md
+++ b/README.md
@@ -309,6 +309,9 @@ alphabetical order. Some important considerations:
 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.
 
+If you're looking to evaluate on the modification date of the issue or PR, 
+check on <a href="#last-modified" ></a>
+
 This condition is best used when with a <a href="#schedule">schedule trigger</a>.
 
 Example:
@@ -407,6 +410,41 @@ regular expressions (Regex). Special characters need to be escaped with double
 backslashes. This is because the backslash in Go strings is an escape character
 and therefore must be escaped itself to appear as a literal in the regex.
 
+### Last Modified (PRs and Issues) <a name="last-modified" />
+
+This condition evaluates the modification date of the PR or Issue. 
+
+If you're looking to evaluate on the creation date of the issue or PR, 
+check on <a href="#age" ></a>
+
+This condition is best used when with a <a href="#schedule">schedule trigger</a>.
+
+Examples:
+
+```yaml
+last-modified:
+  at-most: 1d
+```
+Will label PRs or issues that were last modified at most one day ago
+
+```yaml
+last-modified:
+  at-least: 1d
+```
+
+Will label PRs or issues that were last modified at least one day ago
+
+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.
+
 ### Mergeable status (PRs only) <a name="mergeable" />
 
 This condition is satisfied when the [mergeable
diff --git a/pkg/condition_age.go b/pkg/condition_age.go
index b27de0b..be04a36 100644
--- a/pkg/condition_age.go
+++ b/pkg/condition_age.go
@@ -2,8 +2,6 @@ package labeler
 
 import (
 	"fmt"
-	"strconv"
-	"strings"
 	"time"
 )
 
@@ -36,26 +34,3 @@ func AgeCondition(l *Labeler) Condition {
 		},
 	}
 }
-
-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_last_modified.go b/pkg/condition_last_modified.go
new file mode 100644
index 0000000..3012b6f
--- /dev/null
+++ b/pkg/condition_last_modified.go
@@ -0,0 +1,53 @@
+package labeler
+
+import (
+	"fmt"
+	"time"
+
+	"github.com/google/go-github/v50/github"
+)
+
+func LastModifiedCondition(l *Labeler) Condition {
+	return Condition{
+		GetName: func() string {
+			return "Last modification of issue/PR"
+		},
+		CanEvaluate: func(target *Target) bool {
+			return target.ghIssue != nil || target.ghPR != nil
+		},
+		Evaluate: func(target *Target, matcher LabelMatcher) (bool, error) {
+			if matcher.LastModified == nil {
+				return false, fmt.Errorf("no last modified conditions are set in config")
+			}
+			// Determine the last modification time of the issue or PR
+			var lastModifiedAt *github.Timestamp
+			if target.ghIssue != nil {
+				lastModifiedAt = target.ghIssue.UpdatedAt
+			} else if target.ghPR != nil {
+				lastModifiedAt = target.ghPR.UpdatedAt
+			} else {
+				return false, fmt.Errorf("no issue or PR found in target")
+			}
+			duration := time.Since(lastModifiedAt.Time)
+
+			if matcher.LastModified.AtMost != "" {
+				maxDuration, err := parseExtendedDuration(matcher.LastModified.AtMost)
+				if err != nil {
+					return false, fmt.Errorf("failed to parse `last-modified.at-most` parameter in configuration: %v", err)
+				}
+				return duration <= maxDuration, nil
+			}
+
+			if matcher.LastModified.AtLeast != "" {
+				minDuration, err := parseExtendedDuration(matcher.LastModified.AtLeast)
+				if err != nil {
+					return false, fmt.Errorf("failed to parse `last-modified.at-least` parameter in configuration: %v", err)
+				}
+				return duration >= minDuration, nil
+			}
+
+			return false, fmt.Errorf("no last modified conditions are set in config")
+
+		},
+	}
+}
diff --git a/pkg/labeler.go b/pkg/labeler.go
index eb19ef7..21250b5 100644
--- a/pkg/labeler.go
+++ b/pkg/labeler.go
@@ -7,6 +7,11 @@ import (
 	gh "github.com/google/go-github/v50/github"
 )
 
+type DurationConfig struct {
+	AtLeast string `yaml:"at-least"`
+	AtMost  string `yaml:"at-most"`
+}
+
 type SizeConfig struct {
 	ExcludeFiles []string `yaml:"exclude-files"`
 	Above        string
@@ -23,6 +28,7 @@ type LabelMatcher struct {
 	Draft          string
 	Files          []string
 	Label          string
+	LastModified   *DurationConfig `yaml:"last-modified"`
 	Mergeable      string
 	Negate         bool
 	Size           *SizeConfig
@@ -223,6 +229,7 @@ func (l *Labeler) findMatches(target *Target, config *LabelerConfigV1) (LabelUpd
 		BodyCondition(),
 		BranchCondition(),
 		FilesCondition(l),
+		LastModifiedCondition(l),
 		IsDraftCondition(),
 		IsMergeableCondition(),
 		SizeCondition(l),
diff --git a/pkg/util.go b/pkg/util.go
new file mode 100644
index 0000000..3d2988b
--- /dev/null
+++ b/pkg/util.go
@@ -0,0 +1,30 @@
+package labeler
+
+import (
+	"strconv"
+	"strings"
+	"time"
+)
+
+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
+}


Test patch:
diff --git a/pkg/labeler_test.go b/pkg/labeler_test.go
index 402a930..6a24c92 100644
--- a/pkg/labeler_test.go
+++ b/pkg/labeler_test.go
@@ -965,6 +965,24 @@ func TestHandleEvent(t *testing.T) {
 			initialLabels:  []string{"Meh"},
 			expectedLabels: []string{"Meh", "Test"},
 		},
+		{
+			event:    "pull_request",
+			payloads: []string{"create_pr"},
+			name:     "Add a label to pull request when last-modified.at-least matches",
+			config: LabelerConfigV1{
+				Version: 1,
+				Labels: []LabelMatcher{
+					{
+						Label: "Test",
+						LastModified: &DurationConfig{
+							AtLeast: "1d",
+						},
+					},
+				},
+			},
+			initialLabels:  []string{"Meh"},
+			expectedLabels: []string{"Meh", "Test"},
+		},
 	}
 
 	for _, tc := range testCases {