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..5397c28 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 + This condition is best used when with a schedule trigger. Example: @@ -407,6 +410,42 @@ 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) + +This condition is satisfied when the modification date of the PR or Issue is +larger than the given one. + +If you're looking to evaluate on the creation date of the issue or PR, +check on + +This condition is best used when with a schedule trigger. + +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) 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/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 { 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 +} diff --git a/pkg/condition_age_test.go b/pkg/util_test.go similarity index 100% rename from pkg/condition_age_test.go rename to pkg/util_test.go