6 Commits

Author SHA1 Message Date
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
4c095d62e3 chore(cuddle-please): update
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-08 16:20:29 +02:00
047f830b58 docs(README): add motivation why this project should exist
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 16:09:54 +02:00
16 changed files with 1198 additions and 664 deletions

View File

@@ -6,6 +6,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [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
### Added

View File

@@ -14,6 +14,21 @@ image behind the scenes, this can be changed in the configuration.
<small>Do note that the contractor was run under a personal user, hence the same
user replied</small>
## Motivation
Renovate by default if hosted yourself, is neither sharded, or runs on a
cron-job cycle. This leaves a lot to be desired from a developers point of view.
As it may take quite a long time for renovate to revisit the pull-request again,
if there is a lot of repositories enabled.
This project intends to add an ad-hoc invocation of renovate for a single
repository, this enables developers to retrigger renovate whenever they want.
The project is built to be integrated with github and gitea (initially), and
work in its pull-request system, so when a renovate pr shows up, you can either
manually retrigger it, or enable any of the options in the renovate dashboard,
and retrigger.
## DISCLAIMER
The project is still 0.x.x As such the api is subject to change, and the
@@ -26,15 +41,13 @@ what the project will look like once feature-complete.
- Includes basic setup such as working server bot, and installation command,
automation is missing however. Also only gitea support for now, because this
is where the project initially is supposed to be in use.
- [ ] 0.2.0
- Add GitHub support
- [x] 0.2.0
- 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
- Add Delegation support (not clustering, just delegation of renovate jobs)
- [ ] 0.4.0
- Slack integration
- [ ] 0.5.0
- GitHub App and such support
- [ ] 0.6.0
- Add api key support
## Getting started

View File

@@ -195,7 +195,7 @@ mod please_release {
) -> eyre::Result<()> {
let build_image = client
.container()
.from("kasperhermansen/cuddle-please:main-1691463075");
.from("kasperhermansen/cuddle-please:main-1691504183");
let src = client
.git_opts(

View File

@@ -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())
}
},
@@ -67,79 +50,23 @@ func serverCmd() *cobra.Command {
var (
url string
token string
githubAppID int64
githubInstallationID int64
githubPrivateKeyPath 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)
githubClient := providers.NewGitHubClient(&githubAppID, &githubInstallationID, &githubPrivateKeyPath)
renovateClient := renovate.NewRenovateClient("")
queue := queue.NewGoQueue()
botHandler := bot.NewBotHandler(giteaClient, githubClient)
cancelCtx, cancel := context.WithTimeout(ctx, time.Second*30)
defer cancel()
giteaWebhook := features.NewGiteaWebhook(botHandler, queue)
githubWebhook := features.NewGitHubWebhook(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)
features.RegisterGitHubQueues(queue, renovateClient, githubClient)
cmd := &cobra.Command{
Use: "server",
@@ -148,129 +75,69 @@ 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.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
}
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,
githubWebhook *features.GitHubWebhook,
) *cobra.Command {
cmd := &cobra.Command{
Use: "serve",
Run: func(cmd *cobra.Command, args []string) {
engine := gin.Default()
gitea := engine.Group("/gitea")
github := engine.Group("/github")
{
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
github.POST("/webhook", func(ctx *gin.Context) {
var request features.GitHubWebhookRequest
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 := githubWebhook.HandleGitHubWebhook(ctx.Request.Context(), &request); err != nil {
ctx.AbortWithError(500, err)
return
}
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
}
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 +145,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 `
<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
}

9
go.mod
View File

@@ -5,6 +5,7 @@ go 1.20
require (
dagger.io/dagger v0.8.1
github.com/gin-gonic/gin v1.9.1
github.com/google/go-github/v53 v53.2.0
github.com/google/uuid v1.3.0
github.com/joho/godotenv v1.5.1
github.com/spf13/cobra v1.7.0
@@ -13,15 +14,21 @@ require (
require (
github.com/99designs/gqlgen v0.17.31 // 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/bradleyfalzon/ghinstallation/v2 v2.6.0 // indirect
github.com/bytedance/sonic v1.9.1 // 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/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.14.0 // 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-querystring v1.1.0 // indirect
github.com/iancoleman/strcase v0.3.0 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
@@ -39,10 +46,12 @@ require (
golang.org/x/crypto v0.11.0 // indirect
golang.org/x/mod 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/sys v0.10.0 // indirect
golang.org/x/text 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
gopkg.in/yaml.v3 v3.0.1 // indirect
)

78
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/go.mod h1:CZwYt0FfVsEEYTFytzf2ihESB2P4H1S3/UfnrVxjBsE=
github.com/99designs/gqlgen v0.17.31 h1:VncSQ82VxieHkea8tz11p7h/zSbvHSxSDZfywqWt158=
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/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/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8=
@@ -11,12 +14,19 @@ 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/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
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/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.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
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-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
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/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=
@@ -37,9 +47,21 @@ 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/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
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/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.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-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/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
@@ -97,29 +119,81 @@ 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/vektah/gqlparser/v2 v2.5.6 h1:Ou14T0N1s191eRMZ1gARVqohcbe1e8FrcONScsq8cRU=
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.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
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/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio=
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/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/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/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-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-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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/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/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/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=
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/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/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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
}