Skip to content

Commit

Permalink
Merge pull request #72 from mercari/dylan/no-change-label
Browse files Browse the repository at this point in the history
  • Loading branch information
b4b4r07 authored May 18, 2020
2 parents 7a2ba7c + 3c91c66 commit 42a3e56
Show file tree
Hide file tree
Showing 12 changed files with 152 additions and 32 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,30 @@ terraform:
# ...
```

You can also let tfnotify add a label to PRs whose `terraform plan` output result in no change to the current infrastructure. Currently, this feature is for Github labels only.

```yaml
---
# ...
terraform:
# ...
plan:
template: |
{{ .Title }} <sup>[CI link]( {{ .Link }} )</sup>
{{ .Message }}
{{if .Result}}
<pre><code>{{ .Result }}
</pre></code>
{{end}}
<details><summary>Details (Click me)</summary>
<pre><code>{{ .Body }}
</pre></code></details>
when_no_changes:
label: "no-changes"
# ...
```

Sometimes you may want not to HTML-escape Terraform command outputs.
For example, when you use code block to print command output, it's better to use raw characters instead of character references (e.g. `-/+` -> `-/&#43;`, `"` -> `&#34;`).

Expand Down
10 changes: 8 additions & 2 deletions config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,15 +81,21 @@ type Fmt struct {

// Plan is a terraform plan config
type Plan struct {
Template string `yaml:"template"`
WhenDestroy WhenDestroy `yaml:"when_destroy,omitempty"`
Template string `yaml:"template"`
WhenDestroy WhenDestroy `yaml:"when_destroy,omitempty"`
WhenNoChanges WhenNoChanges `yaml:"when_no_changes,omitempty"`
}

// WhenDestroy is a configuration to notify the plan result contains destroy operation
type WhenDestroy struct {
Template string `yaml:"template"`
}

// WhenNoChange is a configuration to add a label when the plan result contains no change
type WhenNoChanges struct {
Label string `yaml:"label,omitempty"`
}

// Apply is a terraform apply config
type Apply struct {
Template string `yaml:"template"`
Expand Down
7 changes: 5 additions & 2 deletions config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func TestLoadFile(t *testing.T) {
ok: true,
},
{
file: "../example-with-destroy.tfnotify.yaml",
file: "../example-with-destroy-and-no-changes.tfnotify.yaml",
cfg: Config{
CI: "circleci",
Notifier: Notifier{
Expand Down Expand Up @@ -96,13 +96,16 @@ func TestLoadFile(t *testing.T) {
WhenDestroy: WhenDestroy{
Template: "## :warning: WARNING: Resource Deletion will happen :warning:\n\nThis plan contains **resource deletion**. Please check the plan result very carefully!\n",
},
WhenNoChanges: WhenNoChanges{
Label: "no-changes",
},
},
Apply: Apply{
Template: "",
},
UseRawOutput: false,
},
path: "../example-with-destroy.tfnotify.yaml",
path: "../example-with-destroy-and-no-changes.tfnotify.yaml",
},
ok: true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ terraform:
<pre><code>{{ .Body }}
</pre></code></details>
when_no_changes:
label: "no-changes"
when_destroy:
template: |
## :warning: WARNING: Resource Deletion will happen :warning:
Expand Down
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ func (t *tfnotify) Run() error {
Template: t.template,
DestroyWarningTemplate: t.destroyWarningTemplate,
WarnDestroy: t.warnDestroy,
NoChangesLabel: t.config.Terraform.Plan.WhenNoChanges.Label,
})
if err != nil {
return err
Expand Down
2 changes: 2 additions & 0 deletions notifier/github/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ type Config struct {
// DestroyWarningTemplate is used only for additional warning
// the plan result contains destroy operation
DestroyWarningTemplate terraform.Template
// NoChangesLabel is a label to add to PRs when terraform output contains no changes
NoChangesLabel string
}

// PullRequest represents GitHub Pull Request metadata
Expand Down
12 changes: 12 additions & 0 deletions notifier/github/github.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ type API interface {
IssuesCreateComment(ctx context.Context, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
IssuesDeleteComment(ctx context.Context, commentID int64) (*github.Response, error)
IssuesListComments(ctx context.Context, number int, opt *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error)
IssuesAddLabels(ctx context.Context, number int, labels []string) ([]*github.Label, *github.Response, error)
IssuesRemoveLabel(ctx context.Context, number int, label string) (*github.Response, error)
RepositoriesCreateComment(ctx context.Context, sha string, comment *github.RepositoryComment) (*github.RepositoryComment, *github.Response, error)
RepositoriesListCommits(ctx context.Context, opt *github.CommitsListOptions) ([]*github.RepositoryCommit, *github.Response, error)
RepositoriesGetCommit(ctx context.Context, sha string) (*github.RepositoryCommit, *github.Response, error)
Expand All @@ -37,6 +39,16 @@ func (g *GitHub) IssuesListComments(ctx context.Context, number int, opt *github
return g.Client.Issues.ListComments(ctx, g.owner, g.repo, number, opt)
}

// IssuesAddLabels is a wrapper of https://godoc.org/github.com/google/go-github/github#IssuesService.AddLabelsToIssue
func (g *GitHub) IssuesAddLabels(ctx context.Context, number int, labels []string) ([]*github.Label, *github.Response, error) {
return g.Client.Issues.AddLabelsToIssue(ctx, g.owner, g.repo, number, labels)
}

// IssuesAddLabels is a wrapper of https://godoc.org/github.com/google/go-github/github#IssuesService.RemoveLabelForIssue
func (g *GitHub) IssuesRemoveLabel(ctx context.Context, number int, label string) (*github.Response, error) {
return g.Client.Issues.RemoveLabelForIssue(ctx, g.owner, g.repo, number, label)
}

// RepositoriesCreateComment is a wrapper of https://godoc.org/github.com/google/go-github/github#RepositoriesService.CreateComment
func (g *GitHub) RepositoriesCreateComment(ctx context.Context, sha string, comment *github.RepositoryComment) (*github.RepositoryComment, *github.Response, error) {
return g.Client.Repositories.CreateComment(ctx, g.owner, g.repo, sha, comment)
Expand Down
16 changes: 16 additions & 0 deletions notifier/github/github_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ type fakeAPI struct {
FakeIssuesCreateComment func(ctx context.Context, number int, comment *github.IssueComment) (*github.IssueComment, *github.Response, error)
FakeIssuesDeleteComment func(ctx context.Context, commentID int64) (*github.Response, error)
FakeIssuesListComments func(ctx context.Context, number int, opt *github.IssueListCommentsOptions) ([]*github.IssueComment, *github.Response, error)
FakeIssuesAddLabels func(ctx context.Context, number int, labels []string) ([]*github.Label, *github.Response, error)
FakeIssuesRemoveLabel func(ctx context.Context, number int, label string) (*github.Response, error)
FakeRepositoriesCreateComment func(ctx context.Context, sha string, comment *github.RepositoryComment) (*github.RepositoryComment, *github.Response, error)
FakeRepositoriesListCommits func(ctx context.Context, opt *github.CommitsListOptions) ([]*github.RepositoryCommit, *github.Response, error)
FakeRepositoriesGetCommit func(ctx context.Context, sha string) (*github.RepositoryCommit, *github.Response, error)
Expand All @@ -29,6 +31,14 @@ func (g *fakeAPI) IssuesListComments(ctx context.Context, number int, opt *githu
return g.FakeIssuesListComments(ctx, number, opt)
}

func (g *fakeAPI) IssuesAddLabels(ctx context.Context, number int, labels []string) ([]*github.Label, *github.Response, error) {
return g.FakeIssuesAddLabels(ctx, number, labels)
}

func (g *fakeAPI) IssuesRemoveLabel(ctx context.Context, number int, label string) (*github.Response, error) {
return g.FakeIssuesRemoveLabel(ctx, number, label)
}

func (g *fakeAPI) RepositoriesCreateComment(ctx context.Context, sha string, comment *github.RepositoryComment) (*github.RepositoryComment, *github.Response, error) {
return g.FakeRepositoriesCreateComment(ctx, sha, comment)
}
Expand Down Expand Up @@ -66,6 +76,12 @@ func newFakeAPI() fakeAPI {
}
return comments, nil, nil
},
FakeIssuesAddLabels: func(ctx context.Context, number int, labels []string) ([]*github.Label, *github.Response, error) {
return nil, nil, nil
},
FakeIssuesRemoveLabel: func(ctx context.Context, number int, label string) (*github.Response, error) {
return nil, nil
},
FakeRepositoriesCreateComment: func(ctx context.Context, sha string, comment *github.RepositoryComment) (*github.RepositoryComment, *github.Response, error) {
return &github.RepositoryComment{
ID: github.Int64(28427394),
Expand Down
25 changes: 25 additions & 0 deletions notifier/github/notify.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package github

import (
"context"
"github.com/mercari/tfnotify/terraform"
"net/http"
)

// NotifyService handles communication with the notification related
Expand Down Expand Up @@ -30,6 +32,29 @@ func (g *NotifyService) Notify(body string) (exit int, err error) {
return result.ExitCode, err
}
}
if cfg.PR.IsNumber() && cfg.NoChangesLabel != "" {
// Always attempt to remove the label first so that an IssueLabeled event is created
resp, err := g.client.API.IssuesRemoveLabel(
context.Background(),
cfg.PR.Number,
cfg.NoChangesLabel,
)
// Ignore 404 errors, which are from the PR not having the label
if err != nil && resp.StatusCode != http.StatusNotFound {
return result.ExitCode, err
}

if result.HasNoChanges {
_, _, err = g.client.API.IssuesAddLabels(
context.Background(),
cfg.PR.Number,
[]string{cfg.NoChangesLabel},
)
if err != nil {
return result.ExitCode, err
}
}
}
}

template.SetValue(terraform.CommonTemplate{
Expand Down
20 changes: 20 additions & 0 deletions notifier/github/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,26 @@ func TestNotifyNotify(t *testing.T) {
ok: true,
exitCode: 0,
},
{
// valid with no changes
// TODO(drlau): check that the label was actually added
config: Config{
Token: "token",
Owner: "owner",
Repo: "repo",
PR: PullRequest{
Revision: "",
Number: 1,
Message: "message",
},
Parser: terraform.NewPlanParser(),
Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate),
NoChangesLabel: "terraform/no-changes",
},
body: "No changes. Infrastructure is up-to-date.",
ok: true,
exitCode: 0,
},
{
// valid, contains destroy, but not to notify
config: Config{
Expand Down
29 changes: 17 additions & 12 deletions terraform/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,11 @@ type Parser interface {

// ParseResult represents the result of parsed terraform execution
type ParseResult struct {
Result string
HasDestroy bool
ExitCode int
Error error
Result string
HasDestroy bool
HasNoChanges bool
ExitCode int
Error error
}

// DefaultParser is a parser for terraform commands
Expand All @@ -31,9 +32,10 @@ type FmtParser struct {

// PlanParser is a parser for terraform plan
type PlanParser struct {
Pass *regexp.Regexp
Fail *regexp.Regexp
HasDestroy *regexp.Regexp
Pass *regexp.Regexp
Fail *regexp.Regexp
HasDestroy *regexp.Regexp
HasNoChanges *regexp.Regexp
}

// ApplyParser is a parser for terraform apply
Expand All @@ -60,7 +62,8 @@ func NewPlanParser() *PlanParser {
Pass: regexp.MustCompile(`(?m)^(Plan: \d|No changes.)`),
Fail: regexp.MustCompile(`(?m)^(Error: )`),
// "0 to destroy" should be treated as "no destroy"
HasDestroy: regexp.MustCompile(`(?m)([1-9][0-9]* to destroy.)`),
HasDestroy: regexp.MustCompile(`(?m)([1-9][0-9]* to destroy.)`),
HasNoChanges: regexp.MustCompile(`(?m)^(No changes. Infrastructure is up-to-date.)`),
}
}

Expand Down Expand Up @@ -122,12 +125,14 @@ func (p *PlanParser) Parse(body string) ParseResult {
}

hasDestroy := p.HasDestroy.MatchString(line)
hasNoChanges := p.HasNoChanges.MatchString(line)

return ParseResult{
Result: result,
HasDestroy: hasDestroy,
ExitCode: exitCode,
Error: nil,
Result: result,
HasDestroy: hasDestroy,
HasNoChanges: hasNoChanges,
ExitCode: exitCode,
Error: nil,
}
}

Expand Down
36 changes: 20 additions & 16 deletions terraform/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -307,20 +307,22 @@ func TestPlanParserParse(t *testing.T) {
name: "plan ok pattern",
body: planSuccessResult,
result: ParseResult{
Result: "Plan: 1 to add, 0 to change, 0 to destroy.",
HasDestroy: false,
ExitCode: 0,
Error: nil,
Result: "Plan: 1 to add, 0 to change, 0 to destroy.",
HasDestroy: false,
HasNoChanges: false,
ExitCode: 0,
Error: nil,
},
},
{
name: "no stdin",
body: "",
result: ParseResult{
Result: "",
HasDestroy: false,
ExitCode: 1,
Error: errors.New("cannot parse plan result"),
Result: "",
HasDestroy: false,
HasNoChanges: false,
ExitCode: 1,
Error: errors.New("cannot parse plan result"),
},
},
{
Expand All @@ -341,20 +343,22 @@ func TestPlanParserParse(t *testing.T) {
name: "plan no changes",
body: planNoChanges,
result: ParseResult{
Result: "No changes. Infrastructure is up-to-date.",
HasDestroy: false,
ExitCode: 0,
Error: nil,
Result: "No changes. Infrastructure is up-to-date.",
HasDestroy: false,
HasNoChanges: true,
ExitCode: 0,
Error: nil,
},
},
{
name: "plan has destroy",
body: planHasDestroy,
result: ParseResult{
Result: "Plan: 0 to add, 0 to change, 1 to destroy.",
HasDestroy: true,
ExitCode: 0,
Error: nil,
Result: "Plan: 0 to add, 0 to change, 1 to destroy.",
HasDestroy: true,
HasNoChanges: false,
ExitCode: 0,
Error: nil,
},
},
}
Expand Down

0 comments on commit 42a3e56

Please sign in to comment.