diff --git a/pkg/git/git.go b/pkg/git/git.go index 8c9003b..9e3d381 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -116,6 +116,51 @@ func DeleteBranch(repoURL, branch string) error { return nil } +func CreateTag(repoURL, branch, tag string) error { + r, err := git.Clone(memory.NewStorage(), memfs.New(), &git.CloneOptions{ + URL: repoURL, + Depth: 1, + }) + if err != nil { + return fmt.Errorf("error cloning git repo %w", err) + } + + w, err := r.Worktree() + if err != nil { + return fmt.Errorf("error retrieving git worktree %w", err) + } + + err = w.Checkout(&git.CheckoutOptions{ + Branch: plumbing.ReferenceName(defaultLocalRef + "/" + branch), + Force: true, + }) + if err != nil { + return fmt.Errorf("error during git checkout %w", err) + } + + head, err := r.Head() + if err != nil { + return fmt.Errorf("error finding head %w", err) + } + + _, err = r.CreateTag(tag, head.Hash(), &git.CreateTagOptions{}) + if err != nil { + return fmt.Errorf("error creating tag %w", err) + } + + err = r.Push(&git.PushOptions{ + RemoteName: "origin", + RefSpecs: []config.RefSpec{ + config.RefSpec("refs/tags/*:refs/tags/*"), + }, + }) + if err != nil { + return fmt.Errorf("error pushing to repo %w", err) + } + + return nil +} + func CommitAndPush(r *git.Repository, msg string) (string, error) { w, err := r.Worktree() if err != nil { diff --git a/pkg/webhooks/github/actions/aggregate_releases.go b/pkg/webhooks/github/actions/aggregate_releases.go index 0ffa2d9..82ee4df 100644 --- a/pkg/webhooks/github/actions/aggregate_releases.go +++ b/pkg/webhooks/github/actions/aggregate_releases.go @@ -249,11 +249,11 @@ func isReleaseFreeze(ctx context.Context, client *v3.Client, number int, owner, for _, comment := range comments { comment := comment - if ok := searchForCommandInBody(pointer.SafeDeref(comment.Body), IssueCommentReleaseFreeze); ok { + if _, ok := searchForCommandInBody(pointer.SafeDeref(comment.Body), IssueCommentReleaseFreeze); ok { return true, nil } - if ok := searchForCommandInBody(pointer.SafeDeref(comment.Body), IssueCommentReleaseUnfreeze); ok { + if _, ok := searchForCommandInBody(pointer.SafeDeref(comment.Body), IssueCommentReleaseUnfreeze); ok { return false, nil } } diff --git a/pkg/webhooks/github/actions/issues_handler.go b/pkg/webhooks/github/actions/issues_handler.go index 7fc4e3b..1c90325 100644 --- a/pkg/webhooks/github/actions/issues_handler.go +++ b/pkg/webhooks/github/actions/issues_handler.go @@ -20,6 +20,7 @@ const ( IssueCommentBuildFork IssueCommentCommand = IssueCommentCommandPrefix + "ok-to-build" IssueCommentReleaseFreeze IssueCommentCommand = IssueCommentCommandPrefix + "freeze" IssueCommentReleaseUnfreeze IssueCommentCommand = IssueCommentCommandPrefix + "unfreeze" + IssueCommentTag IssueCommentCommand = IssueCommentCommandPrefix + "tag" ) var ( @@ -27,6 +28,7 @@ var ( IssueCommentBuildFork: true, IssueCommentReleaseFreeze: true, IssueCommentReleaseUnfreeze: true, + IssueCommentTag: true, } AllowedAuthorAssociations = map[string]bool{ @@ -84,13 +86,20 @@ func (r *IssuesAction) HandleIssueComment(ctx context.Context, p *IssuesActionPa return nil } - if ok := searchForCommandInBody(p.Comment, IssueCommentBuildFork); ok { + if _, ok := searchForCommandInBody(p.Comment, IssueCommentBuildFork); ok { err := r.buildForkPR(ctx, p) if err != nil { return err } } + if args, ok := searchForCommandInBody(p.Comment, IssueCommentTag); ok { + err := r.tag(ctx, p, args) + if err != nil { + return err + } + } + return nil } @@ -138,11 +147,50 @@ func (r *IssuesAction) buildForkPR(ctx context.Context, p *IssuesActionParams) e return nil } -func searchForCommandInBody(comment string, want IssueCommentCommand) bool { +func (r *IssuesAction) tag(ctx context.Context, p *IssuesActionParams, args []string) error { + if len(args) == 0 { + return fmt.Errorf("no tag name given, skipping") + } + + tag := args[0] + + pullRequest, _, err := r.client.GetV3Client().PullRequests.Get(ctx, r.client.Organization(), p.RepositoryName, p.PullRequestNumber) + if err != nil { + return fmt.Errorf("error finding issue related pull request %w", err) + } + + token, err := r.client.GitToken(ctx) + if err != nil { + return fmt.Errorf("error creating git token %w", err) + } + + targetRepoURL, err := url.Parse(p.RepositoryURL) + if err != nil { + return err + } + targetRepoURL.User = url.UserPassword("x-access-token", token) + + headRef := *pullRequest.Head.Ref + err = git.CreateTag(p.RepositoryURL, headRef, tag) + if err != nil { + return err + } + + r.logger.Infow("pushed tag to repo", "repo", p.RepositoryName, "branch", headRef, "tag", tag) + + return nil +} + +func searchForCommandInBody(comment string, want IssueCommentCommand) ([]string, bool) { for _, line := range strings.Split(comment, "\n") { line = strings.TrimSpace(line) - cmd := IssueCommentCommand(line) + fields := strings.Fields(line) + if len(fields) == 0 { + continue + } + + cmd, args := IssueCommentCommand(fields[0]), fields[1:] _, ok := IssueCommentCommands[cmd] if !ok { @@ -150,9 +198,9 @@ func searchForCommandInBody(comment string, want IssueCommentCommand) bool { } if cmd == want { - return true + return args, true } } - return false + return nil, false } diff --git a/pkg/webhooks/github/actions/issues_handler_test.go b/pkg/webhooks/github/actions/issues_handler_test.go index 71d0841..77cebf9 100644 --- a/pkg/webhooks/github/actions/issues_handler_test.go +++ b/pkg/webhooks/github/actions/issues_handler_test.go @@ -8,16 +8,18 @@ import ( func Test_searchForCommandInBody(t *testing.T) { tests := []struct { - name string - body string - search IssueCommentCommand - want bool + name string + body string + search IssueCommentCommand + want bool + wantArgs []string }{ { - name: "find in single line", - body: "/freeze", - search: IssueCommentReleaseFreeze, - want: true, + name: "find in single line", + body: "/freeze", + search: IssueCommentReleaseFreeze, + want: true, + wantArgs: []string{}, }, { name: "no match", @@ -26,26 +28,40 @@ func Test_searchForCommandInBody(t *testing.T) { want: false, }, { - name: "find with strip", - body: " /freeze ", - search: IssueCommentReleaseFreeze, - want: true, + name: "find with strip", + body: " /freeze ", + search: IssueCommentReleaseFreeze, + want: true, + wantArgs: []string{}, }, { name: "find in multi line", body: `Release is frozen now. /freeze `, - search: IssueCommentReleaseFreeze, - want: true, + search: IssueCommentReleaseFreeze, + want: true, + wantArgs: []string{}, + }, + { + name: "with args", + body: `Tagging. + /tag v0.1.17-rc.0 + `, + search: IssueCommentTag, + want: true, + wantArgs: []string{"v0.1.17-rc.0"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - got := searchForCommandInBody(tt.body, tt.search) + gotArgs, got := searchForCommandInBody(tt.body, tt.search) if diff := cmp.Diff(got, tt.want); diff != "" { t.Errorf("diff: %s", diff) } + if diff := cmp.Diff(gotArgs, tt.wantArgs); diff != "" { + t.Errorf("diff: %s", diff) + } }) } }