Proof of concept #1
6 changed files with 151 additions and 25 deletions
|
@ -27,7 +27,7 @@ steps:
|
||||||
GOMEMLIMIT: "1150MiB"
|
GOMEMLIMIT: "1150MiB"
|
||||||
NITTER_BASE_ADDRESS: https://code.icb4dc0.de
|
NITTER_BASE_ADDRESS: https://code.icb4dc0.de
|
||||||
NITTER_TOKEN:
|
NITTER_TOKEN:
|
||||||
from_secret: gitea_token
|
from_secret: nitter_token
|
||||||
commands:
|
commands:
|
||||||
- go run github.com/magefile/mage -d build -w . lint
|
- go run github.com/magefile/mage -d build -w . lint
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|
|
@ -123,7 +123,7 @@ func runGiteaPRNitting(cmd *cobra.Command, args []string) error {
|
||||||
return nitter.Report(report, issues)
|
return nitter.Report(report, issues)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runGiteaPRLintNitting(cmd *cobra.Command, args []string) error {
|
func runGiteaPRLintNitting(cmd *cobra.Command, _ []string) error {
|
||||||
cfg, err := LoadConfig[gitea.GiteaPRLintConfig](cmd.Flags())
|
cfg, err := LoadConfig[gitea.GiteaPRLintConfig](cmd.Flags())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -133,7 +133,11 @@ func runGiteaPRLintNitting(cmd *cobra.Command, args []string) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
giteaClient, err := giteaSdk.NewClient(cfg.BaseAddress, giteaSdk.SetContext(cmd.Context()), giteaSdk.SetToken(cfg.Token))
|
giteaClient, err := giteaSdk.NewClient(
|
||||||
|
cfg.BaseAddress,
|
||||||
|
giteaSdk.SetContext(cmd.Context()),
|
||||||
|
giteaSdk.SetToken(cfg.Token),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
53
main.go
53
main.go
|
@ -2,21 +2,47 @@ package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"log"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nitter/internal/commands"
|
"code.icb4dc0.de/prskr/nitter/internal/commands"
|
||||||
)
|
)
|
||||||
|
|
||||||
var root = &cobra.Command{
|
var (
|
||||||
|
loggingConfig = struct {
|
||||||
|
LogLevel *slog.LevelVar
|
||||||
|
AddSource bool
|
||||||
|
}{
|
||||||
|
LogLevel: new(slog.LevelVar),
|
||||||
|
}
|
||||||
|
|
||||||
|
root = &cobra.Command{
|
||||||
Use: "nitter",
|
Use: "nitter",
|
||||||
TraverseChildren: true,
|
TraverseChildren: true,
|
||||||
}
|
PersistentPreRun: func(_ *cobra.Command, _ []string) {
|
||||||
|
slogOptions := slog.HandlerOptions{
|
||||||
|
AddSource: loggingConfig.AddSource,
|
||||||
|
Level: loggingConfig.LogLevel.Level(),
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(slogOptions.NewTextHandler(os.Stderr)))
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
slogOptions := slog.HandlerOptions{
|
||||||
|
AddSource: true,
|
||||||
|
Level: slog.LevelInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
slog.SetDefault(slog.New(slogOptions.NewTextHandler(os.Stderr)))
|
||||||
|
|
||||||
|
root.PersistentFlags().AddGoFlagSet(prepareLoggingFlags())
|
||||||
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]")
|
||||||
|
|
||||||
|
@ -26,6 +52,25 @@ func main() {
|
||||||
root.AddCommand(commands.Gitea())
|
root.AddCommand(commands.Gitea())
|
||||||
|
|
||||||
if err := root.ExecuteContext(ctx); err != nil {
|
if err := root.ExecuteContext(ctx); err != nil {
|
||||||
log.Fatal(err)
|
slog.Error("Error occurred during exeuction", err)
|
||||||
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareLoggingFlags() *flag.FlagSet {
|
||||||
|
flagSet := flag.NewFlagSet("logging", flag.PanicOnError)
|
||||||
|
flagSet.TextVar(
|
||||||
|
loggingConfig.LogLevel,
|
||||||
|
"log-level",
|
||||||
|
loggingConfig.LogLevel,
|
||||||
|
"set log level",
|
||||||
|
)
|
||||||
|
flagSet.BoolVar(
|
||||||
|
&loggingConfig.AddSource,
|
||||||
|
"add source line to log messages",
|
||||||
|
false,
|
||||||
|
"Enable to get detailed information where the log was produced",
|
||||||
|
)
|
||||||
|
|
||||||
|
return flagSet
|
||||||
|
}
|
||||||
|
|
19
nitters/gitea/api.go
Normal file
19
nitters/gitea/api.go
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
package gitea
|
||||||
|
|
||||||
|
import "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
|
type PullReviewManager interface {
|
||||||
|
CreatePullReview(owner, repo string, index int64, opt gitea.CreatePullReviewOptions) (*gitea.PullReview, *gitea.Response, error)
|
||||||
|
ListPullReviews(owner, repo string, index int64, opt gitea.ListPullReviewsOptions) ([]*gitea.PullReview, *gitea.Response, error)
|
||||||
|
DeletePullReview(owner, repo string, index, id int64) (*gitea.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type WhoAmIer interface {
|
||||||
|
GetMyUserInfo() (*gitea.User, *gitea.Response, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
//go:generate mockery --name Client --filename gitea_client.mock.go
|
||||||
|
type Client interface {
|
||||||
|
PullReviewManager
|
||||||
|
WhoAmIer
|
||||||
|
}
|
|
@ -3,15 +3,19 @@ package gitea
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
"embed"
|
||||||
|
"strings"
|
||||||
"text/template"
|
"text/template"
|
||||||
|
|
||||||
"code.gitea.io/sdk/gitea"
|
"code.gitea.io/sdk/gitea"
|
||||||
"github.com/golangci/golangci-lint/pkg/report"
|
"github.com/golangci/golangci-lint/pkg/report"
|
||||||
"github.com/golangci/golangci-lint/pkg/result"
|
"github.com/golangci/golangci-lint/pkg/result"
|
||||||
|
"golang.org/x/exp/slog"
|
||||||
|
|
||||||
"code.icb4dc0.de/prskr/nitter/nitters"
|
"code.icb4dc0.de/prskr/nitter/nitters"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const summaryFooter = "\n\n(Created by nitter)"
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ nitters.Nitter = (*prNitter)(nil)
|
_ nitters.Nitter = (*prNitter)(nil)
|
||||||
|
|
||||||
|
@ -28,24 +32,37 @@ func init() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPRNitter(cli PullReviewCreator, cfg *GiteaPRConfig) *prNitter {
|
func NewPRNitter(cli Client, cfg *GiteaPRConfig) *prNitter {
|
||||||
return &prNitter{
|
return &prNitter{
|
||||||
PullReviewCreator: cli,
|
Client: cli,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
//go:generate mockery --name PullReviewCreator --filename pull_review_creator.mock.go
|
|
||||||
type PullReviewCreator interface {
|
|
||||||
CreatePullReview(owner, repo string, index int64, opt gitea.CreatePullReviewOptions) (*gitea.PullReview, *gitea.Response, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
type prNitter struct {
|
type prNitter struct {
|
||||||
PullReviewCreator
|
Client
|
||||||
cfg *GiteaPRConfig
|
cfg *GiteaPRConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
|
review, err := p.preparePullReview(report, issues)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := p.cleanOutdatedReviews(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, *review); err != nil {
|
||||||
|
slog.Error("Failed to submit new PR review", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p prNitter) preparePullReview(report *report.Data, issues []result.Issue) (*gitea.CreatePullReviewOptions, error) {
|
||||||
templateBuf := bytes.Buffer{}
|
templateBuf := bytes.Buffer{}
|
||||||
summaryData := map[string]any{
|
summaryData := map[string]any{
|
||||||
"Report": report,
|
"Report": report,
|
||||||
|
@ -53,10 +70,10 @@ func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := templates.ExecuteTemplate(&templateBuf, "issue_summary.tmpl.md", summaryData); err != nil {
|
if err := templates.ExecuteTemplate(&templateBuf, "issue_summary.tmpl.md", summaryData); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pullReviewOptions := gitea.CreatePullReviewOptions{
|
pullReviewOptions := &gitea.CreatePullReviewOptions{
|
||||||
State: p.cfg.ReviewState,
|
State: p.cfg.ReviewState,
|
||||||
Body: templateBuf.String(),
|
Body: templateBuf.String(),
|
||||||
Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)),
|
Comments: make([]gitea.CreatePullReviewComment, 0, len(issues)),
|
||||||
|
@ -71,7 +88,12 @@ func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
"Issue": issues[i],
|
"Issue": issues[i],
|
||||||
}
|
}
|
||||||
if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil {
|
if err := templates.ExecuteTemplate(&templateBuf, "issue_comment.tmpl.md", templateData); err != nil {
|
||||||
return err
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := templateBuf.WriteString(summaryFooter); err != nil {
|
||||||
|
slog.Error("Failed to append summary footer", err)
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{
|
pullReviewOptions.Comments = append(pullReviewOptions.Comments, gitea.CreatePullReviewComment{
|
||||||
|
@ -84,10 +106,31 @@ func (p prNitter) Report(report *report.Data, issues []result.Issue) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_, _, err := p.CreatePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, pullReviewOptions)
|
return pullReviewOptions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p prNitter) cleanOutdatedReviews() error {
|
||||||
|
me, _, err := p.GetMyUserInfo()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
slog.Debug("Running as", slog.String("name", me.UserName))
|
||||||
|
|
||||||
|
reviews, _, err := p.ListPullReviews(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, gitea.ListPullReviewsOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := range reviews {
|
||||||
|
review := reviews[i]
|
||||||
|
if review.Reviewer.ID == me.ID && strings.HasSuffix(review.Body, summaryFooter) {
|
||||||
|
if _, err = p.DeletePullReview(p.cfg.Namespace, p.cfg.Repo, p.cfg.PRIndex, review.ID); err != nil {
|
||||||
|
slog.Error("Failed to delete existing review", err, slog.Int64("reviewId", review.ID))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,8 @@ import (
|
||||||
"errors"
|
"errors"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
giteasdk "code.gitea.io/sdk/gitea"
|
||||||
|
|
||||||
"github.com/golangci/golangci-lint/pkg/report"
|
"github.com/golangci/golangci-lint/pkg/report"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
|
|
||||||
|
@ -12,15 +14,28 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func Test_PRNitter_Report_MockCreatorError_Error(t *testing.T) {
|
func Test_PRNitter_Report_MockCreatorError_Error(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
expectedError := errors.New("error")
|
expectedError := errors.New("error")
|
||||||
creator := mocks.NewPullReviewCreator(t)
|
client := mocks.NewClient(t)
|
||||||
creator.
|
client.
|
||||||
EXPECT().
|
EXPECT().
|
||||||
CreatePullReview(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
CreatePullReview(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
||||||
Return(nil, nil, expectedError).
|
Return(nil, nil, expectedError).
|
||||||
Times(1)
|
Times(1)
|
||||||
|
|
||||||
p := gitea.NewPRNitter(creator, &gitea.GiteaPRConfig{})
|
client.
|
||||||
|
EXPECT().
|
||||||
|
GetMyUserInfo().
|
||||||
|
Return(&giteasdk.User{ID: 11}, nil, nil).
|
||||||
|
Times(1)
|
||||||
|
|
||||||
|
client.
|
||||||
|
EXPECT().
|
||||||
|
ListPullReviews(mock.Anything, mock.Anything, mock.Anything, mock.Anything).
|
||||||
|
Return(nil, nil, nil).
|
||||||
|
Times(1)
|
||||||
|
|
||||||
|
p := gitea.NewPRNitter(client, &gitea.GiteaPRConfig{})
|
||||||
|
|
||||||
if err := p.Report(new(report.Data), nil); err == nil {
|
if err := p.Report(new(report.Data), nil); err == nil {
|
||||||
t.Error("expected error bot got none")
|
t.Error("expected error bot got none")
|
||||||
|
|
Loading…
Reference in a new issue