Merge branch 'main' into renovate/configure
All checks were successful
Lint Pull Request / Lint PR Title (pull_request) Successful in 9s

This commit is contained in:
Jan K9f 2025-04-09 16:44:16 +00:00
commit 157997d8d7
Signed by:
GPG key ID: 944223E4D46B7412
8 changed files with 219 additions and 1 deletions

21
.gitea/workflows/pr.yml Normal file
View file

@ -0,0 +1,21 @@
name: "Lint Pull Request"
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
lint:
name: Lint PR Title
runs-on: ubuntu-latest
if: github.base_ref != 'release'
steps:
- name: Install go
uses: actions/setup-go@v5
with:
go-version: 1.24.2
- name: Run Pull Request Lint Action
uses: https://git.kjan.de/actions/pull-request-lint@main

View file

@ -1,3 +1,41 @@
# pull-request-lint # pull-request-lint
Lints the title of a pull request A GitHub/Gitea Action that lints pull request titles to ensure they follow the [Conventional Commits](https://www.conventionalcommits.org/) format.
## Usage
Add the following to your GitHub/Gitea workflow:
```yaml
name: Pull Request Lint
on:
pull_request:
types: [opened, edited, reopened, synchronize]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install go
uses: actions/setup-go@v5
with:
go-version: 1.24.2
- uses: https://git.kjan.de/actions/pull-request-lint@release
```
## Validation Rules
The action enforces the following Conventional Commits rules for PR titles:
- Format must be: `type(scope)!: description` (scope and breaking change marker `!` are optional)
- Type and scope must be lowercase
- Description must start with lowercase
- Breaking change indicator and footer are mutually exclusive
- Body must be separated from description by a blank line
- Footer tokens must use hyphens instead of spaces (except for "BREAKING CHANGE")
## License
See [LICENSE](LICENSE) file for details.

5
action.yml Normal file
View file

@ -0,0 +1,5 @@
name: 'Pull Request Lint'
description: 'A gitea action to validate if a pr follows the semantic commit guidelines'
runs:
using: 'go'
main: 'main.go'

View file

@ -1,3 +1,32 @@
# [1.0.0](https://git.kjan.de/actions/pull-request-lint/compare/v0.3.0...v1.0.0) (2025-04-09)
### Code Refactoring
* some polishing ([86febd9](https://git.kjan.de/actions/pull-request-lint/commit/86febd99e507d250adb18fdfede0a01f6e6f8d46))
### BREAKING CHANGES
* Version 1
# [0.3.0](https://git.kjan.de/actions/pull-request-lint/compare/v0.2.1...v0.3.0) (2025-04-09)
### Bug Fixes
* **main:** improve error message for invalid PR title ([c84b1ec](https://git.kjan.de/actions/pull-request-lint/commit/c84b1ecd26d7cbd4530fed79e6078ced5d222e18))
* **main:** improve error message for invalid PR title ([8981c83](https://git.kjan.de/actions/pull-request-lint/commit/8981c8367edc7beff7a477510ba3a3a98b6e6751))
* **validation:** update error message for invalid commit format ([9b1a6a9](https://git.kjan.de/actions/pull-request-lint/commit/9b1a6a928e3483fd4ea6579cdcd8c8a9e95bb669))
### Features
* **action:** update to use Go for pull request linting ([6d9c480](https://git.kjan.de/actions/pull-request-lint/commit/6d9c48078c912b91bb60d8147b1b2d7e74ad8643))
* add GitHub event handling and pull request title display ([0448235](https://git.kjan.de/actions/pull-request-lint/commit/044823552b5fecf0b076098d1bc47d213c00f604))
* add pull request linting action and validation logic ([09380fb](https://git.kjan.de/actions/pull-request-lint/commit/09380fb8d002b200cebb4d56e8800569a0b60f16))
* **invoke-binary:** add ls command execution in runGo function ([da4a8b3](https://git.kjan.de/actions/pull-request-lint/commit/da4a8b3030406f5ba8c82093f2385060cd0d48b8))
## [0.2.1](https://git.kjan.de/actions/pull-request-lint/compare/v0.2.0...v0.2.1) (2025-04-09) ## [0.2.1](https://git.kjan.de/actions/pull-request-lint/compare/v0.2.0...v0.2.1) (2025-04-09)

3
go.mod Normal file
View file

@ -0,0 +1,3 @@
module git.kjan.de/actions/pull-request-lint
go 1.24.2

0
go.sum Normal file
View file

View file

@ -0,0 +1,76 @@
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<type>[a-z]+)(?P<scope>\([a-z]+\))?(?P<breaking>!)?: (?P<description>.+)$`)
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")
commitType := match[typeIndex]
scope := match[scopeIndex]
breaking := match[breakingIndex]
// Type MUST be lowercase
if commitType != strings.ToLower(commitType) {
return fmt.Errorf("type must be 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<token>[A-Za-z-]+|BREAKING CHANGE): (?P<value>.*)$`)
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
}

46
main.go Normal file
View file

@ -0,0 +1,46 @@
package main
import (
"encoding/json"
"fmt"
"os"
"git.kjan.de/actions/pull-request-lint/internal/validation"
)
type GithubEvent struct {
PullRequest struct {
Title string `json:"title"`
} `json:"pull_request"`
}
func main() {
eventPath := os.Getenv("GITHUB_EVENT_PATH")
if eventPath == "" {
fmt.Println("GITHUB_EVENT_PATH not set")
os.Exit(1)
}
eventFile, err := os.Open(eventPath)
if err != nil {
fmt.Printf("Error opening %s: %v\n", eventPath, err)
os.Exit(1)
}
defer eventFile.Close()
var event GithubEvent
decoder := json.NewDecoder(eventFile)
err = decoder.Decode(&event)
if err != nil {
fmt.Printf("Error decoding event.json: %v\n", err)
os.Exit(1)
}
prTitle := event.PullRequest.Title
semanticValidationErr := validation.ValidateConventionalCommit(prTitle)
if semanticValidationErr != nil {
fmt.Println(semanticValidationErr)
os.Exit(1)
}
os.Exit(0)
}