From 09380fb8d002b200cebb4d56e8800569a0b60f16 Mon Sep 17 00:00:00 2001 From: Jan Klattenhoff Date: Wed, 9 Apr 2025 17:16:18 +0200 Subject: [PATCH] feat: add pull request linting action and validation logic --- action.yml | 5 ++ go.mod | 13 +++++ go.sum | 26 ++++++++++ internal/gitea/gitea.go | 16 ++++++ internal/validation/validation.go | 83 +++++++++++++++++++++++++++++++ main.go | 11 ++++ 6 files changed, 154 insertions(+) create mode 100644 action.yml create mode 100644 go.mod create mode 100644 go.sum create mode 100644 internal/gitea/gitea.go create mode 100644 internal/validation/validation.go create mode 100644 main.go diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..524d19d --- /dev/null +++ b/action.yml @@ -0,0 +1,5 @@ +name: 'Pull Request Lint' +description: 'Validates a pull request for semantical commit message' +runs: + using: 'go' + main: 'main.go' diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..d682129 --- /dev/null +++ b/go.mod @@ -0,0 +1,13 @@ +module git.kjan.de/actions/pull-request-lint + +go 1.24.2 + +require ( + code.gitea.io/sdk/gitea v0.21.0 // indirect + github.com/42wim/httpsig v1.2.2 // indirect + github.com/davidmz/go-pageant v1.0.2 // indirect + github.com/go-fed/httpsig v1.1.0 // indirect + github.com/hashicorp/go-version v1.7.0 // indirect + golang.org/x/crypto v0.33.0 // indirect + golang.org/x/sys v0.30.0 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..27105a4 --- /dev/null +++ b/go.sum @@ -0,0 +1,26 @@ +code.gitea.io/sdk/gitea v0.21.0 h1:69n6oz6kEVHRo1+APQQyizkhrZrLsTLXey9142pfkD4= +code.gitea.io/sdk/gitea v0.21.0/go.mod h1:tnBjVhuKJCn8ibdyyhvUyxrR1Ca2KHEoTWoukNhXQPA= +github.com/42wim/httpsig v1.2.2 h1:ofAYoHUNs/MJOLqQ8hIxeyz2QxOz8qdSVvp3PX/oPgA= +github.com/42wim/httpsig v1.2.2/go.mod h1:P/UYo7ytNBFwc+dg35IubuAUIs8zj5zzFIgUCEl55WY= +github.com/davidmz/go-pageant v1.0.2 h1:bPblRCh5jGU+Uptpz6LgMZGD5hJoOt7otgT454WvHn0= +github.com/davidmz/go-pageant v1.0.2/go.mod h1:P2EDDnMqIwG5Rrp05dTRITj9z2zpGcD9efWSkTNKLIE= +github.com/go-fed/httpsig v1.1.0 h1:9M+hb0jkEICD8/cAiNqEB66R87tTINszBRTjwjQzWcI= +github.com/go-fed/httpsig v1.1.0/go.mod h1:RCMrTZvN1bJYtofsG4rd5NaO5obxQ5xBkdiS7xsT7bM= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.33.0 h1:IOBPskki6Lysi0lo9qQvbxiQ+FvsCC/YWOecCHAixus= +golang.org/x/crypto v0.33.0/go.mod h1:bVdXmD7IV/4GdElGPozy6U7lWdRXA4qyRVGJV57uQ5M= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/internal/gitea/gitea.go b/internal/gitea/gitea.go new file mode 100644 index 0000000..a798720 --- /dev/null +++ b/internal/gitea/gitea.go @@ -0,0 +1,16 @@ +package gitea + +import ( + "fmt" + "os" + + "code.gitea.io/sdk/gitea" +) + +func getPrTitle() { + _, err := gitea.NewClient("https://git.kjan.de") + if err != nil { + fmt.Println(err) + os.Exit(1) + } +} diff --git a/internal/validation/validation.go b/internal/validation/validation.go new file mode 100644 index 0000000..46efd86 --- /dev/null +++ b/internal/validation/validation.go @@ -0,0 +1,83 @@ +package validation + +import ( + "fmt" + "regexp" + "strings" +) + +func ValidateConventionalCommit(commit string) error { + // Regex to match the commit format + // type(scope)!: description + // or + // type!: description + // or + // type: description + re := regexp.MustCompile(`^(?P[a-z]+)(?P\([a-z]+\))?(?P!)?: (?P[a-z].+)$`) + match := re.FindStringSubmatch(commit) + + if len(match) == 0 { + return fmt.Errorf("invalid commit format") + } + + typeIndex := re.SubexpIndex("type") + scopeIndex := re.SubexpIndex("scope") + breakingIndex := re.SubexpIndex("breaking") + descriptionIndex := re.SubexpIndex("description") + + commitType := match[typeIndex] + scope := match[scopeIndex] + breaking := match[breakingIndex] + description := match[descriptionIndex] + + // Type MUST be lowercase + if commitType != strings.ToLower(commitType) { + return fmt.Errorf("type must be lowercase") + } + + // Description MUST start with lowercase + if description != strings.ToLower(description) { + return fmt.Errorf("description must start with lowercase") + } + + // Scope MUST be lowercase + if scope != "" && scope != strings.ToLower(scope) { + return fmt.Errorf("scope must be lowercase") + } + + // Check for breaking change indicator + hasBreakingChangeFooter := strings.Contains(commit, "BREAKING CHANGE:") + if breaking == "!" && hasBreakingChangeFooter { + return fmt.Errorf("breaking change indicator and footer are mutually exclusive") + } + + lines := strings.Split(commit, "\n") + if len(lines) > 1 { + body := strings.Join(lines[1:], "\n") + // Check if body is separated from description by a blank line + if !strings.HasPrefix(body, "\n") { + return fmt.Errorf("body must be separated from description by a blank line") + } + } + + // Check for footers + footerRegex := regexp.MustCompile(`(?m)^(?P[A-Za-z-]+|BREAKING CHANGE): (?P.*)$`) + footerMatches := footerRegex.FindAllStringSubmatch(commit, -1) + + for _, footerMatch := range footerMatches { + tokenIndex := footerRegex.SubexpIndex("token") + token := footerMatch[tokenIndex] + + // BREAKING-CHANGE MUST be synonymous with BREAKING CHANGE + if token == "BREAKING-CHANGE" { + continue + } + + // Token MUST use - in place of whitespace characters, except for BREAKING CHANGE + if token != "BREAKING CHANGE" && strings.Contains(token, " ") { + return fmt.Errorf("footer token must use - in place of whitespace characters") + } + } + + return nil +} diff --git a/main.go b/main.go new file mode 100644 index 0000000..537ff8d --- /dev/null +++ b/main.go @@ -0,0 +1,11 @@ +package pullrequestlint + +import ( + "fmt" + "os" +) + +func main() { + prTitle := os.Getenv("GITEA_PULL_REQUEST_TITLE") + fmt.Println(prTitle) +}