Skip to content

feat: support sending msg to dingding based on pr author #45

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Sep 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 8 additions & 14 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -10,20 +10,14 @@ COPY main.go main.go
RUN go mod download
RUN CGO_ENABLED=0 go build -ldflags "-w -s" -o gogit

FROM ubuntu:kinetic
FROM alpine:3.10

LABEL "repository"="https://github.com/linuxsuren/gogit"
LABEL "homepage"="https://github.com/linuxsuren/gogit"
LABEL "maintainer"="Rick"
LABEL "Name"="A tool for sending build status to git providers"

COPY --from=builder /workspace/gogit /usr/bin/gogit
RUN apt update -y && apt install ca-certificates -y
ENTRYPOINT ["gogit"]
RUN apk add ca-certificates

# for uknown reason, the following code does not work
#FROM alpine:3.10
#
#LABEL "repository"="https://github.com/linuxsuren/gogit"
#LABEL "homepage"="https://github.com/linuxsuren/gogit"
#LABEL "maintainer"="Rick"
#LABEL "Name"="A tool for sending build status to git providers"
#
#COPY --from=builder /workspace/gogit /usr/bin/gogit
#
#ENTRYPOINT ["gogit"]
ENTRYPOINT ["gogit"]
15 changes: 14 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,21 @@ Or in the following use cases:

* [Tekton Task](https://hub.tekton.dev/tekton/task/gogit)

## Argo workflow Executor
### Send a notification to DingDing
Below is an example of sending a notification to DingDing based on the Gitlab/GitHub pull request author/reviewers/assignees:

```shell
gogit pr --provider gitlab \
--server http://10.121.218.82:6080 \
--repo yaml-readme \
--pr 1 \
--username linuxsuren \
--token h-zez9CWzyzykbLoS53s \
--msg 'workflow done' \
--dingding-tokens linuxsuren=dingdingtoken
```

## Argo workflow Executor
Install as an Argo workflow executor plugin:

```shell
Expand Down
10 changes: 8 additions & 2 deletions cmd/argoworkflow/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,14 @@ COPY . .
RUN go mod download
RUN GOPROXY=${GOPROXY} CGO_ENABLED=0 go build -ldflags "-w -s" -o workflow-executor-gogit

FROM ubuntu:kinetic
FROM alpine:3.10

LABEL "repository"="https://github.com/linuxsuren/gogit"
LABEL "homepage"="https://github.com/linuxsuren/gogit"
LABEL "maintainer"="Rick"
LABEL "Name"="A tool for sending build status to git providers"

COPY --from=builder /workspace/workflow-executor-gogit /usr/bin/workflow-executor-gogit
RUN apt update -y && apt install ca-certificates -y
RUN apk add ca-certificates

CMD ["workflow-executor-gogit"]
2 changes: 1 addition & 1 deletion cmd/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ func newCommentCommand() (c *cobra.Command) {
RunE: opt.runE,
}

opt.addFlags(c)
flags := c.Flags()
opt.addFlags(flags)
flags.StringVarP(&opt.message, "message", "m", "", "The comment body")
flags.StringVarP(&opt.identity, "identity", "", pkg.CommentEndMarker, "The identity for matching exiting comment")
return
Expand Down
130 changes: 130 additions & 0 deletions cmd/pull_request.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package cmd

import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"sync"

"github.com/jenkins-x/go-scm/scm"
"github.com/spf13/cobra"
)

func newPullRequestCmd() (c *cobra.Command) {
opt := &pullRequestOption{}
c = &cobra.Command{
Use: "pr",
Short: "Pull request related commands",
PreRunE: opt.preRunE,
RunE: opt.runE,
}
opt.addFlags(c)
flags := c.Flags()
flags.BoolVarP(&opt.printAuthor, "author", "", false, "Print the author of the pull request")
flags.BoolVarP(&opt.printReviewer, "reviewer", "", false, "Print the reviewers of the pull request")
flags.BoolVarP(&opt.printAssignee, "assignee", "", false, "Print the assignees of the pull request")
flags.StringVarP(&opt.msg, "msg", "", "", "The message of the pull request")
flags.StringSliceVarP(&opt.dingdingTokenPairs, "dingding-tokens", "", []string{}, "The dingding token pairs of the pull request, format: login=token")
return
}

func (o *pullRequestOption) preRunE(c *cobra.Command, args []string) (err error) {
o.dingdingTokenMap = make(map[string]string)
for _, pair := range o.dingdingTokenPairs {
keyVal := strings.Split(pair, "=")
if len(keyVal) == 2 {
o.dingdingTokenMap[keyVal[0]] = keyVal[1]
} else {
err = fmt.Errorf("invalid dingding token pair: %q", pair)
return
}
}
return
}

func (o *pullRequestOption) runE(c *cobra.Command, args []string) (err error) {
var scmClient *scm.Client
if scmClient, err = o.getClient(); err != nil {
return
}

var pr *scm.PullRequest
if pr, _, err = scmClient.PullRequests.Find(c.Context(), o.repo, o.pr); err != nil {
return
}

users := make(map[string]string, 0)
addToMap(users, pr.Author.Login)

if o.printAuthor {
c.Println(pr.Author.Email, pr.Author.Name, pr.Author.Login, pr.Author.ID)
}
for _, user := range pr.Reviewers {
if o.printReviewer {
c.Println(user.Email, user.Name, user.Login, user.ID)
}
addToMap(users, user.Login)
}
for _, user := range pr.Assignees {
if o.printAssignee {
c.Println(user.Email, user.Name, user.Login, user.ID)
}
addToMap(users, user.Login)
}

var wait sync.WaitGroup
for login, _ := range users {
wait.Add(1)
go func(login string) {
defer wait.Done()
if token, ok := o.dingdingTokenMap[login]; ok {
api := fmt.Sprintf("https://oapi.dingtalk.com/robot/send?access_token=%s", token)
msg := strings.NewReader(`{"msgtype": "text", "text": {"content": "` + o.msg + `"}}`)

resp, err := http.Post(api, "application/json", msg)
if err != nil {
c.Println(err)
} else if resp.StatusCode != http.StatusOK {
c.Printf("send message to %q failed, received code %d instead of 200\n", login, resp.StatusCode)
} else {
body, _ := io.ReadAll(resp.Body)

dingdingResp := &DingDingResponse{}
if err := json.Unmarshal(body, dingdingResp); err != nil {
c.Printf("cannot unmarshal the response for %q: %v\n", login, err)
} else if dingdingResp.ErrCode != 0 {
c.Printf("receive error response for %q: %q\n", login, dingdingResp.ErrMsg)
} else {
c.Printf("send message to %q successfully\n", login)
}

}
}
}(login)
}
wait.Wait()
return
}

type DingDingResponse struct {
ErrCode int `json:"errcode"`
ErrMsg string `json:"errmsg"`
}

func addToMap(m map[string]string, k string) {
if _, ok := m[k]; !ok {
m[k] = ""
}
}

type pullRequestOption struct {
gitProviderOption
printAuthor bool
printReviewer bool
printAssignee bool
msg string
dingdingTokenPairs []string
dingdingTokenMap map[string]string
}
40 changes: 40 additions & 0 deletions cmd/pull_request_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package cmd

import (
"io"
"testing"

"github.com/stretchr/testify/assert"
)

func TestPullRequestCmd(t *testing.T) {
t.Run("missing required flags", func(t *testing.T) {
c := NewRootCommand()
c.SetOut(io.Discard)

c.SetArgs([]string{"pr", "--dingding-tokens", "a=b"})
err := c.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "required flag(s)")
})

t.Run("invalid dingding token pairs", func(t *testing.T) {
c := NewRootCommand()
c.SetOut(io.Discard)

c.SetArgs([]string{"pr", "--dingding-tokens", "a=b=c"})
err := c.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "invalid dingding token pair")
})

t.Run("invalid git kind", func(t *testing.T) {
c := NewRootCommand()
c.SetOut(io.Discard)

c.SetArgs([]string{"pr", "--provider", "invalid", "--pr=1", "--repo=xxx/xxx", "--token=token", "--username=xxx"})
err := c.Execute()
assert.Error(t, err)
assert.Contains(t, err.Error(), "Unsupported")
})
}
3 changes: 2 additions & 1 deletion cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ func NewRootCommand() (c *cobra.Command) {
}

c.AddCommand(newCheckoutCommand(),
newStatusCmd(), newCommentCommand())
newStatusCmd(), newCommentCommand(),
newPullRequestCmd())
return
}
25 changes: 17 additions & 8 deletions cmd/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"os"
"strings"

"github.com/jenkins-x/go-scm/scm"
"github.com/jenkins-x/go-scm/scm/factory"
"github.com/linuxsuren/gogit/pkg"
"github.com/spf13/cobra"
flag "github.com/spf13/pflag"
)

func newStatusCmd() (cmd *cobra.Command) {
Expand All @@ -18,8 +19,8 @@ func newStatusCmd() (cmd *cobra.Command) {
RunE: opt.runE,
}

opt.addFlags(cmd)
flags := cmd.Flags()
opt.addFlags(flags)
flags.StringVarP(&opt.status, "status", "", "",
"Build token, such as: pending, success, cancelled, error")
flags.StringVarP(&opt.target, "target", "", "https://github.com/LinuxSuRen/gogit", "Address of the build server")
Expand All @@ -28,11 +29,6 @@ func newStatusCmd() (cmd *cobra.Command) {
flags.StringVarP(&opt.description, "description", "", "",
"The description of a build token")
flags.BoolVarP(&opt.print, "print", "", false, "Print the status list then exit")

_ = cmd.MarkFlagRequired("repo")
_ = cmd.MarkFlagRequired("pr")
_ = cmd.MarkFlagRequired("username")
_ = cmd.MarkFlagRequired("token")
return
}

Expand Down Expand Up @@ -110,7 +106,8 @@ type gitProviderOption struct {
pr int
}

func (o *gitProviderOption) addFlags(flags *flag.FlagSet) {
func (o *gitProviderOption) addFlags(c *cobra.Command) {
flags := c.Flags()
flags.StringVarP(&o.provider, "provider", "p", "github", "The provider of git, such as: gitlab, github")
flags.StringVarP(&o.server, "server", "s", "", "The server address of target git provider, only need when it's a private provider")
flags.StringVarP(&o.owner, "owner", "o", "", "Owner of a git repository")
Expand All @@ -119,6 +116,11 @@ func (o *gitProviderOption) addFlags(flags *flag.FlagSet) {
flags.StringVarP(&o.username, "username", "u", "", "Username of the git repository")
flags.StringVarP(&o.token, "token", "t", "",
"The access token of the git repository. Or you could provide a file path, such as: file:///var/token")

_ = c.MarkFlagRequired("repo")
_ = c.MarkFlagRequired("pr")
_ = c.MarkFlagRequired("username")
_ = c.MarkFlagRequired("token")
}

func (o *gitProviderOption) preHandle() {
Expand All @@ -127,6 +129,13 @@ func (o *gitProviderOption) preHandle() {
}
}

func (o *gitProviderOption) getClient() (scmClient *scm.Client, err error) {
scmClient, err = factory.NewClient(o.provider, o.server, o.token, func(c *scm.Client) {
c.Username = o.username
})
return
}

type statusOption struct {
gitProviderOption
status string
Expand Down