Skip to content

Commit

Permalink
feat: support sending msg to dingding based on pr author (#45)
Browse files Browse the repository at this point in the history
  • Loading branch information
LinuxSuRen committed Sep 21, 2023
1 parent aed063d commit 5fef75d
Show file tree
Hide file tree
Showing 8 changed files with 220 additions and 27 deletions.
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

0 comments on commit 5fef75d

Please sign in to comment.