20 Commits

Author SHA1 Message Date
cuddle-please
0711cf4afa chore(release): 0.2.1
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
2024-04-01 20:16:38 +00:00
561bcf0845 fix(deps): update all dependencies to v61
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-04-01 19:29:42 +00:00
cc6c71b50f fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-03-21 15:58:50 +00:00
a18951e1ad fix(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2024-01-23 19:49:08 +00:00
f59d4aca3c fix(deps): update all dependencies to v58
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-10 03:43:07 +00:00
e27d489737 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2024-01-05 16:10:19 +00:00
ed2012af25 fix(deps): update rust crate futures to 0.3.30
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-24 14:57:33 +00:00
4c5017f8d3 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-12 17:26:52 +00:00
c7fffdbb44 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-12-01 18:01:41 +00:00
aaf3bf307f fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-11-04 21:34:10 +00:00
8a3155f3e0 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 16:10:26 +00:00
7c969f1bd6 fix(deps): update rust crate futures to 0.3.29
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-26 15:01:04 +00:00
ad17071744 fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-17 02:58:28 +00:00
b787bf7058 fix(deps): update all dependencies
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
2023-10-12 00:40:05 +00:00
3a6020813a fix(deps): update all dependencies
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
2023-10-12 00:09:06 +00:00
cuddle-please
647306ea55 chore(release): 0.2.0
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/tag Build is failing
continuous-integration/drone/push Build is passing
2023-08-09 13:49:15 +00:00
8f924e5ddd docs(README): update with github support
All checks were successful
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-09 15:46:52 +02:00
89c1c72d87 feat(github): add github support
All checks were successful
continuous-integration/drone/push Build is passing
continuous-integration/drone/pr Build is passing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-09 15:44:31 +02:00
65fba21285 refactor(app): split the main command file into multiples
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is failing
Signed-off-by: kjuulh <contact@kjuulh.io>
2023-08-08 19:41:19 +02:00
cuddle-please
d721b74d05 chore(release): 0.1.1
Some checks failed
continuous-integration/drone/pr Build is passing
continuous-integration/drone/push Build is passing
continuous-integration/drone/tag Build is failing
2023-08-08 14:22:50 +00:00
17 changed files with 1280 additions and 692 deletions

View File

@@ -6,6 +6,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [Unreleased]
## [0.2.1] - 2024-04-01
### Fixed
- *(deps)* update all dependencies to v61
- *(deps)* update all dependencies
- *(deps)* update all dependencies
- *(deps)* update all dependencies to v58
- *(deps)* update all dependencies
- *(deps)* update rust crate futures to 0.3.30
- *(deps)* update all dependencies
- *(deps)* update all dependencies
- *(deps)* update all dependencies
- *(deps)* update all dependencies
- *(deps)* update rust crate futures to 0.3.29
- *(deps)* update all dependencies
- *(deps)* update all dependencies
- *(deps)* update all dependencies
## [0.2.0] - 2023-08-09
### Added
- *(github)* add github support
### Docs
- *(README)* update with github support
### Other
- *(app)* split the main command file into multiples
## [0.1.1] - 2023-08-08
### Docs
- *(README)* add motivation why this project should exist
### Other
- *(cuddle-please)* update
## [0.1.0] - 2023-08-08 ## [0.1.0] - 2023-08-08
### Added ### Added

View File

@@ -41,15 +41,13 @@ what the project will look like once feature-complete.
- Includes basic setup such as working server bot, and installation command, - Includes basic setup such as working server bot, and installation command,
automation is missing however. Also only gitea support for now, because this automation is missing however. Also only gitea support for now, because this
is where the project initially is supposed to be in use. is where the project initially is supposed to be in use.
- [ ] 0.2.0 - [x] 0.2.0
- Add GitHub support - Add GitHub support, only github app support for now. This means that install is not needed, because a github app will automatically receive webhooks if setup properly. docs are missing for this (tbd).
- [ ] 0.3.0 - [ ] 0.3.0
- Add Delegation support (not clustering, just delegation of renovate jobs) - Add Delegation support (not clustering, just delegation of renovate jobs)
- [ ] 0.4.0 - [ ] 0.4.0
- Slack integration - Slack integration
- [ ] 0.5.0 - [ ] 0.5.0
- GitHub App and such support
- [ ] 0.6.0
- Add api key support - Add api key support
## Getting started ## Getting started

41
ci/Cargo.lock generated
View File

@@ -74,13 +74,12 @@ checksum = "eab1c04a571841102f5345a8fc0f6bb3d31c315dec879b5c6e42e40ce7ffa34e"
[[package]] [[package]]
name = "async-scoped" name = "async-scoped"
version = "0.7.1" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7a6a57c8aeb40da1ec037f5d455836852f7a57e69e1b1ad3d8f38ac1d6cadf" checksum = "4042078ea593edffc452eef14e99fdb2b120caa4ad9618bcdeabc4a023b98740"
dependencies = [ dependencies = [
"futures", "futures",
"pin-project", "pin-project",
"slab",
"tokio", "tokio",
] ]
@@ -541,9 +540,9 @@ dependencies = [
[[package]] [[package]]
name = "futures" name = "futures"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23342abe12aba583913b2e62f22225ff9c950774065e4bfb61a19cd9770fec40" checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",
@@ -556,9 +555,9 @@ dependencies = [
[[package]] [[package]]
name = "futures-channel" name = "futures-channel"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "955518d47e09b25bbebc7a18df10b81f0c766eaf4c4f1cccef2fca5f2a4fb5f2" checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-sink", "futures-sink",
@@ -566,15 +565,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bca583b7e26f571124fe5b7561d49cb2868d79116cfa0eefce955557c6fee8c" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccecee823288125bd88b4d7f565c9e58e41858e47ab72e8ea2d64e93624386e0" checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -583,15 +582,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-io" name = "futures-io"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]] [[package]]
name = "futures-macro" name = "futures-macro"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -600,21 +599,21 @@ dependencies = [
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f43be4fe21a13b9781a69afa4985b0f6ee0e1afab2c6f454a8cf30e2b2237b6e" checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.28" version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26b01e40b772d54cf6c6d721c1d1abd0647a0106a12ecaa1c186273392a69533" checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-channel", "futures-channel",
"futures-core", "futures-core",

View File

@@ -11,6 +11,6 @@ eyre = "*"
color-eyre = "*" color-eyre = "*"
tokio = "1" tokio = "1"
clap = {version = "4", features = ["derive"]} clap = {version = "4", features = ["derive"]}
futures = "0.3.28" futures = "0.3.30"
async-scoped = { version = "0.7.1", features = ["tokio", "use-tokio"] } async-scoped = { version = "0.9.0", features = ["tokio", "use-tokio"] }
dotenv = "*" dotenv = "*"

View File

@@ -1,34 +1,17 @@
package contractor package contractor
import ( import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"html"
"io"
"log" "log"
"net/http"
"os"
"strings"
"sync"
"time"
"dagger.io/dagger"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/google/uuid"
"github.com/spf13/cobra" "github.com/spf13/cobra"
)
type createHook struct { "git.front.kjuulh.io/kjuulh/contractor/internal/bot"
Active bool `json:"active"` "git.front.kjuulh.io/kjuulh/contractor/internal/features"
AuthorizationHeader string `json:"authorization_header"` "git.front.kjuulh.io/kjuulh/contractor/internal/providers"
BranchFilter string `json:"branch_filter"` "git.front.kjuulh.io/kjuulh/contractor/internal/queue"
Config map[string]string `json:"config"` "git.front.kjuulh.io/kjuulh/contractor/internal/renovate"
Events []string `json:"events"` )
Type string `json:"type"`
}
func installCmd() *cobra.Command { func installCmd() *cobra.Command {
var ( var (
@@ -44,7 +27,7 @@ func installCmd() *cobra.Command {
Use: "install", Use: "install",
Run: func(cmd *cobra.Command, args []string) { 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()) log.Printf("failed to add create webhook: %s", err.Error())
} }
}, },
@@ -67,79 +50,23 @@ func serverCmd() *cobra.Command {
var ( var (
url string url string
token string token string
githubAppID int64
githubInstallationID int64
githubPrivateKeyPath string
) )
giteaClient := NewGiteaClient(&url, &token) giteaClient := providers.NewGiteaClient(&url, &token)
renovateClient := NewRenovateClient("") githubClient := providers.NewGitHubClient(&githubAppID, &githubInstallationID, &githubPrivateKeyPath)
queue := NewGoQueue() renovateClient := renovate.NewRenovateClient("")
queue.Subscribe( queue := queue.NewGoQueue()
MessageTypeRefreshRepository, botHandler := bot.NewBotHandler(giteaClient, githubClient)
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
}
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*30) giteaWebhook := features.NewGiteaWebhook(botHandler, queue)
defer cancel() githubWebhook := features.NewGitHubWebhook(botHandler, queue)
if err := renovateClient.RefreshRepository(cancelCtx, request.Owner, request.Repository); err != nil { features.RegisterGiteaQueues(queue, renovateClient, giteaClient)
queue.Insert(MessageTypeRefreshRepositoryDone, RefreshDoneRepositoryRequest{ features.RegisterGitHubQueues(queue, renovateClient, githubClient)
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)
},
)
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "server", Use: "server",
@@ -148,129 +75,69 @@ func serverCmd() *cobra.Command {
cmd.PersistentFlags().StringVar(&url, "url", "", "the api url of the server") cmd.PersistentFlags().StringVar(&url, "url", "", "the api url of the server")
cmd.PersistentFlags().StringVar(&token, "token", "", "the token to authenticate with") cmd.PersistentFlags().StringVar(&token, "token", "", "the token to authenticate with")
cmd.AddCommand(serverServeCmd(&url, &token, queue, giteaClient)) cmd.PersistentFlags().Int64Var(&githubAppID, "github-app-id", 0, "github app id to authenticate with")
cmd.PersistentFlags().Int64Var(&githubInstallationID, "github-installation-id", 0, "github installation id to authenticate with")
cmd.PersistentFlags().StringVar(&githubPrivateKeyPath, "github-private-key-path", "", "path to the github app private key")
cmd.AddCommand(serverServeCmd(&url, &token, giteaWebhook, githubWebhook))
return cmd 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( func serverServeCmd(
url *string, url *string,
token *string, token *string,
queue *GoQueue, giteaWebhook *features.GiteaWebhook,
giteaClient *GiteaClient, githubWebhook *features.GitHubWebhook,
) *cobra.Command { ) *cobra.Command {
cmd := &cobra.Command{ cmd := &cobra.Command{
Use: "serve", Use: "serve",
Run: func(cmd *cobra.Command, args []string) { Run: func(cmd *cobra.Command, args []string) {
engine := gin.Default() engine := gin.Default()
gitea := engine.Group("/gitea") github := engine.Group("/github")
{ {
gitea.POST("/webhook", func(ctx *gin.Context) { github.POST("/webhook", func(ctx *gin.Context) {
log.Println("received") var request features.GitHubWebhookRequest
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
if err := ctx.BindJSON(&request); err != nil { if err := ctx.BindJSON(&request); err != nil {
ctx.AbortWithError(500, err) ctx.AbortWithError(500, err)
return return
} }
command, ok := validateBotComment(request.Comment.Body) if err := githubWebhook.HandleGitHubWebhook(ctx.Request.Context(), &request); err != nil {
if ok { ctx.AbortWithError(500, err)
log.Printf("got webhook request: contractor %s", command) return
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)
} }
ctx.Status(204)
}) })
} }
engine.Run("0.0.0.0:8080") gitea := engine.Group("/gitea")
{
gitea.POST("/webhook", func(ctx *gin.Context) {
var request features.GiteaWebhookRequest
if err := ctx.BindJSON(&request); err != nil {
ctx.AbortWithError(500, err)
return
}
if err := giteaWebhook.HandleGiteaWebhook(ctx.Request.Context(), &request); err != nil {
ctx.AbortWithError(500, err)
return
}
ctx.Status(204)
})
}
engine.Run("0.0.0.0:9111")
}, },
} }
return cmd 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 { func RootCmd() *cobra.Command {
cmd := &cobra.Command{Use: "contractor"} cmd := &cobra.Command{Use: "contractor"}
@@ -278,473 +145,3 @@ func RootCmd() *cobra.Command {
return cmd 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 `
<h3>Contractor triggered renovate refresh on this repository</h3>
This comment will be updated with status
<!-- Status update start -->
<!-- Status update end -->
`, nil
}
return b.Help(), errors.New("could not recognize command")
}
output, err = innerHandle(input)
output = fmt.Sprintf(
"%s\n<small>This comment was generated by <a href='https://git.front.kjuulh.io/kjuulh/contractor'>Contractor</a></small>",
output,
)
return output, err
}
func (b *BotHandler) Help() string {
return `<details open>
<summary><h3>/contractor [command]</h3></summary>
<strong>Commands:</strong>
* /contractor help
* triggers the help menu
* /contractor refresh
* triggers renovate to refresh the current pull request
</details>`
}
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 := "<!-- Status update start -->"
startIdx := strings.Index(commentBody, startCmnt)
endIdx := strings.Index(commentBody, "<!-- Status update end -->")
if startIdx >= 0 && endIdx >= 0 {
log.Println("found comment to replace")
var content string
if doneRequest.Error != "" {
content = fmt.Sprintf("<pre>ERROR: %s</pre><br>", doneRequest.Error)
}
if doneRequest.Status != "" {
content = fmt.Sprintf("<p>%s</p>", doneRequest.Status)
}
doneRequest.CommentBody = fmt.Sprintf(
"%s<br><hr>%s<hr><br>%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
}

28
go.mod
View File

@@ -1,27 +1,41 @@
module git.front.kjuulh.io/kjuulh/contractor module git.front.kjuulh.io/kjuulh/contractor
go 1.20 go 1.21
toolchain go1.22.1
require ( require (
dagger.io/dagger v0.8.1 dagger.io/dagger v0.8.1
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/google/uuid v1.3.0 github.com/google/go-github/v53 v53.2.0
github.com/google/go-github/v61 v61.0.0
github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.7.0 github.com/spf13/cobra v1.8.0
) )
require ( require (
github.com/99designs/gqlgen v0.17.31 // indirect github.com/99designs/gqlgen v0.17.31 // indirect
github.com/Khan/genqlient v0.6.0 // indirect github.com/Khan/genqlient v0.6.0 // indirect
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect
github.com/adrg/xdg v0.4.0 // indirect github.com/adrg/xdg v0.4.0 // indirect
github.com/bytedance/sonic v1.9.1 // indirect github.com/bytedance/sonic v1.9.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/cloudflare/circl v1.3.3 // indirect
github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // indirect github.com/go-playground/validator/v10 v10.14.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v4 v4.5.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/google/go-github/v55 v55.0.0 // indirect
github.com/google/go-github/v56 v56.0.0 // indirect
github.com/google/go-github/v57 v57.0.0 // indirect
github.com/google/go-github/v60 v60.0.0 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
@@ -36,13 +50,15 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/vektah/gqlparser/v2 v2.5.6 // indirect github.com/vektah/gqlparser/v2 v2.5.6 // indirect
golang.org/x/arch v0.3.0 // indirect golang.org/x/arch v0.3.0 // indirect
golang.org/x/crypto v0.11.0 // indirect golang.org/x/crypto v0.12.0 // indirect
golang.org/x/mod v0.12.0 // indirect golang.org/x/mod v0.12.0 // indirect
golang.org/x/net v0.12.0 // indirect golang.org/x/net v0.12.0 // indirect
golang.org/x/oauth2 v0.8.0 // indirect
golang.org/x/sync v0.3.0 // indirect golang.org/x/sync v0.3.0 // indirect
golang.org/x/sys v0.10.0 // indirect golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.11.0 // indirect golang.org/x/text v0.12.0 // indirect
golang.org/x/tools v0.11.0 // indirect golang.org/x/tools v0.11.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/protobuf v1.30.0 // indirect google.golang.org/protobuf v1.30.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

112
go.sum
View File

@@ -1,9 +1,12 @@
cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k=
dagger.io/dagger v0.8.1 h1:jLNPGubxrLWUfsX+snjaw913B1lxVmWftzdVehB+RQU= dagger.io/dagger v0.8.1 h1:jLNPGubxrLWUfsX+snjaw913B1lxVmWftzdVehB+RQU=
dagger.io/dagger v0.8.1/go.mod h1:CZwYt0FfVsEEYTFytzf2ihESB2P4H1S3/UfnrVxjBsE= dagger.io/dagger v0.8.1/go.mod h1:CZwYt0FfVsEEYTFytzf2ihESB2P4H1S3/UfnrVxjBsE=
github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158= github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158=
github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4= github.com/99designs/gqlgen v0.17.31/go.mod h1:i4rEatMrzzu6RXaHydq1nmEPZkb3bKQsnxNRHS4DQB4=
github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk= github.com/Khan/genqlient v0.6.0 h1:Bwb1170ekuNIVIwTJEqvO8y7RxBxXu639VJOkKSrwAk=
github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM= github.com/Khan/genqlient v0.6.0/go.mod h1:rvChwWVTqXhiapdhLDV4bp9tz/Xvtewwkon4DpWWCRM=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA=
github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
@@ -11,12 +14,27 @@ github.com/agnivade/levenshtein v1.1.1/go.mod h1:veldBMzWxcCG2ZvUTKD2kJNRdCk5hVb
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0 h1:IRY7Xy588KylkoycsUhFpW7cdGpy5Y5BPsz4IfuJtGk=
github.com/bradleyfalzon/ghinstallation/v2 v2.6.0/go.mod h1:oQ3etOwN3TRH4EwgW5/7MxSVMGlMlzG/O8TU7eYdoSk=
github.com/bradleyfalzon/ghinstallation/v2 v2.7.0 h1:ranXaC3Zz/F6G/f0Joj3LrFp2OzOKfJZev5Q7OaMc88=
github.com/bradleyfalzon/ghinstallation/v2 v2.7.0/go.mod h1:ymxfmloxXBFXvvF1KpeUhOQM6Dfz9NYtfvTiJyk82UE=
github.com/bradleyfalzon/ghinstallation/v2 v2.8.0 h1:yUmoVv70H3J4UOqxqsee39+KlXxNEDfTbAp8c/qULKk=
github.com/bradleyfalzon/ghinstallation/v2 v2.8.0/go.mod h1:fmPmvCiBWhJla3zDv9ZTQSZc8AbwyRnGW1yg5ep1Pcs=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0 h1:HmxIYqnxubRYcYGRc5v3wUekmo5Wv2uX3gukmWJ0AFk=
github.com/bradleyfalzon/ghinstallation/v2 v2.9.0/go.mod h1:wmkTDJf8CmVypxE8ijIStFnKoTa6solK5QfdmJrP9KI=
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0 h1:XWuWBRFEpqVrHepQob9yPS3Xg4K3Wr9QCx4fu8HbUNg=
github.com/bradleyfalzon/ghinstallation/v2 v2.10.0/go.mod h1:qoGA4DxWPaYTgVCrmEspVSjlTu4WYAiSxMIhorMRXXc=
github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I=
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -37,12 +55,42 @@ github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg
github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg=
github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-github/v53 v53.2.0 h1:wvz3FyF53v4BK+AsnvCmeNhf8AkTaeh2SoYu/XUvTtI=
github.com/google/go-github/v53 v53.2.0/go.mod h1:XhFRObz+m/l+UCm9b7KSIC3lT3NWSXGt7mOsAWEloao=
github.com/google/go-github/v55 v55.0.0 h1:4pp/1tNMB9X/LuAhs5i0KQAE40NmiR/y6prLNb9x9cg=
github.com/google/go-github/v55 v55.0.0/go.mod h1:JLahOTA1DnXzhxEymmFF5PP2tSS9JVNj68mSZNDwskA=
github.com/google/go-github/v56 v56.0.0 h1:TysL7dMa/r7wsQi44BjqlwaHvwlFlqkK8CtBWCX3gb4=
github.com/google/go-github/v56 v56.0.0/go.mod h1:D8cdcX98YWJvi7TLo7zM4/h8ZTx6u6fwGEkCdisopo0=
github.com/google/go-github/v57 v57.0.0 h1:L+Y3UPTY8ALM8x+TV0lg+IEBI+upibemtBD8Q9u7zHs=
github.com/google/go-github/v57 v57.0.0/go.mod h1:s0omdnye0hvK/ecLvpsGfJMiRt85PimQh4oygmLIxHw=
github.com/google/go-github/v58 v58.0.0/go.mod h1:k4hxDKEfoWpSqFlc8LTpGd9fu2KrV1YAa6Hi6FmDNY4=
github.com/google/go-github/v60 v60.0.0 h1:oLG98PsLauFvvu4D/YPxq374jhSxFYdzQGNCyONLfn8=
github.com/google/go-github/v60 v60.0.0/go.mod h1:ByhX2dP9XT9o/ll2yXAu2VD8l5eNVg8hD4Cr0S/LmQk=
github.com/google/go-github/v61 v61.0.0/go.mod h1:0WR+KmsWX75G2EbpyGsGmradjo3IiciuI4BmdVCobQY=
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4=
github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU=
github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI= github.com/iancoleman/strcase v0.3.0 h1:nTXanmYxhfFAMjZL34Ov6gkzEsSJZ5DbhxWjvSASxEI=
github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
@@ -77,6 +125,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I=
github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0=
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@@ -97,29 +147,87 @@ github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4d
github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU= github.com/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME= github.com/vektah/gqlparser/v2 v2.5.6/go.mod h1:z8xXUff237NntSuH8mLFijZ+1tjV1swDbpDqjJmk6ME=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4=
golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA=
golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
golang.org/x/crypto v0.12.0 h1:tFM/ta59kqch6LlvYnPa0yx5a83cL2nHflFhYKvv9Yk=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc=
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50=
golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA=
golang.org/x/oauth2 v0.8.0 h1:6dkIjl3j3LtZ/O3sTgZTMsLKSftL/B8Zgq4huOIIUu8=
golang.org/x/oauth2 v0.8.0/go.mod h1:yr7u4HXZRm1R1kBWqr/xKNqewf0plRYoB7sla+BCIXE=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E=
golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0 h1:eG7RXZHdqOJ1i+0lgLgCpSXAp6M3LYlAo6osgSi0xOM=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4=
golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/text v0.12.0 h1:k+n5B8goJNdU7hSvEtMUz3d1Q6D/XW4COJSJR6fN0mc=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8= golang.org/x/tools v0.11.0 h1:EMCa6U9S2LtZXLAMoWiR/R8dAQFRqbAitmbJ2UKhoi8=
golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8= golang.org/x/tools v0.11.0/go.mod h1:anzJrxPjNtfgiYQYirP2CPGzGLxrH2u2QBhn6Bf3qY8=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c=
google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

76
internal/bot/giteabot.go Normal file
View File

@@ -0,0 +1,76 @@
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
githubClient *providers.GitHubClient
}
func NewBotHandler(gitea *providers.GiteaClient, github *providers.GitHubClient) *BotHandler {
return &BotHandler{giteaClient: gitea, githubClient: github}
}
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 `
<h3>Contractor triggered renovate refresh on this repository</h3>
This comment will be updated with status
<!-- Status update start -->
<!-- Status update end -->
`, nil
}
return b.Help(), errors.New("could not recognize command")
}
output, err = innerHandle(input)
output = fmt.Sprintf(
"%s\n<small>This comment was generated by <a href='https://git.front.kjuulh.io/kjuulh/contractor'>Contractor</a></small>",
output,
)
return output, err
}
func (b *BotHandler) Help() string {
return `<details open>
<summary><h3>/contractor [command]</h3></summary>
<strong>Commands:</strong>
* /contractor help
* triggers the help menu
* /contractor refresh
* triggers renovate to refresh the current pull request
</details>`
}
func (b *BotHandler) AppendComment(
owner string,
repository string,
pullRequest int,
comment string,
backend models.SupportedBackend,
) (*models.AddCommentResponse, error) {
switch backend {
case models.SupportedBackendGitHub:
return b.githubClient.AddComment(owner, repository, pullRequest, comment)
case models.SupportedBackendGitea:
return b.giteaClient.AddComment(owner, repository, pullRequest, comment)
default:
panic("backend chosen was not a valid option")
}
}

View File

@@ -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.MessageTypeRefreshGiteaRepository,
func(ctx context.Context, item *queue.QueueMessage) error {
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
return nil
},
)
goqueue.Subscribe(
models.MessageTypeRefreshGiteaRepositoryDone,
func(ctx context.Context, item *queue.QueueMessage) error {
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
return nil
},
)
goqueue.Subscribe(
models.MessageTypeRefreshGiteaRepository,
func(ctx context.Context, item *queue.QueueMessage) error {
var request models.RefreshGiteaRepositoryRequest
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.MessageTypeRefreshGiteaRepositoryDone, models.RefreshGiteaRepositoryDoneRequest{
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.MessageTypeRefreshGiteaRepositoryDone, models.RefreshGiteaRepositoryDoneRequest{
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.MessageTypeRefreshGiteaRepositoryDone,
func(ctx context.Context, item *queue.QueueMessage) error {
var doneRequest models.RefreshGiteaRepositoryDoneRequest
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)
},
)
}

View File

@@ -0,0 +1,84 @@
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,
models.SupportedBackendGitea,
)
if err != nil {
return err
}
if err := gw.queue.Insert(models.MessageTypeRefreshGiteaRepository, models.RefreshGiteaRepositoryRequest{
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
}

View File

@@ -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 RegisterGitHubQueues(goqueue *queue.GoQueue, renovate *renovate.RenovateClient, giteaClient *providers.GitHubClient) {
goqueue.Subscribe(
models.MessageTypeRefreshGitHubRepository,
func(ctx context.Context, item *queue.QueueMessage) error {
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
return nil
},
)
goqueue.Subscribe(
models.MessageTypeRefreshGitHubRepositoryDone,
func(ctx context.Context, item *queue.QueueMessage) error {
log.Printf("handling message: %s, content: %s", item.Type, item.Content)
return nil
},
)
goqueue.Subscribe(
models.MessageTypeRefreshGitHubRepository,
func(ctx context.Context, item *queue.QueueMessage) error {
var request models.RefreshGitHubRepositoryRequest
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.MessageTypeRefreshGitHubRepositoryDone, models.RefreshGitHubRepositoryDoneRequest{
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.MessageTypeRefreshGitHubRepositoryDone, models.RefreshGitHubRepositoryDoneRequest{
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.MessageTypeRefreshGitHubRepositoryDone,
func(ctx context.Context, item *queue.QueueMessage) error {
var doneRequest models.RefreshGitHubRepositoryDoneRequest
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)
},
)
}

View File

@@ -0,0 +1,76 @@
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 GitHubWebhook struct {
botHandler *bot.BotHandler
queue *queue.GoQueue
}
type GitHubWebhookRequest 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 NewGitHubWebhook(botHandler *bot.BotHandler, queue *queue.GoQueue) *GitHubWebhook {
return &GitHubWebhook{
botHandler: botHandler,
queue: queue,
}
}
func (gw *GitHubWebhook) HandleGitHubWebhook(ctx context.Context, request *GitHubWebhookRequest) 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,
models.SupportedBackendGitHub,
)
if err != nil {
return err
}
if err := gw.queue.Insert(models.MessageTypeRefreshGitHubRepository, models.RefreshGitHubRepositoryRequest{
Repository: parts[1],
Owner: parts[0],
PullRequestID: request.Issue.Number,
CommentID: comment.ID,
CommentBody: comment.Body,
ReportProgress: true,
}); err != nil {
return err
}
}
return nil
}

View File

@@ -0,0 +1,69 @@
package models
const (
MessageTypeRefreshGiteaRepository = "refresh_gitea_repository"
MessageTypeRefreshGiteaRepositoryDone = "refresh_gitea_repository_done"
MessageTypeRefreshGitHubRepository = "refresh_github_repository"
MessageTypeRefreshGitHubRepositoryDone = "refresh_github_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 RefreshGiteaRepositoryRequest 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 RefreshGiteaRepositoryDoneRequest 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 RefreshGitHubRepositoryRequest 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 RefreshGitHubRepositoryDoneRequest 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"`
}
type SupportedBackend string
const (
SupportedBackendGitHub SupportedBackend = "github"
SupportedBackendGitea SupportedBackend = "gitea"
)

227
internal/providers/gitea.go Normal file
View File

@@ -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.RefreshGiteaRepositoryDoneRequest,
) error {
commentBody := html.UnescapeString(doneRequest.CommentBody)
startCmnt := "<!-- Status update start -->"
startIdx := strings.Index(commentBody, startCmnt)
endIdx := strings.Index(commentBody, "<!-- Status update end -->")
if startIdx >= 0 && endIdx >= 0 {
log.Println("found comment to replace")
var content string
if doneRequest.Error != "" {
content = fmt.Sprintf("<pre>ERROR: %s</pre><br>", doneRequest.Error)
}
if doneRequest.Status != "" {
content = fmt.Sprintf("%s<p>%s</p>", content, doneRequest.Status)
}
doneRequest.CommentBody = fmt.Sprintf(
"%s<br><hr>%s<hr><br>%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
}

View File

@@ -0,0 +1,117 @@
package providers
import (
"context"
"fmt"
"html"
"log"
"net/http"
"strings"
"git.front.kjuulh.io/kjuulh/contractor/internal/models"
"github.com/bradleyfalzon/ghinstallation/v2"
"github.com/google/go-github/v53/github"
)
type GitHubClient struct {
appID *int64
installationID *int64
privateKeyPath *string
client *github.Client
}
func NewGitHubClient(appID, installationID *int64, privateKeyPath *string) *GitHubClient {
return &GitHubClient{
appID: appID,
installationID: installationID,
privateKeyPath: privateKeyPath,
}
}
func (gc *GitHubClient) makeSureClientExist() {
if gc.client != nil {
return
}
tr := http.DefaultTransport
itr, err := ghinstallation.NewKeyFromFile(tr, *gc.appID, *gc.installationID, *gc.privateKeyPath)
if err != nil {
log.Fatal(err)
}
client := github.NewClient(&http.Client{Transport: itr})
gc.client = client
}
func (gc *GitHubClient) EditComment(
ctx context.Context,
doneRequest *models.RefreshGitHubRepositoryDoneRequest,
) error {
gc.makeSureClientExist()
commentBody := html.UnescapeString(doneRequest.CommentBody)
startCmnt := "<!-- Status update start -->"
startIdx := strings.Index(commentBody, startCmnt)
endIdx := strings.Index(commentBody, "<!-- Status update end -->")
if startIdx >= 0 && endIdx >= 0 {
log.Println("found comment to replace")
var content string
if doneRequest.Error != "" {
content = fmt.Sprintf("<pre>ERROR: %s</pre><br>", doneRequest.Error)
}
if doneRequest.Status != "" {
content = fmt.Sprintf("%s<p>%s</p>", content, doneRequest.Status)
}
doneRequest.CommentBody = fmt.Sprintf(
"%s<br><hr>%s<hr><br>%s",
commentBody[:startIdx+len(startCmnt)],
content,
commentBody[endIdx:],
)
}
_, _, err := gc.client.Issues.EditComment(ctx, doneRequest.Owner, doneRequest.Repository, int64(doneRequest.CommentID), &github.IssueComment{
Body: &doneRequest.CommentBody,
})
if err != nil {
log.Printf("failed to update comment: %s", err.Error())
return err
}
return nil
}
func (gc *GitHubClient) CreateWebhook(owner, repository string) error {
gc.makeSureClientExist()
// TODO: support for personal access tokens
// We implicitly get support via. github apps
return nil
}
func (gc *GitHubClient) AddComment(
owner, repository string,
pullRequest int,
comment string,
) (*models.AddCommentResponse, error) {
gc.makeSureClientExist()
resp, _, err := gc.client.Issues.CreateComment(context.Background(), owner, repository, pullRequest, &github.IssueComment{
Body: &comment,
})
if err != nil {
return nil, err
}
return &models.AddCommentResponse{
Body: *resp.Body,
ID: int(*resp.ID),
}, nil
}

113
internal/queue/goqueue.go Normal file
View File

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

103
internal/renovate/client.go Normal file
View File

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