feat(gitea): add lint-pr command
lint-pr runs golangci-lint, captures the result and uses it directly without temporary files It also allows to automatically retrieve the PRs base commit and run golangci-lint only against changes in the current PR
This commit is contained in:
parent
1707692170
commit
c1c1667d9f
16 changed files with 570 additions and 83 deletions
36
.drone.yml
36
.drone.yml
|
@ -14,38 +14,13 @@ steps:
|
||||||
image: docker.io/golang:1.20-bullseye
|
image: docker.io/golang:1.20-bullseye
|
||||||
network_mode: host
|
network_mode: host
|
||||||
commands:
|
commands:
|
||||||
- apt-get update && apt-get install -y jq
|
|
||||||
- go mod download
|
- go mod download
|
||||||
- go install "github.com/vektra/mockery/v2@$(curl https://api.github.com/repos/vektra/mockery/releases | jq -r '. | first | .tag_name')"
|
- go run github.com/magefile/mage -d build -w . generate
|
||||||
- go generate ./...
|
|
||||||
volumes:
|
volumes:
|
||||||
- name: go-cache
|
- name: go-cache
|
||||||
path: /go
|
path: /go
|
||||||
|
|
||||||
- name: Lint PR
|
- name: Lint
|
||||||
image: docker.io/golangci/golangci-lint:latest
|
|
||||||
network_mode: host
|
|
||||||
environment:
|
|
||||||
CGO_ENABLED: "0"
|
|
||||||
GOMEMLIMIT: "1150MiB"
|
|
||||||
NITTER_BASE_ADDRESS: https://code.icb4dc0.de
|
|
||||||
NITTER_TOKEN:
|
|
||||||
from_secret: gitea_token
|
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- pull_request
|
|
||||||
commands:
|
|
||||||
- apt-get update && apt-get install -y jq
|
|
||||||
- mkdir out
|
|
||||||
- golangci-lint run --out-format json --issues-exit-code 0 --new-from-rev "$(curl https://code.icb4dc0.de/api/v1/repos/prskr/nitter/pulls/1 | jq -r '.base.sha')" > out/results.json
|
|
||||||
- go run main.go gitea pr --namespace "$${DRONE_REPO_NAMESPACE}" --repo "$${DRONE_REPO_NAME}" --result-file out/results.json --pull-index "$${DRONE_PULL_REQUEST}"
|
|
||||||
depends_on:
|
|
||||||
- Setup
|
|
||||||
volumes:
|
|
||||||
- name: go-cache
|
|
||||||
path: /go
|
|
||||||
|
|
||||||
- name: Lint repo
|
|
||||||
image: docker.io/golangci/golangci-lint:latest
|
image: docker.io/golangci/golangci-lint:latest
|
||||||
environment:
|
environment:
|
||||||
CGO_ENABLED: "0"
|
CGO_ENABLED: "0"
|
||||||
|
@ -53,13 +28,8 @@ steps:
|
||||||
NITTER_BASE_ADDRESS: https://code.icb4dc0.de
|
NITTER_BASE_ADDRESS: https://code.icb4dc0.de
|
||||||
NITTER_TOKEN:
|
NITTER_TOKEN:
|
||||||
from_secret: gitea_token
|
from_secret: gitea_token
|
||||||
when:
|
|
||||||
event:
|
|
||||||
- push
|
|
||||||
- tag
|
|
||||||
commands:
|
commands:
|
||||||
- mkdir out
|
- go run github.com/magefile/mage -d build -w . lint
|
||||||
- golangci-lint run -v
|
|
||||||
depends_on:
|
depends_on:
|
||||||
- Setup
|
- Setup
|
||||||
volumes:
|
volumes:
|
||||||
|
|
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -21,7 +21,4 @@ mock_*_test.go
|
||||||
# vendor/
|
# vendor/
|
||||||
out/
|
out/
|
||||||
|
|
||||||
# Go workspace file
|
|
||||||
go.work
|
|
||||||
|
|
||||||
.idea/
|
.idea/
|
33
build/format.go
Normal file
33
build/format.go
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/magefile/mage/mg"
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Format() {
|
||||||
|
mg.Deps(GoImports)
|
||||||
|
mg.Deps(GoFumpt)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoImports() error {
|
||||||
|
if err := ensureGoTool("goimports", "golang.org/x/tools/cmd/goimports", "latest"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.RunV(
|
||||||
|
"goimports",
|
||||||
|
"-local=inetmock.icb4dc0.de/inetmock",
|
||||||
|
"-w",
|
||||||
|
WorkingDir,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GoFumpt() error {
|
||||||
|
if err := ensureGoTool("gofumpt", "mvdan.cc/gofumpt", "latest"); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return sh.RunV("gofumpt", "-l", "-w", WorkingDir)
|
||||||
|
}
|
47
build/generate.go
Normal file
47
build/generate.go
Normal file
|
@ -0,0 +1,47 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/magefile/mage/mg"
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
"github.com/magefile/mage/target"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Generate(ctx context.Context) error {
|
||||||
|
mockeryVersion, err := getLatestReleaseTag(ctx, "vektra/mockery")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureGoTool("mockery", "github.com/vektra/mockery/v2", mockeryVersion); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mg.Deps(GenerateGo)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateGo() error {
|
||||||
|
lastMockGeneration, err := target.NewestModTime(GeneratedMockFiles...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
lastSourceModification, err := target.NewestModTime(GoSourceFiles...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Debug("Determined last time mocks where generated", slog.Time("lastMockGeneration", lastMockGeneration))
|
||||||
|
|
||||||
|
if lastMockGeneration.After(lastSourceModification) {
|
||||||
|
slog.Info("Skipping unnecessary 'go generate' invocation")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.RunV("go", "generate", "-x", "./...")
|
||||||
|
}
|
16
build/go.mod
Normal file
16
build/go.mod
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
module build
|
||||||
|
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/carlmjohnson/requests v0.23.2
|
||||||
|
github.com/magefile/mage v1.14.0
|
||||||
|
golang.org/x/exp v0.0.0-20230307190834-24139beb5833
|
||||||
|
golang.org/x/sync v0.1.0
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
code.gitea.io/sdk/gitea v0.15.1 // indirect
|
||||||
|
github.com/hashicorp/go-version v1.2.1 // indirect
|
||||||
|
golang.org/x/net v0.5.0 // indirect
|
||||||
|
)
|
38
build/go.sum
Normal file
38
build/go.sum
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
code.gitea.io/gitea-vet v0.2.1/go.mod h1:zcNbT/aJEmivCAhfmkHOlT645KNOf9W2KnkLgFjGGfE=
|
||||||
|
code.gitea.io/sdk/gitea v0.15.1 h1:WJreC7YYuxbn0UDaPuWIe/mtiNKTvLN8MLkaw71yx/M=
|
||||||
|
code.gitea.io/sdk/gitea v0.15.1/go.mod h1:klY2LVI3s3NChzIk/MzMn7G1FHrfU7qd63iSMVoHRBA=
|
||||||
|
github.com/carlmjohnson/requests v0.23.2 h1:SzaY+/5v8QOvt++7HTXe1xgmIb3wc/bYf2QJmrO73sM=
|
||||||
|
github.com/carlmjohnson/requests v0.23.2/go.mod h1:09VwhOaRQYCraJcByjEuvuOGO1jxUjIx6vnAEkt2ges=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/hashicorp/go-version v1.2.1 h1:zEfKbn2+PDgroKdiOzqiE8rsmLqU2uwi5PB5pBJ3TkI=
|
||||||
|
github.com/hashicorp/go-version v1.2.1/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA=
|
||||||
|
github.com/magefile/mage v1.14.0 h1:6QDX3g6z1YvJ4olPhT1wksUcSa/V0a1B+pJb73fBjyo=
|
||||||
|
github.com/magefile/mage v1.14.0/go.mod h1:z5UZb/iS3GoOSn0JgWuiw7dxlurVYTu+/jHXqQg881A=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
|
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
|
||||||
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
|
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
|
golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
|
||||||
|
golang.org/x/exp v0.0.0-20230307190834-24139beb5833/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
|
||||||
|
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
|
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
|
golang.org/x/net v0.5.0 h1:GyT4nK/YDHSqa1c4753ouYCDajOYKTja9Xb/OHtgvSw=
|
||||||
|
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||||
|
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
|
||||||
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
|
golang.org/x/tools v0.0.0-20200325010219-a49f79bcc224/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8=
|
||||||
|
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
42
build/lint.go
Normal file
42
build/lint.go
Normal file
|
@ -0,0 +1,42 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/magefile/mage/mg"
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Lint(ctx context.Context) {
|
||||||
|
mg.CtxDeps(ctx, Generate)
|
||||||
|
mg.CtxDeps(ctx, Format)
|
||||||
|
mg.Deps(LintGo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LintGo() (err error) {
|
||||||
|
if IsPRBuild {
|
||||||
|
return lintGoPR()
|
||||||
|
}
|
||||||
|
|
||||||
|
return sh.RunV(
|
||||||
|
"golangci-lint",
|
||||||
|
"run",
|
||||||
|
"-v",
|
||||||
|
"--issues-exit-code=1",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lintGoPR() error {
|
||||||
|
return GoRun(
|
||||||
|
"main.go",
|
||||||
|
"gitea",
|
||||||
|
"lint-pr",
|
||||||
|
"--from-pr-base",
|
||||||
|
fmt.Sprintf("--namespace=%s", repoOwner),
|
||||||
|
fmt.Sprintf("--repo=%s", repoName),
|
||||||
|
fmt.Sprintf("--pull-index=%d", prIndex),
|
||||||
|
)
|
||||||
|
}
|
99
build/setup.go
Normal file
99
build/setup.go
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
WorkingDir string
|
||||||
|
GoSourceFiles []string
|
||||||
|
GeneratedMockFiles []string
|
||||||
|
IsReleaseBuild bool
|
||||||
|
IsPRBuild bool
|
||||||
|
|
||||||
|
repoOwner = "prskr"
|
||||||
|
repoName = "nitter"
|
||||||
|
prIndex int64 = 1
|
||||||
|
|
||||||
|
dirsToIgnore = []string{
|
||||||
|
".git",
|
||||||
|
"build",
|
||||||
|
".run",
|
||||||
|
".task",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
slog.SetDefault(slog.New(slog.NewTextHandler(os.Stderr)))
|
||||||
|
|
||||||
|
IsReleaseBuild = strings.EqualFold(os.Getenv("DRONE_BUILD_EVENT"), "tag")
|
||||||
|
IsPRBuild = strings.EqualFold(os.Getenv("DRONE_BUILD_EVENT"), "pull_request")
|
||||||
|
|
||||||
|
if wd, err := os.Getwd(); err != nil {
|
||||||
|
slog.Error("Failed to get working directory", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
WorkingDir = wd
|
||||||
|
}
|
||||||
|
|
||||||
|
if owner := os.Getenv("DRONE_REPO_NAMESPACE"); owner != "" {
|
||||||
|
repoOwner = owner
|
||||||
|
}
|
||||||
|
|
||||||
|
if name := os.Getenv("DRONE_REPO_NAME"); name != "" {
|
||||||
|
repoName = name
|
||||||
|
}
|
||||||
|
|
||||||
|
if prIdxRaw := os.Getenv("DRONE_PULL_REQUEST"); prIdxRaw != "" {
|
||||||
|
if parsed, err := strconv.ParseInt(prIdxRaw, 10, 64); err != nil {
|
||||||
|
slog.Error("Failed to parse PR index", err)
|
||||||
|
os.Exit(1)
|
||||||
|
} else {
|
||||||
|
prIndex = parsed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := initSourceFiles(); err != nil {
|
||||||
|
slog.Error("Failed to init source files", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.Info("Completed initialization")
|
||||||
|
}
|
||||||
|
|
||||||
|
func initSourceFiles() error {
|
||||||
|
return filepath.WalkDir(WorkingDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.IsDir() {
|
||||||
|
if slices.Contains(dirsToIgnore, filepath.Base(path)) {
|
||||||
|
return fs.SkipDir
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ext, found := strings.Cut(filepath.Base(path), ".")
|
||||||
|
if !found {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ext {
|
||||||
|
case "mock.go":
|
||||||
|
GeneratedMockFiles = append(GeneratedMockFiles, path)
|
||||||
|
case "go":
|
||||||
|
GoSourceFiles = append(GoSourceFiles, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
71
build/tools.go
Normal file
71
build/tools.go
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
//go:build mage
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/carlmjohnson/requests"
|
||||||
|
"github.com/magefile/mage/sh"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
GoReleaser = sh.RunCmd("goreleaser")
|
||||||
|
GoInstall = sh.RunCmd("go", "install")
|
||||||
|
GoBuild = sh.RunCmd("go", "build")
|
||||||
|
GoRun = sh.RunCmd("go", "run")
|
||||||
|
)
|
||||||
|
|
||||||
|
func getLatestReleaseTag(ctx context.Context, repo string) (tag string, err error) {
|
||||||
|
type release struct {
|
||||||
|
TagName string `json:"tag_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var releases []release
|
||||||
|
|
||||||
|
err = requests.
|
||||||
|
URL(fmt.Sprintf("https://%s", path.Join("api.github.com/repos", repo, "releases"))).
|
||||||
|
ToJSON(&releases).
|
||||||
|
Fetch(ctx)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(releases) < 1 {
|
||||||
|
return "", fmt.Errorf("no release found for repo %s", repo)
|
||||||
|
}
|
||||||
|
|
||||||
|
return releases[0].TagName, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureURLTool(ctx context.Context, toolName, downloadURL string) error {
|
||||||
|
return checkForTool(toolName, func() error {
|
||||||
|
return requests.
|
||||||
|
URL(downloadURL).
|
||||||
|
ToFile(filepath.Join("/", "usr", "local", "bin", toolName)).
|
||||||
|
Fetch(ctx)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureGoTool(toolName, importPath, version string) error {
|
||||||
|
return checkForTool(toolName, func() error {
|
||||||
|
toolToInstall := fmt.Sprintf("%s@%s", importPath, version)
|
||||||
|
slog.Info("Installing Go tool", slog.String("toolToInstall", toolToInstall))
|
||||||
|
return GoInstall(toolToInstall)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkForTool(toolName string, fallbackAction func() error) error {
|
||||||
|
if _, err := exec.LookPath(toolName); err != nil {
|
||||||
|
slog.Warn("tool is missing", slog.String("toolName", toolName))
|
||||||
|
return fallbackAction()
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
6
go.work
Normal file
6
go.work
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
go 1.20
|
||||||
|
|
||||||
|
use (
|
||||||
|
.
|
||||||
|
./build
|
||||||
|
)
|
9
go.work.sum
Normal file
9
go.work.sum
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||||
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
|
golang.org/x/net v0.6.0 h1:L4ZwwTvKW9gr0ZMS1yrHD9GZhIuVjOBBnaKH+SPQK0Q=
|
||||||
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
|
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo=
|
||||||
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
|
@ -1,12 +1,44 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
gosdk "code.gitea.io/sdk/gitea"
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
giteaSdk "code.gitea.io/sdk/gitea"
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nitter/nitters/gitea"
|
"code.icb4dc0.de/prskr/nitter/nitters/gitea"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type reviewStateTypeValue giteaSdk.ReviewStateType
|
||||||
|
|
||||||
|
func (r *reviewStateTypeValue) String() string {
|
||||||
|
return string(*r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reviewStateTypeValue) Set(s string) error {
|
||||||
|
switch strings.ToUpper(s) {
|
||||||
|
case string(giteaSdk.ReviewStateApproved):
|
||||||
|
*r = reviewStateTypeValue(giteaSdk.ReviewStateApproved)
|
||||||
|
return nil
|
||||||
|
case string(giteaSdk.ReviewStateRequestChanges):
|
||||||
|
*r = reviewStateTypeValue(giteaSdk.ReviewStateRequestChanges)
|
||||||
|
return nil
|
||||||
|
case string(giteaSdk.ReviewStateComment):
|
||||||
|
*r = reviewStateTypeValue(giteaSdk.ReviewStateComment)
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("value %s doesn't match any expected state type", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *reviewStateTypeValue) Type() string {
|
||||||
|
return "string"
|
||||||
|
}
|
||||||
|
|
||||||
|
var errorReviewStateType = reviewStateTypeValue(giteaSdk.ReviewStateComment)
|
||||||
|
|
||||||
|
//nolint:lll //flag setup causes long lines
|
||||||
func Gitea() *cobra.Command {
|
func Gitea() *cobra.Command {
|
||||||
giteaCmd := &cobra.Command{
|
giteaCmd := &cobra.Command{
|
||||||
Use: "gitea",
|
Use: "gitea",
|
||||||
|
@ -18,31 +50,9 @@ func Gitea() *cobra.Command {
|
||||||
|
|
||||||
pr := &cobra.Command{
|
pr := &cobra.Command{
|
||||||
Use: "pull-request",
|
Use: "pull-request",
|
||||||
Aliases: []string{"merge-request", "pr", "mr", "pull"},
|
Short: "Read golangci-lint result and create a PR review",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
Aliases: []string{"merge-request", "pr", "pull"},
|
||||||
cfg, err := LoadConfig[gitea.GiteaPRConfig](cmd.Flags())
|
RunE: runGiteaPRNitting,
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = cfg.Validate(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
report, issues, err := ReadResultsFile(cmd.Flag("result-file").Value.String())
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
giteaClient, err := gosdk.NewClient(cfg.BaseAddress, gosdk.SetContext(cmd.Context()), gosdk.SetToken(cfg.Token))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
nitter := gitea.NewPRNitter(giteaClient, cfg)
|
|
||||||
|
|
||||||
return nitter.Report(report, issues)
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pr.Flags().Int64P(
|
pr.Flags().Int64P(
|
||||||
|
@ -52,7 +62,97 @@ func Gitea() *cobra.Command {
|
||||||
"PR index to add reviews to - note, this is not the ID of the PR but its number [$NITTER_PULL_INDEX]",
|
"PR index to add reviews to - note, this is not the ID of the PR but its number [$NITTER_PULL_INDEX]",
|
||||||
)
|
)
|
||||||
|
|
||||||
giteaCmd.AddCommand(pr)
|
pr.Flags().Var(&errorReviewStateType, "review-state", "state in which to create the review if there are issues [comment,approved,]")
|
||||||
|
pr.Flags().StringP("result-file", "f", "", "path to the golangci-lint JSON output [$NITTER_RESULT_FILE]")
|
||||||
|
|
||||||
|
lintPr := &cobra.Command{
|
||||||
|
Use: "lint-pull-request",
|
||||||
|
Short: "Run golangci-lint directly and create a PR review based on the captured results - required golangci-lint being in $PATH",
|
||||||
|
Aliases: []string{"lint-pr", "lint-pull"},
|
||||||
|
RunE: runGiteaPRLintNitting,
|
||||||
|
}
|
||||||
|
|
||||||
|
lintPr.Flags().Var(&errorReviewStateType, "review-state", "state in which to create the review if there are issues [comment,approved,]")
|
||||||
|
|
||||||
|
lintPr.Flags().Int64P(
|
||||||
|
"pull-index",
|
||||||
|
"i",
|
||||||
|
-1,
|
||||||
|
"PR index to add reviews to - note, this is not the ID of the PR but its number [$NITTER_PULL_INDEX]",
|
||||||
|
)
|
||||||
|
|
||||||
|
lintPr.Flags().Bool(
|
||||||
|
"from-pr-base",
|
||||||
|
false,
|
||||||
|
"if enabled, nitter will get the PRs metadata and start golangci-lint with --new-from-rev=<PR base commit>",
|
||||||
|
)
|
||||||
|
|
||||||
|
lintPr.Flags().StringSlice(
|
||||||
|
"golangci-lint-extra-args",
|
||||||
|
nil,
|
||||||
|
"Extra args to append to golangci-lint run commands, keep in mind that out-format will be set to JSON and --new-from-rev if you enable --from-pr-base",
|
||||||
|
)
|
||||||
|
|
||||||
|
giteaCmd.AddCommand(pr, lintPr)
|
||||||
|
|
||||||
return giteaCmd
|
return giteaCmd
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func runGiteaPRNitting(cmd *cobra.Command, args []string) error {
|
||||||
|
cfg, err := LoadConfig[gitea.GiteaPRConfig](cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cfg.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
report, issues, err := ReadResultsFile(cmd.Flag("result-file").Value.String())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaClient, err := giteaSdk.NewClient(cfg.BaseAddress, giteaSdk.SetContext(cmd.Context()), giteaSdk.SetToken(cfg.Token))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nitter := gitea.NewPRNitter(giteaClient, cfg)
|
||||||
|
|
||||||
|
return nitter.Report(report, issues)
|
||||||
|
}
|
||||||
|
|
||||||
|
func runGiteaPRLintNitting(cmd *cobra.Command, args []string) error {
|
||||||
|
cfg, err := LoadConfig[gitea.GiteaPRLintConfig](cmd.Flags())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = cfg.Validate(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
giteaClient, err := giteaSdk.NewClient(cfg.BaseAddress, giteaSdk.SetContext(cmd.Context()), giteaSdk.SetToken(cfg.Token))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cfg.FromPRBase {
|
||||||
|
var pr *giteaSdk.PullRequest
|
||||||
|
if pr, _, err = giteaClient.GetPullRequest(cfg.Namespace, cfg.Repo, cfg.PRIndex); err != nil {
|
||||||
|
return err
|
||||||
|
} else {
|
||||||
|
cfg.ExtraArgs = append(cfg.ExtraArgs, fmt.Sprintf("--new-from-rev=%s", pr.Base.Sha))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
report, issues, err := RunGolangCiLint(cmd.Context(), cfg.ExtraArgs)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
nitter := gitea.NewPRNitter(giteaClient, &cfg.GiteaPRConfig)
|
||||||
|
|
||||||
|
return nitter.Report(report, issues)
|
||||||
|
}
|
||||||
|
|
|
@ -1,10 +1,14 @@
|
||||||
package commands
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
|
||||||
"github.com/golangci/golangci-lint/pkg/printers"
|
"github.com/golangci/golangci-lint/pkg/printers"
|
||||||
"github.com/golangci/golangci-lint/pkg/report"
|
"github.com/golangci/golangci-lint/pkg/report"
|
||||||
|
@ -39,3 +43,44 @@ func ReadResults(reader io.Reader) (*report.Data, []result.Issue, error) {
|
||||||
|
|
||||||
return printerResult.Report, printerResult.Issues, nil
|
return printerResult.Report, printerResult.Issues, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RunGolangCiLint(ctx context.Context, extraFlags []string) (*report.Data, []result.Issue, error) {
|
||||||
|
const defaultArgsLen = 3
|
||||||
|
golangCiLintArgs := make([]string, 1, defaultArgsLen+len(extraFlags))
|
||||||
|
golangCiLintArgs[0] = "run"
|
||||||
|
golangCiLintArgs = append(golangCiLintArgs, extraFlags...)
|
||||||
|
golangCiLintArgs = append(golangCiLintArgs, "--issues-exit-code=0", "--out-format=json")
|
||||||
|
|
||||||
|
golangCiLintArgs = append(golangCiLintArgs, extraFlags...)
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(
|
||||||
|
ctx,
|
||||||
|
"golangci-lint",
|
||||||
|
golangCiLintArgs...,
|
||||||
|
)
|
||||||
|
|
||||||
|
stderrBuf := bytes.NewBuffer(nil)
|
||||||
|
|
||||||
|
cmd.Stderr = stderrBuf
|
||||||
|
|
||||||
|
stdout, err := cmd.StdoutPipe()
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Start(); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var printerResult printers.JSONResult
|
||||||
|
|
||||||
|
if err := json.NewDecoder(stdout).Decode(&printerResult); err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cmd.Wait(); err != nil {
|
||||||
|
return nil, nil, fmt.Errorf("stderr: %s - %w", stderrBuf.String(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return printerResult.Report, printerResult.Issues, nil
|
||||||
|
}
|
||||||
|
|
1
main.go
1
main.go
|
@ -19,7 +19,6 @@ var root = &cobra.Command{
|
||||||
func main() {
|
func main() {
|
||||||
root.PersistentFlags().StringP("namespace", "n", "", "Namespace a.k.a. organization/owner/group of the repository [$NITTER_NAMESPACE]")
|
root.PersistentFlags().StringP("namespace", "n", "", "Namespace a.k.a. organization/owner/group of the repository [$NITTER_NAMESPACE]")
|
||||||
root.PersistentFlags().StringP("repo", "r", "", "Repo to interact with [$NITTER_REPO]")
|
root.PersistentFlags().StringP("repo", "r", "", "Repo to interact with [$NITTER_REPO]")
|
||||||
root.PersistentFlags().StringP("result-file", "f", "", "path to the golangci-lint JSON output [$NITTER_RESULT_FILE]")
|
|
||||||
|
|
||||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, os.Kill)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
|
@ -3,6 +3,8 @@ package gitea
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
|
||||||
|
"code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nitter/nitters"
|
"code.icb4dc0.de/prskr/nitter/nitters"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,7 +38,8 @@ func (gc GiteaConfig) Validate() error {
|
||||||
|
|
||||||
type GiteaPRConfig struct {
|
type GiteaPRConfig struct {
|
||||||
GiteaConfig `mapstructure:",squash"`
|
GiteaConfig `mapstructure:",squash"`
|
||||||
PRIndex int64 `mapstructure:"pull-index"`
|
PRIndex int64 `mapstructure:"pull-index"`
|
||||||
|
ReviewState gitea.ReviewStateType `mapstructure:"review-state"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (gpc GiteaPRConfig) Validate() error {
|
func (gpc GiteaPRConfig) Validate() error {
|
||||||
|
@ -50,3 +53,13 @@ func (gpc GiteaPRConfig) Validate() error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type GiteaPRLintConfig struct {
|
||||||
|
GiteaPRConfig `mapstructure:",squash"`
|
||||||
|
FromPRBase bool `mapstructure:"from-pr-base"`
|
||||||
|
ExtraArgs []string `mapstructure:"golangci-lint-extra-args"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gpc GiteaPRLintConfig) Validate() error {
|
||||||
|
return gpc.GiteaPRConfig.Validate()
|
||||||
|
}
|
||||||
|
|
|
@ -47,7 +47,6 @@ type prNitter struct {
|
||||||
|
|
||||||
func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
templateBuf := bytes.Buffer{}
|
templateBuf := bytes.Buffer{}
|
||||||
|
|
||||||
summaryData := map[string]any{
|
summaryData := map[string]any{
|
||||||
"Report": report,
|
"Report": report,
|
||||||
"Issues": issues,
|
"Issues": issues,
|
||||||
|
@ -58,28 +57,31 @@ func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
pullReviewOptions := gitea.CreatePullReviewOptions{
|
pullReviewOptions := gitea.CreatePullReviewOptions{
|
||||||
State: gitea.ReviewStateComment,
|
State: p.cfg.ReviewState,
|
||||||
Body: templateBuf.String(),
|
Body: templateBuf.String(),
|
||||||
Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)),
|
Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)),
|
||||||
}
|
}
|
||||||
|
|
||||||
templateBuf.Reset()
|
if len(issues) == 0 {
|
||||||
|
pullReviewOptions.State = gitea.ReviewStateApproved
|
||||||
for i := range issues {
|
} else {
|
||||||
templateData := map[string]any{
|
|
||||||
"Issue": issues[i],
|
|
||||||
}
|
|
||||||
if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{
|
|
||||||
Path: issues[i].Pos.Filename,
|
|
||||||
Body: templateBuf.String(),
|
|
||||||
NewLineNum: int64(issues[i].Pos.Line),
|
|
||||||
})
|
|
||||||
|
|
||||||
templateBuf.Reset()
|
templateBuf.Reset()
|
||||||
|
for i := range issues {
|
||||||
|
templateData := map[string]any{
|
||||||
|
"Issue": issues[i],
|
||||||
|
}
|
||||||
|
if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{
|
||||||
|
Path: issues[i].Pos.Filename,
|
||||||
|
Body: templateBuf.String(),
|
||||||
|
NewLineNum: int64(issues[i].Pos.Line),
|
||||||
|
})
|
||||||
|
|
||||||
|
templateBuf.Reset()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, pullReviewOptions)
|
_, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, pullReviewOptions)
|
||||||
|
|
Loading…
Reference in a new issue