Proof of concept #1

Merged
prskr merged 8 commits from feature/proof-of-concept into main 2023-03-10 10:17:07 +00:00
6 changed files with 151 additions and 25 deletions
Showing only changes of commit d51a05fea1 - Show all commits

View file

@ -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:

View file

@ -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
} }

55
main.go
View file

@ -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 (
Use: "nitter", loggingConfig = struct {
TraverseChildren: true, LogLevel *slog.LevelVar
AddSource bool
}{
LogLevel: new(slog.LevelVar),
} }
root = &cobra.Command{
Use: "nitter",
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
View 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
}

View file

@ -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
} }

View file

@ -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")