diff --git a/cmd/contractor/main.go b/cmd/contractor/main.go
index 5ad8051..792a690 100644
--- a/cmd/contractor/main.go
+++ b/cmd/contractor/main.go
@@ -1,34 +1,17 @@
package contractor
import (
- "bytes"
- "context"
- "encoding/json"
- "errors"
- "fmt"
- "html"
- "io"
"log"
- "net/http"
- "os"
- "strings"
- "sync"
- "time"
- "dagger.io/dagger"
"github.com/gin-gonic/gin"
- "github.com/google/uuid"
"github.com/spf13/cobra"
-)
-type createHook struct {
- Active bool `json:"active"`
- AuthorizationHeader string `json:"authorization_header"`
- BranchFilter string `json:"branch_filter"`
- Config map[string]string `json:"config"`
- Events []string `json:"events"`
- Type string `json:"type"`
-}
+ "git.front.kjuulh.io/kjuulh/contractor/internal/bot"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/features"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/providers"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/queue"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/renovate"
+)
func installCmd() *cobra.Command {
var (
@@ -44,7 +27,7 @@ func installCmd() *cobra.Command {
Use: "install",
Run: func(cmd *cobra.Command, args []string) {
- if err := NewGiteaClient(&url, &token).CreateWebhook(owner, repository); err != nil {
+ if err := providers.NewGiteaClient(&url, &token).CreateWebhook(owner, repository); err != nil {
log.Printf("failed to add create webhook: %s", err.Error())
}
},
@@ -69,77 +52,14 @@ func serverCmd() *cobra.Command {
token string
)
- giteaClient := NewGiteaClient(&url, &token)
- renovateClient := NewRenovateClient("")
- queue := NewGoQueue()
- queue.Subscribe(
- MessageTypeRefreshRepository,
- func(ctx context.Context, item *QueueMessage) error {
- log.Printf("handling message: %s, content: %s", item.Type, item.Content)
- return nil
- },
- )
- queue.Subscribe(
- MessageTypeRefreshRepositoryDone,
- func(ctx context.Context, item *QueueMessage) error {
- log.Printf("handling message: %s, content: %s", item.Type, item.Content)
- return nil
- },
- )
- queue.Subscribe(
- MessageTypeRefreshRepository,
- func(ctx context.Context, item *QueueMessage) error {
- var request RefreshRepositoryRequest
- if err := json.Unmarshal([]byte(item.Content), &request); err != nil {
- log.Printf("failed to unmarshal request body: %s", err.Error())
- return err
- }
+ giteaClient := providers.NewGiteaClient(&url, &token)
+ renovateClient := renovate.NewRenovateClient("")
+ queue := queue.NewGoQueue()
+ botHandler := bot.NewBotHandler(giteaClient)
- cancelCtx, cancel := context.WithTimeout(ctx, time.Second*30)
- defer cancel()
+ giteaWebhook := features.NewGiteaWebhook(botHandler, queue)
- if err := renovateClient.RefreshRepository(cancelCtx, request.Owner, request.Repository); err != nil {
- queue.Insert(MessageTypeRefreshRepositoryDone, RefreshDoneRepositoryRequest{
- Repository: request.Repository,
- Owner: request.Owner,
- PullRequestID: request.PullRequestID,
- CommentID: request.CommentID,
- CommentBody: request.CommentBody,
- ReportProgress: request.ReportProgress,
- Status: "failed",
- Error: err.Error(),
- })
-
- return err
- }
-
- queue.Insert(MessageTypeRefreshRepositoryDone, RefreshDoneRepositoryRequest{
- Repository: request.Repository,
- Owner: request.Owner,
- PullRequestID: request.PullRequestID,
- CommentID: request.CommentID,
- CommentBody: request.CommentBody,
- ReportProgress: request.ReportProgress,
- Status: "done",
- Error: "",
- })
-
- return nil
- },
- )
-
- queue.Subscribe(
- MessageTypeRefreshRepositoryDone,
- func(ctx context.Context, item *QueueMessage) error {
- var doneRequest RefreshDoneRepositoryRequest
- if err := json.Unmarshal([]byte(item.Content), &doneRequest); err != nil {
- log.Printf("failed to unmarshal request body: %s", err.Error())
- return err
- }
-
- return giteaClient.EditComment(ctx, &doneRequest)
- },
- )
+ features.RegisterGiteaQueues(queue, renovateClient, giteaClient)
cmd := &cobra.Command{
Use: "server",
@@ -148,41 +68,15 @@ func serverCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&url, "url", "", "the api url of the server")
cmd.PersistentFlags().StringVar(&token, "token", "", "the token to authenticate with")
- cmd.AddCommand(serverServeCmd(&url, &token, queue, giteaClient))
+ cmd.AddCommand(serverServeCmd(&url, &token, giteaWebhook))
return cmd
}
-const (
- MessageTypeRefreshRepository = "refresh_repository"
- MessageTypeRefreshRepositoryDone = "refresh_repository_done"
-)
-
-type RefreshRepositoryRequest struct {
- Repository string `json:"repository"`
- Owner string `json:"owner"`
- PullRequestID int `json:"pullRequestId"`
- CommentID int `json:"commentId"`
- CommentBody string `json:"commentBody"`
- ReportProgress bool `json:"reportProgress"`
-}
-
-type RefreshDoneRepositoryRequest struct {
- Repository string `json:"repository"`
- Owner string `json:"owner"`
- PullRequestID int `json:"pullRequestId"`
- CommentID int `json:"commentId"`
- CommentBody string `json:"commentBody"`
- ReportProgress bool `json:"reportProgress"`
- Status string `json:"status"`
- Error string `json:"error"`
-}
-
func serverServeCmd(
url *string,
token *string,
- queue *GoQueue,
- giteaClient *GiteaClient,
+ giteaWebhook *features.GiteaWebhook,
) *cobra.Command {
cmd := &cobra.Command{
Use: "serve",
@@ -192,67 +86,19 @@ func serverServeCmd(
gitea := engine.Group("/gitea")
{
gitea.POST("/webhook", func(ctx *gin.Context) {
- log.Println("received")
-
- type GiteaWebhookRequest struct {
- Action string `json:"action"`
- Issue struct {
- Id int `json:"id"`
- Number int `json:"number"`
- } `json:"issue"`
- Comment struct {
- Body string `json:"body"`
- } `json:"comment"`
- Repository struct {
- FullName string `json:"full_name"`
- }
- }
-
- var request GiteaWebhookRequest
+ var request features.GiteaWebhookRequest
if err := ctx.BindJSON(&request); err != nil {
ctx.AbortWithError(500, err)
return
}
- command, ok := validateBotComment(request.Comment.Body)
- if ok {
- log.Printf("got webhook request: contractor %s", command)
-
- bot := NewBotHandler(giteaClient)
- output, err := bot.Handle(command)
- if err != nil {
- log.Printf("failed to run bot handler with error: %s", err.Error())
- }
-
- parts := strings.Split(request.Repository.FullName, "/")
-
- comment, err := bot.AppendComment(
- parts[0],
- parts[1],
- request.Issue.Number,
- output,
- )
- if err != nil {
- ctx.AbortWithError(500, err)
- return
- }
-
- if err := queue.Insert(MessageTypeRefreshRepository, RefreshRepositoryRequest{
- Repository: parts[1],
- Owner: parts[0],
- PullRequestID: request.Issue.Number,
- CommentID: comment.ID,
- CommentBody: comment.Body,
- ReportProgress: true,
- }); err != nil {
- ctx.AbortWithError(500, err)
- return
- }
-
- ctx.Status(204)
-
+ if err := giteaWebhook.HandleGiteaWebhook(ctx.Request.Context(), &request); err != nil {
+ ctx.AbortWithError(500, err)
+ return
}
+
+ ctx.Status(204)
})
}
@@ -263,14 +109,6 @@ func serverServeCmd(
return cmd
}
-func validateBotComment(s string) (request string, ok bool) {
- if after, ok := strings.CutPrefix(s, "/contractor"); ok {
- return strings.TrimSpace(after), true
- }
-
- return "", false
-}
-
func RootCmd() *cobra.Command {
cmd := &cobra.Command{Use: "contractor"}
@@ -278,473 +116,3 @@ func RootCmd() *cobra.Command {
return cmd
}
-
-type BotHandler struct {
- giteaClient *GiteaClient
-}
-
-func NewBotHandler(gitea *GiteaClient) *BotHandler {
- return &BotHandler{giteaClient: gitea}
-}
-
-func (b *BotHandler) Handle(input string) (output string, err error) {
- innerHandle := func(input string) (output string, err error) {
- if strings.HasPrefix(input, "help") {
- return b.Help(), nil
- }
-
- if strings.HasPrefix(input, "refresh") {
- return `
-
Contractor triggered renovate refresh on this repository
-This comment will be updated with status
-
-
-
-`, nil
- }
-
- return b.Help(), errors.New("could not recognize command")
- }
-
- output, err = innerHandle(input)
- output = fmt.Sprintf(
- "%s\nThis comment was generated by Contractor",
- output,
- )
- return output, err
-}
-
-func (b *BotHandler) Help() string {
- return `
- /contractor [command]
-
-Commands:
-
-* /contractor help
- * triggers the help menu
-* /contractor refresh
- * triggers renovate to refresh the current pull request
- `
-}
-
-type AddCommentResponse struct {
- Body string `json:"body"`
- ID int `json:"id"`
-}
-
-func (b *BotHandler) AppendComment(
- owner string,
- repository string,
- pullRequest int,
- comment string,
-) (*AddCommentResponse, error) {
- return b.giteaClient.AddComment(owner, repository, pullRequest, comment)
-}
-
-type QueueMessage struct {
- Type string `json:"type"`
- Content string `json:"content"`
-}
-
-type GoQueue struct {
- queue []*QueueMessage
- queueLock sync.Mutex
- subscribers map[string]map[string]func(ctx context.Context, item *QueueMessage) error
- subscribersLock sync.RWMutex
-}
-
-func NewGoQueue() *GoQueue {
- return &GoQueue{
- queue: make([]*QueueMessage, 0),
- subscribers: make(
- map[string]map[string]func(ctx context.Context, item *QueueMessage) error,
- ),
- }
-}
-
-func (gq *GoQueue) Subscribe(
- messageType string,
- callback func(ctx context.Context, item *QueueMessage) error,
-) string {
- gq.subscribersLock.Lock()
- defer gq.subscribersLock.Unlock()
-
- uid, err := uuid.NewUUID()
- if err != nil {
- panic(err)
- }
- id := uid.String()
-
- _, ok := gq.subscribers[messageType]
- if !ok {
- messageTypeSubscribers := make(
- map[string]func(ctx context.Context, item *QueueMessage) error,
- )
- messageTypeSubscribers[id] = callback
- gq.subscribers[messageType] = messageTypeSubscribers
- } else {
- gq.subscribers[messageType][id] = callback
- }
-
- return id
-}
-
-func (gq *GoQueue) Unsubscribe(messageType string, id string) {
- gq.subscribersLock.Lock()
- defer gq.subscribersLock.Unlock()
- _, ok := gq.subscribers[messageType]
- if !ok {
- // No work to be done
- return
- } else {
- delete(gq.subscribers[messageType], id)
- }
-}
-
-func (gq *GoQueue) Insert(messageType string, content any) error {
- gq.queueLock.Lock()
- defer gq.queueLock.Unlock()
-
- contents, err := json.Marshal(content)
- if err != nil {
- return err
- }
-
- gq.queue = append(gq.queue, &QueueMessage{
- Type: messageType,
- Content: string(contents),
- })
-
- go func() {
- gq.handle(context.Background())
- }()
-
- return nil
-}
-
-func (gq *GoQueue) handle(ctx context.Context) {
- gq.queueLock.Lock()
- defer gq.queueLock.Unlock()
-
- for {
- if len(gq.queue) == 0 {
- return
- }
-
- item := gq.queue[0]
- gq.queue = gq.queue[1:]
-
- gq.subscribersLock.RLock()
- defer gq.subscribersLock.RUnlock()
-
- for id, callback := range gq.subscribers[item.Type] {
- log.Printf("sending message to %s", id)
- go callback(ctx, item)
- }
- }
-}
-
-type GiteaClient struct {
- url *string
- token *string
-
- client *http.Client
-}
-
-func NewGiteaClient(url, token *string) *GiteaClient {
- return &GiteaClient{
- url: url,
- token: token,
- client: http.DefaultClient,
- }
-}
-
-func (gc *GiteaClient) EditComment(
- ctx context.Context,
- doneRequest *RefreshDoneRepositoryRequest,
-) error {
- commentBody := html.UnescapeString(doneRequest.CommentBody)
- startCmnt := ""
- startIdx := strings.Index(commentBody, startCmnt)
- endIdx := strings.Index(commentBody, "")
- if startIdx >= 0 && endIdx >= 0 {
- log.Println("found comment to replace")
-
- var content string
-
- if doneRequest.Error != "" {
- content = fmt.Sprintf("ERROR: %s
", doneRequest.Error)
- }
- if doneRequest.Status != "" {
- content = fmt.Sprintf("%s
", doneRequest.Status)
- }
-
- doneRequest.CommentBody = fmt.Sprintf(
- "%s
%s
%s",
- commentBody[:startIdx+len(startCmnt)],
- content,
- commentBody[endIdx:],
- )
- }
-
- editComment := struct {
- Body string `json:"body"`
- }{
- Body: doneRequest.CommentBody,
- }
-
- body, err := json.Marshal(editComment)
- if err != nil {
- log.Println("failed to marshal request body: %w", err)
- return err
- }
- bodyReader := bytes.NewReader(body)
-
- request, err := http.NewRequest(
- http.MethodPatch,
- fmt.Sprintf(
- "%s/repos/%s/%s/issues/comments/%d",
- strings.TrimSuffix(*gc.url, "/"),
- doneRequest.Owner,
- doneRequest.Repository,
- doneRequest.CommentID,
- ),
- bodyReader,
- )
- if err != nil {
- log.Printf("failed to form update comment request: %s", err.Error())
- return err
- }
- request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
- request.Header.Add("Content-Type", "application/json")
-
- resp, err := gc.client.Do(request)
- if err != nil {
- log.Printf("failed to update comment: %s", err.Error())
- return err
- }
-
- if resp.StatusCode > 299 {
- log.Printf("failed to update comment with status code: %d", resp.StatusCode)
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Printf("failed to read body of error response: %s", err.Error())
- } else {
- log.Printf("request body: %s", string(respBody))
- }
- }
-
- return nil
-}
-
-func (gc *GiteaClient) CreateWebhook(owner, repository string) error {
- createHookOptions := createHook{
- Active: true,
- AuthorizationHeader: "",
- BranchFilter: "*",
- Config: map[string]string{
- "url": "http://10.0.9.1:8080/gitea/webhook",
- "content_type": "json",
- },
- Events: []string{
- "pull_request_comment",
- },
- Type: "gitea",
- }
-
- body, err := json.Marshal(createHookOptions)
- if err != nil {
- log.Println("failed to marshal request body: %w", err)
- return err
- }
- bodyReader := bytes.NewReader(body)
- request, err := http.NewRequest(
- http.MethodPost,
- fmt.Sprintf(
- "%s/repos/%s/%s/hooks",
- strings.TrimSuffix(*gc.url, "/"),
- owner,
- repository,
- ),
- bodyReader,
- )
- if err != nil {
- log.Printf("failed to form create hook request: %s", err.Error())
- return err
- }
- request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
- request.Header.Add("Content-Type", "application/json")
-
- resp, err := gc.client.Do(request)
- if err != nil {
- log.Printf("failed to register hook: %s", err.Error())
- return err
- }
-
- if resp.StatusCode > 299 {
- log.Printf("failed to register with status code: %d", resp.StatusCode)
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- log.Printf("failed to read body of error response: %s", err.Error())
- } else {
- log.Printf("request body: %s", string(respBody))
- }
- }
-
- return nil
-}
-
-func (gc *GiteaClient) AddComment(
- owner, repository string,
- pullRequest int,
- comment string,
-) (*AddCommentResponse, error) {
- addComment := struct {
- Body string `json:"body"`
- }{
- Body: comment,
- }
-
- body, err := json.Marshal(addComment)
- if err != nil {
- return nil, err
- }
- bodyReader := bytes.NewReader(body)
-
- request, err := http.NewRequest(
- http.MethodPost,
- fmt.Sprintf(
- "%s/repos/%s/%s/issues/%d/comments",
- strings.TrimSuffix(*gc.url, "/"),
- owner,
- repository,
- pullRequest,
- ),
- bodyReader,
- )
- if err != nil {
- return nil, err
- }
- request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
- request.Header.Add("Content-Type", "application/json")
-
- resp, err := gc.client.Do(request)
- if err != nil {
- return nil, err
- }
-
- if resp.StatusCode > 299 {
- log.Printf("failed to register with status code: %d", resp.StatusCode)
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- } else {
- log.Printf("request body: %s", string(respBody))
- }
- }
-
- respBody, err := io.ReadAll(resp.Body)
- if err != nil {
- return nil, err
- }
-
- var response AddCommentResponse
- if err := json.Unmarshal(respBody, &response); err != nil {
- return nil, err
- }
-
- return &response, nil
-}
-
-type RenovateClient struct {
- config string
-}
-
-func NewRenovateClient(config string) *RenovateClient {
- return &RenovateClient{config: config}
-}
-
-func (rc *RenovateClient) RefreshRepository(ctx context.Context, owner, repository string) error {
- client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
- if err != nil {
- return err
- }
-
- envRenovateToken := os.Getenv("GITEA_RENOVATE_TOKEN")
- log.Println(envRenovateToken)
-
- renovateToken := client.SetSecret("RENOVATE_TOKEN", envRenovateToken)
- githubComToken := client.SetSecret("GITHUB_COM_TOKEN", os.Getenv("GITHUB_COM_TOKEN"))
- renovateSecret := client.SetSecret("RENOVATE_SECRETS", os.Getenv("RENOVATE_SECRETS"))
-
- output, err := client.Container().
- From("renovate/renovate:latest").
- WithNewFile("/opts/renovate/config.json", dagger.ContainerWithNewFileOpts{
- Contents: `{
- "$schema": "https://docs.renovatebot.com/renovate-schema.json",
- "platform": "gitea",
- "endpoint": "https://git.front.kjuulh.io/api/v1/",
- "automerge": true,
- "automergeType": "pr",
- "extends": [
- "config:base"
- ],
- "hostRules": [
- {
- "hostType": "docker",
- "matchHost": "harbor.front.kjuulh.io",
- "username": "service",
- "password": "{{ secrets.HARBOR_SERVER_PASSWORD }}"
- }
- ],
- "packageRules": [
- {
- "matchDatasources": ["docker"],
- "registryUrls": ["https://harbor.front.kjuulh.io/docker-proxy/library/"]
- },
- {
- "groupName": "all dependencies",
- "separateMajorMinor": false,
- "groupSlug": "all",
- "packageRules": [
- {
- "matchPackagePatterns": [
- "*"
- ],
- "groupName": "all dependencies",
- "groupSlug": "all"
- }
- ],
- "lockFileMaintenance": {
- "enabled": false
- }
- }
- ]
-}`,
- Permissions: 755,
- Owner: "root",
- }).
- WithSecretVariable("RENOVATE_TOKEN", renovateToken).
- WithSecretVariable("GITHUB_COM_TOKEN", githubComToken).
- WithSecretVariable("RENOVATE_SECRETS", renovateSecret).
- WithEnvVariable("LOG_LEVEL", "warn").
- WithEnvVariable("RENOVATE_CONFIG_FILE", "/opts/renovate/config.json").
- WithExec([]string{
- fmt.Sprintf("%s/%s", owner, repository),
- }).
- Sync(ctx)
-
- stdout, outerr := output.Stdout(ctx)
- if outerr == nil {
- log.Printf("stdout: %s", stdout)
- }
- stderr, outerr := output.Stderr(ctx)
- if outerr == nil {
- log.Printf("stderr: %s", stderr)
- }
- if err != nil {
- return fmt.Errorf("error: %w, \nstderr: %s\nstdout: %s", err, stderr, stdout)
- }
-
- return nil
-}
diff --git a/internal/bot/giteabot.go b/internal/bot/giteabot.go
new file mode 100644
index 0000000..27ab20f
--- /dev/null
+++ b/internal/bot/giteabot.go
@@ -0,0 +1,67 @@
+package bot
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "git.front.kjuulh.io/kjuulh/contractor/internal/models"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/providers"
+)
+
+type BotHandler struct {
+ giteaClient *providers.GiteaClient
+}
+
+func NewBotHandler(gitea *providers.GiteaClient) *BotHandler {
+ return &BotHandler{giteaClient: gitea}
+}
+
+func (b *BotHandler) Handle(input string) (output string, err error) {
+ innerHandle := func(input string) (output string, err error) {
+ if strings.HasPrefix(input, "help") {
+ return b.Help(), nil
+ }
+
+ if strings.HasPrefix(input, "refresh") {
+ return `
+Contractor triggered renovate refresh on this repository
+This comment will be updated with status
+
+
+
+`, nil
+ }
+
+ return b.Help(), errors.New("could not recognize command")
+ }
+
+ output, err = innerHandle(input)
+ output = fmt.Sprintf(
+ "%s\nThis comment was generated by Contractor",
+ output,
+ )
+ return output, err
+}
+
+func (b *BotHandler) Help() string {
+ return `
+ /contractor [command]
+
+Commands:
+
+* /contractor help
+ * triggers the help menu
+* /contractor refresh
+ * triggers renovate to refresh the current pull request
+ `
+}
+
+func (b *BotHandler) AppendComment(
+ owner string,
+ repository string,
+ pullRequest int,
+ comment string,
+) (*models.AddCommentResponse, error) {
+ return b.giteaClient.AddComment(owner, repository, pullRequest, comment)
+}
diff --git a/internal/features/handle_gitea_events.go b/internal/features/handle_gitea_events.go
new file mode 100644
index 0000000..1c312bf
--- /dev/null
+++ b/internal/features/handle_gitea_events.go
@@ -0,0 +1,84 @@
+package features
+
+import (
+ "context"
+ "encoding/json"
+ "log"
+ "time"
+
+ "git.front.kjuulh.io/kjuulh/contractor/internal/models"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/providers"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/queue"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/renovate"
+)
+
+func RegisterGiteaQueues(goqueue *queue.GoQueue, renovate *renovate.RenovateClient, giteaClient *providers.GiteaClient) {
+ goqueue.Subscribe(
+ models.MessageTypeRefreshRepository,
+ func(ctx context.Context, item *queue.QueueMessage) error {
+ log.Printf("handling message: %s, content: %s", item.Type, item.Content)
+ return nil
+ },
+ )
+ goqueue.Subscribe(
+ models.MessageTypeRefreshRepositoryDone,
+ func(ctx context.Context, item *queue.QueueMessage) error {
+ log.Printf("handling message: %s, content: %s", item.Type, item.Content)
+ return nil
+ },
+ )
+ goqueue.Subscribe(
+ models.MessageTypeRefreshRepository,
+ func(ctx context.Context, item *queue.QueueMessage) error {
+ var request models.RefreshRepositoryRequest
+ if err := json.Unmarshal([]byte(item.Content), &request); err != nil {
+ log.Printf("failed to unmarshal request body: %s", err.Error())
+ return err
+ }
+
+ cancelCtx, cancel := context.WithTimeout(ctx, time.Minute*5)
+ defer cancel()
+
+ if err := renovate.RefreshRepository(cancelCtx, request.Owner, request.Repository); err != nil {
+ goqueue.Insert(models.MessageTypeRefreshRepositoryDone, models.RefreshDoneRepositoryRequest{
+ Repository: request.Repository,
+ Owner: request.Owner,
+ PullRequestID: request.PullRequestID,
+ CommentID: request.CommentID,
+ CommentBody: request.CommentBody,
+ ReportProgress: request.ReportProgress,
+ Status: "failed",
+ Error: err.Error(),
+ })
+
+ return err
+ }
+
+ goqueue.Insert(models.MessageTypeRefreshRepositoryDone, models.RefreshDoneRepositoryRequest{
+ Repository: request.Repository,
+ Owner: request.Owner,
+ PullRequestID: request.PullRequestID,
+ CommentID: request.CommentID,
+ CommentBody: request.CommentBody,
+ ReportProgress: request.ReportProgress,
+ Status: "done",
+ Error: "",
+ })
+
+ return nil
+ },
+ )
+
+ goqueue.Subscribe(
+ models.MessageTypeRefreshRepositoryDone,
+ func(ctx context.Context, item *queue.QueueMessage) error {
+ var doneRequest models.RefreshDoneRepositoryRequest
+ if err := json.Unmarshal([]byte(item.Content), &doneRequest); err != nil {
+ log.Printf("failed to unmarshal request body: %s", err.Error())
+ return err
+ }
+
+ return giteaClient.EditComment(ctx, &doneRequest)
+ },
+ )
+}
diff --git a/internal/features/handle_gitea_webhook.go b/internal/features/handle_gitea_webhook.go
new file mode 100644
index 0000000..d7a7490
--- /dev/null
+++ b/internal/features/handle_gitea_webhook.go
@@ -0,0 +1,83 @@
+package features
+
+import (
+ "context"
+ "log"
+ "strings"
+
+ "git.front.kjuulh.io/kjuulh/contractor/internal/bot"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/models"
+ "git.front.kjuulh.io/kjuulh/contractor/internal/queue"
+)
+
+type GiteaWebhook struct {
+ botHandler *bot.BotHandler
+ queue *queue.GoQueue
+}
+
+type GiteaWebhookRequest struct {
+ Action string `json:"action"`
+ Issue struct {
+ Id int `json:"id"`
+ Number int `json:"number"`
+ } `json:"issue"`
+ Comment struct {
+ Body string `json:"body"`
+ } `json:"comment"`
+ Repository struct {
+ FullName string `json:"full_name"`
+ }
+}
+
+func NewGiteaWebhook(botHandler *bot.BotHandler, queue *queue.GoQueue) *GiteaWebhook {
+ return &GiteaWebhook{
+ botHandler: botHandler,
+ queue: queue,
+ }
+}
+
+func (gw *GiteaWebhook) HandleGiteaWebhook(ctx context.Context, request *GiteaWebhookRequest) error {
+ command, ok := validateBotComment(request.Comment.Body)
+ if ok {
+ log.Printf("got webhook request: contractor %s", command)
+
+ bot := gw.botHandler
+ output, err := bot.Handle(command)
+ if err != nil {
+ log.Printf("failed to run bot handler with error: %s", err.Error())
+ }
+
+ parts := strings.Split(request.Repository.FullName, "/")
+
+ comment, err := bot.AppendComment(
+ parts[0],
+ parts[1],
+ request.Issue.Number,
+ output,
+ )
+ if err != nil {
+ return err
+ }
+
+ if err := gw.queue.Insert(models.MessageTypeRefreshRepository, models.RefreshRepositoryRequest{
+ Repository: parts[1],
+ Owner: parts[0],
+ PullRequestID: request.Issue.Number,
+ CommentID: comment.ID,
+ CommentBody: comment.Body,
+ ReportProgress: true,
+ }); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func validateBotComment(s string) (request string, ok bool) {
+ if after, ok := strings.CutPrefix(s, "/contractor"); ok {
+ return strings.TrimSpace(after), true
+ }
+
+ return "", false
+}
diff --git a/internal/models/requests.go b/internal/models/requests.go
new file mode 100644
index 0000000..392aed0
--- /dev/null
+++ b/internal/models/requests.go
@@ -0,0 +1,40 @@
+package models
+
+const (
+ MessageTypeRefreshRepository = "refresh_repository"
+ MessageTypeRefreshRepositoryDone = "refresh_repository_done"
+)
+
+type CreateHook struct {
+ Active bool `json:"active"`
+ AuthorizationHeader string `json:"authorization_header"`
+ BranchFilter string `json:"branch_filter"`
+ Config map[string]string `json:"config"`
+ Events []string `json:"events"`
+ Type string `json:"type"`
+}
+
+type RefreshRepositoryRequest struct {
+ Repository string `json:"repository"`
+ Owner string `json:"owner"`
+ PullRequestID int `json:"pullRequestId"`
+ CommentID int `json:"commentId"`
+ CommentBody string `json:"commentBody"`
+ ReportProgress bool `json:"reportProgress"`
+}
+
+type RefreshDoneRepositoryRequest struct {
+ Repository string `json:"repository"`
+ Owner string `json:"owner"`
+ PullRequestID int `json:"pullRequestId"`
+ CommentID int `json:"commentId"`
+ CommentBody string `json:"commentBody"`
+ ReportProgress bool `json:"reportProgress"`
+ Status string `json:"status"`
+ Error string `json:"error"`
+}
+
+type AddCommentResponse struct {
+ Body string `json:"body"`
+ ID int `json:"id"`
+}
diff --git a/internal/providers/gitea.go b/internal/providers/gitea.go
new file mode 100644
index 0000000..2851198
--- /dev/null
+++ b/internal/providers/gitea.go
@@ -0,0 +1,227 @@
+package providers
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "html"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+
+ "git.front.kjuulh.io/kjuulh/contractor/internal/models"
+)
+
+type GiteaClient struct {
+ url *string
+ token *string
+
+ client *http.Client
+}
+
+func NewGiteaClient(url, token *string) *GiteaClient {
+ return &GiteaClient{
+ url: url,
+ token: token,
+ client: http.DefaultClient,
+ }
+}
+
+func (gc *GiteaClient) EditComment(
+ ctx context.Context,
+ doneRequest *models.RefreshDoneRepositoryRequest,
+) error {
+ commentBody := html.UnescapeString(doneRequest.CommentBody)
+ startCmnt := ""
+ startIdx := strings.Index(commentBody, startCmnt)
+ endIdx := strings.Index(commentBody, "")
+ if startIdx >= 0 && endIdx >= 0 {
+ log.Println("found comment to replace")
+
+ var content string
+
+ if doneRequest.Error != "" {
+ content = fmt.Sprintf("ERROR: %s
", doneRequest.Error)
+ }
+ if doneRequest.Status != "" {
+ content = fmt.Sprintf("%s%s
", content, doneRequest.Status)
+ }
+
+ doneRequest.CommentBody = fmt.Sprintf(
+ "%s
%s
%s",
+ commentBody[:startIdx+len(startCmnt)],
+ content,
+ commentBody[endIdx:],
+ )
+ }
+
+ editComment := struct {
+ Body string `json:"body"`
+ }{
+ Body: doneRequest.CommentBody,
+ }
+
+ body, err := json.Marshal(editComment)
+ if err != nil {
+ log.Println("failed to marshal request body: %w", err)
+ return err
+ }
+ bodyReader := bytes.NewReader(body)
+
+ request, err := http.NewRequest(
+ http.MethodPatch,
+ fmt.Sprintf(
+ "%s/repos/%s/%s/issues/comments/%d",
+ strings.TrimSuffix(*gc.url, "/"),
+ doneRequest.Owner,
+ doneRequest.Repository,
+ doneRequest.CommentID,
+ ),
+ bodyReader,
+ )
+ if err != nil {
+ log.Printf("failed to form update comment request: %s", err.Error())
+ return err
+ }
+ request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
+ request.Header.Add("Content-Type", "application/json")
+
+ resp, err := gc.client.Do(request)
+ if err != nil {
+ log.Printf("failed to update comment: %s", err.Error())
+ return err
+ }
+
+ if resp.StatusCode > 299 {
+ log.Printf("failed to update comment with status code: %d", resp.StatusCode)
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Printf("failed to read body of error response: %s", err.Error())
+ } else {
+ log.Printf("request body: %s", string(respBody))
+ }
+ }
+
+ return nil
+}
+
+func (gc *GiteaClient) CreateWebhook(owner, repository string) error {
+ createHookOptions := models.CreateHook{
+ Active: true,
+ AuthorizationHeader: "",
+ BranchFilter: "*",
+ Config: map[string]string{
+ "url": "http://10.0.9.1:8080/gitea/webhook",
+ "content_type": "json",
+ },
+ Events: []string{
+ "pull_request_comment",
+ },
+ Type: "gitea",
+ }
+
+ body, err := json.Marshal(createHookOptions)
+ if err != nil {
+ log.Println("failed to marshal request body: %w", err)
+ return err
+ }
+ bodyReader := bytes.NewReader(body)
+ request, err := http.NewRequest(
+ http.MethodPost,
+ fmt.Sprintf(
+ "%s/repos/%s/%s/hooks",
+ strings.TrimSuffix(*gc.url, "/"),
+ owner,
+ repository,
+ ),
+ bodyReader,
+ )
+ if err != nil {
+ log.Printf("failed to form create hook request: %s", err.Error())
+ return err
+ }
+ request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
+ request.Header.Add("Content-Type", "application/json")
+
+ resp, err := gc.client.Do(request)
+ if err != nil {
+ log.Printf("failed to register hook: %s", err.Error())
+ return err
+ }
+
+ if resp.StatusCode > 299 {
+ log.Printf("failed to register with status code: %d", resp.StatusCode)
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ log.Printf("failed to read body of error response: %s", err.Error())
+ } else {
+ log.Printf("request body: %s", string(respBody))
+ }
+ }
+
+ return nil
+}
+
+func (gc *GiteaClient) AddComment(
+ owner, repository string,
+ pullRequest int,
+ comment string,
+) (*models.AddCommentResponse, error) {
+ addComment := struct {
+ Body string `json:"body"`
+ }{
+ Body: comment,
+ }
+
+ body, err := json.Marshal(addComment)
+ if err != nil {
+ return nil, err
+ }
+ bodyReader := bytes.NewReader(body)
+
+ request, err := http.NewRequest(
+ http.MethodPost,
+ fmt.Sprintf(
+ "%s/repos/%s/%s/issues/%d/comments",
+ strings.TrimSuffix(*gc.url, "/"),
+ owner,
+ repository,
+ pullRequest,
+ ),
+ bodyReader,
+ )
+ if err != nil {
+ return nil, err
+ }
+ request.Header.Add("Authorization", fmt.Sprintf("token %s", *gc.token))
+ request.Header.Add("Content-Type", "application/json")
+
+ resp, err := gc.client.Do(request)
+ if err != nil {
+ return nil, err
+ }
+
+ if resp.StatusCode > 299 {
+ log.Printf("failed to register with status code: %d", resp.StatusCode)
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ } else {
+ log.Printf("request body: %s", string(respBody))
+ }
+ }
+
+ respBody, err := io.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ var response models.AddCommentResponse
+ if err := json.Unmarshal(respBody, &response); err != nil {
+ return nil, err
+ }
+
+ return &response, nil
+}
diff --git a/internal/queue/goqueue.go b/internal/queue/goqueue.go
new file mode 100644
index 0000000..13b863c
--- /dev/null
+++ b/internal/queue/goqueue.go
@@ -0,0 +1,113 @@
+package queue
+
+import (
+ "context"
+ "encoding/json"
+ "log"
+ "sync"
+
+ "github.com/google/uuid"
+)
+
+type QueueMessage struct {
+ Type string `json:"type"`
+ Content string `json:"content"`
+}
+
+type GoQueue struct {
+ queue []*QueueMessage
+ queueLock sync.Mutex
+ subscribers map[string]map[string]func(ctx context.Context, item *QueueMessage) error
+ subscribersLock sync.RWMutex
+}
+
+func NewGoQueue() *GoQueue {
+ return &GoQueue{
+ queue: make([]*QueueMessage, 0),
+ subscribers: make(
+ map[string]map[string]func(ctx context.Context, item *QueueMessage) error,
+ ),
+ }
+}
+
+func (gq *GoQueue) Subscribe(
+ messageType string,
+ callback func(ctx context.Context, item *QueueMessage) error,
+) string {
+ gq.subscribersLock.Lock()
+ defer gq.subscribersLock.Unlock()
+
+ uid, err := uuid.NewUUID()
+ if err != nil {
+ panic(err)
+ }
+ id := uid.String()
+
+ _, ok := gq.subscribers[messageType]
+ if !ok {
+ messageTypeSubscribers := make(
+ map[string]func(ctx context.Context, item *QueueMessage) error,
+ )
+ messageTypeSubscribers[id] = callback
+ gq.subscribers[messageType] = messageTypeSubscribers
+ } else {
+ gq.subscribers[messageType][id] = callback
+ }
+
+ return id
+}
+
+func (gq *GoQueue) Unsubscribe(messageType string, id string) {
+ gq.subscribersLock.Lock()
+ defer gq.subscribersLock.Unlock()
+ _, ok := gq.subscribers[messageType]
+ if !ok {
+ // No work to be done
+ return
+ } else {
+ delete(gq.subscribers[messageType], id)
+ }
+}
+
+func (gq *GoQueue) Insert(messageType string, content any) error {
+ gq.queueLock.Lock()
+ defer gq.queueLock.Unlock()
+
+ contents, err := json.Marshal(content)
+ if err != nil {
+ return err
+ }
+
+ gq.queue = append(gq.queue, &QueueMessage{
+ Type: messageType,
+ Content: string(contents),
+ })
+
+ go func() {
+ gq.handle(context.Background())
+ }()
+
+ return nil
+}
+
+func (gq *GoQueue) handle(ctx context.Context) {
+ gq.queueLock.Lock()
+ defer gq.queueLock.Unlock()
+
+ for {
+ if len(gq.queue) == 0 {
+ return
+ }
+
+ item := gq.queue[0]
+ gq.queue = gq.queue[1:]
+
+ gq.subscribersLock.RLock()
+ defer gq.subscribersLock.RUnlock()
+
+ for id, callback := range gq.subscribers[item.Type] {
+ log.Printf("sending message to %s", id)
+ go callback(ctx, item)
+ }
+ }
+}
diff --git a/internal/renovate/client.go b/internal/renovate/client.go
new file mode 100644
index 0000000..f0db6bd
--- /dev/null
+++ b/internal/renovate/client.go
@@ -0,0 +1,103 @@
+package renovate
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+
+ "dagger.io/dagger"
+)
+
+type RenovateClient struct {
+ config string
+}
+
+func NewRenovateClient(config string) *RenovateClient {
+ return &RenovateClient{config: config}
+}
+
+func (rc *RenovateClient) RefreshRepository(ctx context.Context, owner, repository string) error {
+ client, err := dagger.Connect(ctx, dagger.WithLogOutput(os.Stdout))
+ if err != nil {
+ return err
+ }
+
+ envRenovateToken := os.Getenv("GITEA_RENOVATE_TOKEN")
+ log.Println(envRenovateToken)
+
+ renovateToken := client.SetSecret("RENOVATE_TOKEN", envRenovateToken)
+ githubComToken := client.SetSecret("GITHUB_COM_TOKEN", os.Getenv("GITHUB_COM_TOKEN"))
+ renovateSecret := client.SetSecret("RENOVATE_SECRETS", os.Getenv("RENOVATE_SECRETS"))
+
+ output, err := client.Container().
+ From("renovate/renovate:latest").
+ WithNewFile("/opts/renovate/config.json", dagger.ContainerWithNewFileOpts{
+ Contents: `{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "platform": "gitea",
+ "endpoint": "https://git.front.kjuulh.io/api/v1/",
+ "automerge": true,
+ "automergeType": "pr",
+ "extends": [
+ "config:base"
+ ],
+ "hostRules": [
+ {
+ "hostType": "docker",
+ "matchHost": "harbor.front.kjuulh.io",
+ "username": "service",
+ "password": "{{ secrets.HARBOR_SERVER_PASSWORD }}"
+ }
+ ],
+ "packageRules": [
+ {
+ "matchDatasources": ["docker"],
+ "registryUrls": ["https://harbor.front.kjuulh.io/docker-proxy/library/"]
+ },
+ {
+ "groupName": "all dependencies",
+ "separateMajorMinor": false,
+ "groupSlug": "all",
+ "packageRules": [
+ {
+ "matchPackagePatterns": [
+ "*"
+ ],
+ "groupName": "all dependencies",
+ "groupSlug": "all"
+ }
+ ],
+ "lockFileMaintenance": {
+ "enabled": false
+ }
+ }
+ ]
+}`,
+ Permissions: 755,
+ Owner: "root",
+ }).
+ WithSecretVariable("RENOVATE_TOKEN", renovateToken).
+ WithSecretVariable("GITHUB_COM_TOKEN", githubComToken).
+ WithSecretVariable("RENOVATE_SECRETS", renovateSecret).
+ WithEnvVariable("LOG_LEVEL", "warn").
+ WithEnvVariable("RENOVATE_CONFIG_FILE", "/opts/renovate/config.json").
+ WithExec([]string{
+ fmt.Sprintf("%s/%s", owner, repository),
+ }).
+ Sync(ctx)
+
+ stdout, outerr := output.Stdout(ctx)
+ if outerr == nil {
+ log.Printf("stdout: %s", stdout)
+ }
+ stderr, outerr := output.Stderr(ctx)
+ if outerr == nil {
+ log.Printf("stderr: %s", stderr)
+ }
+ if err != nil {
+ return fmt.Errorf("error: %w, \nstderr: %s\nstdout: %s", err, stderr, stdout)
+ }
+
+ return nil
+}