diff --git a/.codecov.yml b/.codecov.yml
index b52a5da..0d496e0 100644
--- a/.codecov.yml
+++ b/.codecov.yml
@@ -7,3 +7,4 @@ ignore:
- "main.go"
- "notifier/github/github.go"
- "notifier/slack/slack.go"
+ - "notifier/typetalk/typetalk.go"
diff --git a/Gopkg.lock b/Gopkg.lock
index 1b23eca..8098e47 100644
--- a/Gopkg.lock
+++ b/Gopkg.lock
@@ -69,6 +69,18 @@
revision = "6ca4dbf54d38eea1a992b3c722a76a5d1c4cb25c"
version = "v0.0.4"
+[[projects]]
+ digest = "1:f2506d323836b8fa307d3cdcadd5475015367b95f465a002a8a0a7d77b505b8b"
+ name = "github.com/nulab/go-typetalk"
+ packages = [
+ "typetalk/internal",
+ "typetalk/shared",
+ "typetalk/v1",
+ ]
+ pruneopts = "NUT"
+ revision = "a2c8b0c4afa7b874f4d911a1e9199999bc15b2f4"
+ version = "v2.1.0"
+
[[projects]]
digest = "1:5cf3f025cbee5951a4ee961de067c8a89fc95a5adabead774f82822efabab121"
name = "github.com/pkg/errors"
@@ -147,6 +159,8 @@
"github.com/lestrrat-go/slack",
"github.com/lestrrat-go/slack/objects",
"github.com/mattn/go-colorable",
+ "github.com/nulab/go-typetalk/typetalk/shared",
+ "github.com/nulab/go-typetalk/typetalk/v1",
"github.com/urfave/cli",
"golang.org/x/oauth2",
"gopkg.in/yaml.v2",
diff --git a/README.md b/README.md
index 8c4d6f7..5907d72 100644
--- a/README.md
+++ b/README.md
@@ -23,6 +23,8 @@ You can do this by using this command.
+
+
## Installation
Grab the binary from GitHub Releases (Recommended)
@@ -60,7 +62,7 @@ For `plan` command, you also need to specify `plan` as the argument of tfnotify.
When running tfnotify, you can specify the configuration path via `--config` option (if it's omitted, it defaults to `{.,}tfnotify.y{,a}ml`).
-The example settings of GitHub, GitHub Enterprise and Slack are as follows. Incidentally, there is no need to replace TOKEN string such as `$GITHUB_TOKEN` with the actual token. Instead, it must be defined as environment variables in CI settings.
+The example settings of GitHub and GitHub Enterprise, Slack, [Typetalk](https://www.typetalk.com/) are as follows. Incidentally, there is no need to replace TOKEN string such as `$GITHUB_TOKEN` with the actual token. Instead, it must be defined as environment variables in CI settings.
[template](https://golang.org/pkg/text/template/) of Go can be used for `template`. The templates can be used in `tfnotify.yaml` are as follows:
@@ -198,6 +200,32 @@ terraform:
+
+For Typetalk
+
+```yaml
+---
+ci: circleci
+notifier:
+ typetalk:
+ token: $TYPETALK_TOKEN
+ topic_id: $TYPETALK_TOPIC_ID
+terraform:
+ plan:
+ template: |
+ {{ .Message }}
+ {{if .Result}}
+ ```
+ {{ .Result }}
+ ```
+ {{end}}
+ ```
+ {{ .Body }}
+ ```
+```
+
+
+
### Supported CI
Currently, supported CI are here:
diff --git a/config/config.go b/config/config.go
index 11a257a..419a4b1 100644
--- a/config/config.go
+++ b/config/config.go
@@ -21,8 +21,9 @@ type Config struct {
// Notifier is a notification notifier
type Notifier struct {
- Github GithubNotifier `yaml:"github"`
- Slack SlackNotifier `yaml:"slack"`
+ Github GithubNotifier `yaml:"github"`
+ Slack SlackNotifier `yaml:"slack"`
+ Typetalk TypetalkNotifier `yaml:"typetalk"`
}
// GithubNotifier is a notifier for GitHub
@@ -45,6 +46,12 @@ type SlackNotifier struct {
Bot string `yaml:"bot"`
}
+// TypetalkNotifier is a notifier for Typetalk
+type TypetalkNotifier struct {
+ Token string `yaml:"token"`
+ TopicID string `yaml:"topic_id"`
+}
+
// Terraform represents terraform configurations
type Terraform struct {
Default Default `yaml:"default"`
@@ -111,6 +118,11 @@ func (cfg *Config) Validation() error {
return fmt.Errorf("slack channel id is missing")
}
}
+ if cfg.isDefinedTypetalk() {
+ if cfg.Notifier.Typetalk.TopicID == "" {
+ return fmt.Errorf("Typetalk topic id is missing")
+ }
+ }
notifier := cfg.GetNotifierType()
if notifier == "" {
return fmt.Errorf("notifier is missing")
@@ -128,6 +140,11 @@ func (cfg *Config) isDefinedSlack() bool {
return cfg.Notifier.Slack != (SlackNotifier{})
}
+func (cfg *Config) isDefinedTypetalk() bool {
+ // not empty
+ return cfg.Notifier.Typetalk != (TypetalkNotifier{})
+}
+
// GetNotifierType return notifier type described in Config
func (cfg *Config) GetNotifierType() string {
if cfg.isDefinedGithub() {
@@ -136,6 +153,9 @@ func (cfg *Config) GetNotifierType() string {
if cfg.isDefinedSlack() {
return "slack"
}
+ if cfg.isDefinedTypetalk() {
+ return "typetalk"
+ }
return ""
}
diff --git a/config/config_test.go b/config/config_test.go
index 7d73d39..fcd29b7 100644
--- a/config/config_test.go
+++ b/config/config_test.go
@@ -37,6 +37,10 @@ func TestLoadFile(t *testing.T) {
Channel: "",
Bot: "",
},
+ Typetalk: TypetalkNotifier{
+ Token: "",
+ TopicID: "",
+ },
},
Terraform: Terraform{
Default: Default{
@@ -73,6 +77,10 @@ func TestLoadFile(t *testing.T) {
Channel: "",
Bot: "",
},
+ Typetalk: TypetalkNotifier{
+ Token: "",
+ TopicID: "",
+ },
},
Terraform: Terraform{
Default: Default{
@@ -186,6 +194,33 @@ notifier:
slack:
token: token
channel: channel
+`),
+ expected: "",
+ },
+ {
+ contents: []byte(`
+ci: circleci
+notifier:
+ typetalk:
+`),
+ expected: "notifier is missing",
+ },
+ {
+ contents: []byte(`
+ci: circleci
+notifier:
+ typetalk:
+ token: token
+`),
+ expected: "Typetalk topic id is missing",
+ },
+ {
+ contents: []byte(`
+ci: circleci
+notifier:
+ typetalk:
+ token: token
+ topic_id: 12345
`),
expected: "",
},
@@ -221,6 +256,10 @@ func TestGetNotifierType(t *testing.T) {
contents: []byte("repository:\n owner: a\n name: b\nci: circleci\nnotifier:\n slack:\n token: token\n"),
expected: "slack",
},
+ {
+ contents: []byte("repository:\n owner: a\n name: b\nci: circleci\nnotifier:\n typetalk:\n token: token\n"),
+ expected: "typetalk",
+ },
}
for _, testCase := range testCases {
cfg, err := helperLoadConfig(testCase.contents)
diff --git a/main.go b/main.go
index f563198..4b2fca7 100644
--- a/main.go
+++ b/main.go
@@ -9,6 +9,7 @@ import (
"github.com/mercari/tfnotify/notifier"
"github.com/mercari/tfnotify/notifier/github"
"github.com/mercari/tfnotify/notifier/slack"
+ "github.com/mercari/tfnotify/notifier/typetalk"
"github.com/mercari/tfnotify/terraform"
"github.com/urfave/cli"
@@ -100,6 +101,20 @@ func (t *tfnotify) Run() error {
return err
}
notifier = client.Notify
+ case "typetalk":
+ client, err := typetalk.NewClient(typetalk.Config{
+ Token: t.config.Notifier.Typetalk.Token,
+ TopicID: t.config.Notifier.Typetalk.TopicID,
+ Title: t.context.String("title"),
+ Message: t.context.String("message"),
+ CI: ci.URL,
+ Parser: t.parser,
+ Template: t.template,
+ })
+ if err != nil {
+ return err
+ }
+ notifier = client.Notify
case "":
return fmt.Errorf("notifier is missing")
default:
diff --git a/misc/images/3.png b/misc/images/3.png
new file mode 100644
index 0000000..f16ce9a
Binary files /dev/null and b/misc/images/3.png differ
diff --git a/notifier/typetalk/client.go b/notifier/typetalk/client.go
new file mode 100644
index 0000000..283cbf2
--- /dev/null
+++ b/notifier/typetalk/client.go
@@ -0,0 +1,79 @@
+package typetalk
+
+import (
+ "errors"
+ "os"
+ "strconv"
+
+ "github.com/mercari/tfnotify/terraform"
+ typetalk "github.com/nulab/go-typetalk/typetalk/v1"
+)
+
+// EnvToken is Typetalk API Token
+const EnvToken = "TYPETALK_TOKEN"
+
+// EnvTopicID is Typetalk topic ID
+const EnvTopicID = "TYPETALK_TOPIC_ID"
+
+// Client represents Typetalk API client.
+type Client struct {
+ *typetalk.Client
+ Config Config
+ common service
+ Notify *NotifyService
+ API API
+}
+
+// Config is a configuration for Typetalk Client
+type Config struct {
+ Token string
+ Title string
+ TopicID string
+ Message string
+ CI string
+ Parser terraform.Parser
+ Template terraform.Template
+}
+
+type service struct {
+ client *Client
+}
+
+// NewClient returns Client initialized with Config
+func NewClient(cfg Config) (*Client, error) {
+ token := os.ExpandEnv(cfg.Token)
+ if token == EnvToken {
+ token = os.Getenv(EnvToken)
+ }
+ if token == "" {
+ return &Client{}, errors.New("Typetalk token is missing")
+ }
+
+ topicIDString := os.ExpandEnv(cfg.TopicID)
+ if topicIDString == EnvTopicID {
+ topicIDString = os.Getenv(EnvTopicID)
+ }
+ if topicIDString == "" {
+ return &Client{}, errors.New("Typetalk topic ID is missing")
+ }
+
+ topicID, err := strconv.Atoi(topicIDString)
+ if err != nil {
+ return &Client{}, errors.New("Typetalk topic ID is not numeric value")
+ }
+
+ client := typetalk.NewClient(nil)
+ client.SetTypetalkToken(token)
+ c := &Client{
+ Config: cfg,
+ Client: client,
+ }
+ c.common.client = c
+ c.Notify = (*NotifyService)(&c.common)
+ c.API = &Typetalk{
+ Client: client,
+ TopicID: topicID,
+ }
+
+ return c, nil
+}
diff --git a/notifier/typetalk/client_test.go b/notifier/typetalk/client_test.go
new file mode 100644
index 0000000..dced5cc
--- /dev/null
+++ b/notifier/typetalk/client_test.go
@@ -0,0 +1,73 @@
+package typetalk
+
+import (
+ "os"
+ "testing"
+)
+
+func TestNewClient(t *testing.T) {
+ typetalkToken := os.Getenv(EnvToken)
+ defer func() {
+ os.Setenv(EnvToken, typetalkToken)
+ }()
+ os.Setenv(EnvToken, "")
+
+ testCases := []struct {
+ config Config
+ envToken string
+ expect string
+ }{
+ {
+ // specify directly
+ config: Config{Token: "abcdefg", TopicID: "12345"},
+ envToken: "",
+ expect: "",
+ },
+ {
+ // specify via env but not to be set env (part 1)
+ config: Config{Token: "TYPETALK_TOKEN", TopicID: "12345"},
+ envToken: "",
+ expect: "Typetalk token is missing",
+ },
+ {
+ // specify via env (part 1)
+ config: Config{Token: "TYPETALK_TOKEN", TopicID: "12345"},
+ envToken: "abcdefg",
+ expect: "",
+ },
+ {
+ // specify via env but not to be set env (part 2)
+ config: Config{Token: "$TYPETALK_TOKEN", TopicID: "12345"},
+ envToken: "",
+ expect: "typetalk token is missing",
+ },
+ {
+ // specify via env (part 2)
+ config: Config{Token: "$TYPETALK_TOKEN", TopicID: "12345"},
+ envToken: "abcdefg",
+ expect: "",
+ },
+ {
+ // no specification (part 1)
+ config: Config{TopicID: "12345"},
+ envToken: "",
+ expect: "Typetalk token is missing",
+ },
+ {
+ // no specification (part 2)
+ config: Config{TopicID: "12345"},
+ envToken: "abcdefg",
+ expect: "Typetalk token is missing",
+ },
+ }
+ for _, testCase := range testCases {
+ os.Setenv(EnvToken, testCase.envToken)
+ _, err := NewClient(testCase.config)
+ if err == nil {
+ continue
+ }
+ if err.Error() != testCase.expect {
+ t.Errorf("got %q but want %q", err.Error(), testCase.expect)
+ }
+ }
+}
diff --git a/notifier/typetalk/notify.go b/notifier/typetalk/notify.go
new file mode 100644
index 0000000..e4663f2
--- /dev/null
+++ b/notifier/typetalk/notify.go
@@ -0,0 +1,44 @@
+package typetalk
+
+import (
+ "context"
+ "errors"
+
+ "github.com/mercari/tfnotify/terraform"
+)
+
+// NotifyService handles notification process.
+type NotifyService service
+
+// Notify posts message to Typetalk.
+func (s *NotifyService) Notify(body string) (exit int, err error) {
+ cfg := s.client.Config
+ parser := s.client.Config.Parser
+ template := s.client.Config.Template
+
+ if cfg.TopicID == "" {
+ return terraform.ExitFail, errors.New("topic id is required")
+ }
+
+ result := parser.Parse(body)
+ if result.Error != nil {
+ return result.ExitCode, result.Error
+ }
+ if result.Result == "" {
+ return result.ExitCode, result.Error
+ }
+
+ template.SetValue(terraform.CommonTemplate{
+ Title: cfg.Title,
+ Message: cfg.Message,
+ Result: result.Result,
+ Body: body,
+ })
+ text, err := template.Execute()
+ if err != nil {
+ return result.ExitCode, err
+ }
+
+ _, _, err = s.client.API.ChatPostMessage(context.Background(), text)
+ return result.ExitCode, err
+}
diff --git a/notifier/typetalk/notify_test.go b/notifier/typetalk/notify_test.go
new file mode 100644
index 0000000..20bba6f
--- /dev/null
+++ b/notifier/typetalk/notify_test.go
@@ -0,0 +1,64 @@
+package typetalk
+
+import (
+ "context"
+ "testing"
+
+ "github.com/mercari/tfnotify/terraform"
+ typetalkShared "github.com/nulab/go-typetalk/typetalk/shared"
+ typetalk "github.com/nulab/go-typetalk/typetalk/v1"
+)
+
+func TestNotify(t *testing.T) {
+ testCases := []struct {
+ config Config
+ body string
+ exitCode int
+ ok bool
+ }{
+ {
+ config: Config{
+ Token: "token",
+ TopicID: "12345",
+ Message: "",
+ Parser: terraform.NewPlanParser(),
+ Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate),
+ },
+ body: "Plan: 1 to add",
+ exitCode: 0,
+ ok: true,
+ },
+ {
+ config: Config{
+ Token: "token",
+ TopicID: "12345",
+ Message: "",
+ Parser: terraform.NewPlanParser(),
+ Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate),
+ },
+ body: "BLUR BLUR BLUR",
+ exitCode: 1,
+ ok: false,
+ },
+ }
+ fake := fakeAPI{
+ FakeChatPostMessage: func(ctx context.Context, message string) (*typetalk.PostedMessageResult, *typetalkShared.Response, error) {
+ return nil, nil, nil
+ },
+ }
+
+ for _, testCase := range testCases {
+ client, err := NewClient(testCase.config)
+ if err != nil {
+ t.Fatal(err)
+ }
+ client.API = &fake
+ exitCode, err := client.Notify.Notify(testCase.body)
+ if (err == nil) != testCase.ok {
+ t.Errorf("got error %q", err)
+ }
+ if exitCode != testCase.exitCode {
+ t.Errorf("got %q but want %q", exitCode, testCase.exitCode)
+ }
+ }
+}
diff --git a/notifier/typetalk/typetalk.go b/notifier/typetalk/typetalk.go
new file mode 100644
index 0000000..df96c2f
--- /dev/null
+++ b/notifier/typetalk/typetalk.go
@@ -0,0 +1,24 @@
+package typetalk
+
+import (
+ "context"
+
+ typetalkShared "github.com/nulab/go-typetalk/typetalk/shared"
+ typetalk "github.com/nulab/go-typetalk/typetalk/v1"
+)
+
+// API is Typetalk API interface
+type API interface {
+ ChatPostMessage(ctx context.Context, message string) (*typetalk.PostedMessageResult, *typetalkShared.Response, error)
+}
+
+// Typetalk represents the attribute information necessary for requesting Typetalk API
+type Typetalk struct {
+ *typetalk.Client
+ TopicID int
+}
+
+// ChatPostMessage is wrapper for https://godoc.org/github.com/nulab/go-typetalk/typetalk/v1#MessagesService.PostMessage
+func (t *Typetalk) ChatPostMessage(ctx context.Context, message string) (*typetalk.PostedMessageResult, *typetalkShared.Response, error) {
+ return t.Client.Messages.PostMessage(ctx, t.TopicID, message, nil)
+}
diff --git a/notifier/typetalk/typetalk_test.go b/notifier/typetalk/typetalk_test.go
new file mode 100644
index 0000000..501252c
--- /dev/null
+++ b/notifier/typetalk/typetalk_test.go
@@ -0,0 +1,17 @@
+package typetalk
+
+import (
+ "context"
+
+ typetalkShared "github.com/nulab/go-typetalk/typetalk/shared"
+ typetalk "github.com/nulab/go-typetalk/typetalk/v1"
+)
+
+type fakeAPI struct {
+ API
+ FakeChatPostMessage func(ctx context.Context, message string) (*typetalk.PostedMessageResult, *typetalkShared.Response, error)
+}
+
+func (g *fakeAPI) ChatPostMessage(ctx context.Context, message string) (*typetalk.PostedMessageResult, *typetalkShared.Response, error) {
+ return g.FakeChatPostMessage(ctx, message)
+}