diff --git a/plugin/notify/flowdock.go b/plugin/notify/flowdock.go new file mode 100644 index 000000000..27c81e7d7 --- /dev/null +++ b/plugin/notify/flowdock.go @@ -0,0 +1,84 @@ +package notify + +import ( + "fmt" + "strings" + + "github.com/drone/drone/shared/model" + "github.com/stvp/flowdock" +) + +const ( + flowdockStartedSubject = "Building %s (%s)" + flowdockSuccessSubject = "Build: %s (%s) is SUCCESS" + flowdockFailureSubject = "Build: %s (%s) is FAILED" + flowdockMessage = "

%s

\nBuild: %s
\nResult: %s
\nAuthor: %s
Commit: %s
\nRepository Url: %s" + flowdockBuildOkEmail = "build+ok@flowdock.com" + flowdockBuildFailEmail = "build+fail@flowdock.com" +) + +type Flowdock struct { + Token string `yaml:"token,omitempty"` + Source string `yaml:"source,omitempty"` + Tags string `yaml:"tags,omitempty"` + Started bool `yaml:"on_started,omitempty"` + Success bool `yaml:"on_success,omitempty"` + Failure bool `yaml:"on_failure,omitempty"` +} + +func (f *Flowdock) Send(context *model.Request) error { + switch { + case context.Commit.Status == "Started" && f.Started: + return f.sendStarted(context) + case context.Commit.Status == "Success" && f.Success: + return f.sendSuccess(context) + case context.Commit.Status == "Failure" && f.Failure: + return f.sendFailure(context) + } + + return nil +} + +func (f *Flowdock) getBuildUrl(context *model.Request) string { + return fmt.Sprintf("%s/%s/%s/%s/%s/%s", context.Host, context.Repo.Host, context.Repo.Owner, context.Repo.Name, context.Commit.Branch, context.Commit.Sha) +} + +func (f *Flowdock) getRepoUrl(context *model.Request) string { + return fmt.Sprintf("%s/%s/%s/%s", context.Host, context.Repo.Host, context.Repo.Owner, context.Repo.Name) +} + +func (f *Flowdock) getMessage(context *model.Request) string { + buildUrl := fmt.Sprintf("%s", f.getBuildUrl(context), context.Commit.ShaShort()) + return fmt.Sprintf(flowdockMessage, context.Repo.Name, buildUrl, context.Commit.Status, context.Commit.Author, context.Commit.Message, f.getRepoUrl(context)) +} + +func (f *Flowdock) sendStarted(context *model.Request) error { + fromAddress := context.Commit.Author + subject := fmt.Sprintf(flowdockStartedSubject, context.Repo.Name, context.Commit.Branch) + msg := f.getMessage(context) + tags := strings.Split(f.Tags, ",") + return f.send(fromAddress, subject, msg, tags) +} + +func (f *Flowdock) sendFailure(context *model.Request) error { + fromAddress := flowdockBuildFailEmail + tags := strings.Split(f.Tags, ",") + subject := fmt.Sprintf(flowdockFailureSubject, context.Repo.Name, context.Commit.Branch) + msg := f.getMessage(context) + return f.send(fromAddress, subject, msg, tags) +} + +func (f *Flowdock) sendSuccess(context *model.Request) error { + fromAddress := flowdockBuildOkEmail + tags := strings.Split(f.Tags, ",") + subject := fmt.Sprintf(flowdockSuccessSubject, context.Repo.Name, context.Commit.Branch) + msg := f.getMessage(context) + return f.send(fromAddress, subject, msg, tags) +} + +// helper function to send Flowdock requests +func (f *Flowdock) send(fromAddress, subject, message string, tags []string) error { + c := flowdock.Client{Token: f.Token, Source: f.Source, FromName: "drone.io", FromAddress: fromAddress, Tags: tags} + go c.Inbox(subject, message) + return nil +} diff --git a/plugin/notify/notification.go b/plugin/notify/notification.go index dab57f43d..d3b2de6ae 100644 --- a/plugin/notify/notification.go +++ b/plugin/notify/notification.go @@ -21,12 +21,13 @@ type Sender interface { // for notifying a user, or group of users, // when their Build has completed. type Notification struct { - Email *email.Email `yaml:"email,omitempty"` - Webhook *webhook.Webhook `yaml:"webhook,omitempty"` - Hipchat *Hipchat `yaml:"hipchat,omitempty"` - Irc *irc.IRC `yaml:"irc,omitempty"` - Slack *Slack `yaml:"slack,omitempty"` - Gitter *Gitter `yaml:"gitter,omitempty"` + Email *email.Email `yaml:"email,omitempty"` + Webhook *webhook.Webhook `yaml:"webhook,omitempty"` + Hipchat *Hipchat `yaml:"hipchat,omitempty"` + Irc *irc.IRC `yaml:"irc,omitempty"` + Slack *Slack `yaml:"slack,omitempty"` + Gitter *Gitter `yaml:"gitter,omitempty"` + Flowdock *Flowdock `yaml:"flowdock,omitempty"` GitHub github.GitHub `yaml:"--"` } @@ -80,6 +81,14 @@ func (n *Notification) Send(context *model.Request) error { } } + // send gitter notifications + if n.Flowdock != nil { + err := n.Flowdock.Send(context) + if err != nil { + log.Println(err) + } + } + // send email notifications // TODO (bradrydzewski) need to improve this code githubStatus := new(github.GitHub) diff --git a/plugin/publish/github.go b/plugin/publish/github.go new file mode 100644 index 000000000..9e98ea177 --- /dev/null +++ b/plugin/publish/github.go @@ -0,0 +1,131 @@ +package publish + +import ( + "fmt" + "strings" + + "github.com/drone/drone/plugin/condition" + "github.com/drone/drone/shared/build/buildfile" +) + +import () + +type Github struct { + // Script is an optional list of commands to run to prepare for a release. + Script []string `yaml:"script"` + + // Artifacts is a list of files or directories to release. + Artifacts []string `yaml:"artifacts"` + + // Tag is the name of the tag to create for this release. + Tag string `yaml:"tag"` + + // Name is the name of the release. Defaults to tag. + Name string `yaml:"name"` + + // Description describes the release. Defaults to empty string. + Description string `yaml:"description"` + + // Draft is an identifier on a Github release. + Draft bool `yaml:"draft"` + + // Prerelease is an identifier on a Github release. + Prerelease bool `yaml:"prerelease"` + + // Token is the Github token to use when publishing the release. + Token string `yaml:"token"` + + // User is the Github user for the repository you'd like to publish to. + User string `yaml:"user"` + + // Repo is the name of the Github repostiory you like to publish to. + Repo string `yaml:"repo"` + + Condition *condition.Condition `yaml:"when,omitempty"` +} + +// Write adds commands to run that will publish a Github release. +func (g *Github) Write(f *buildfile.Buildfile) { + if len(g.Artifacts) == 0 || g.Tag == "" || g.Token == "" || g.User == "" || g.Repo == "" { + f.WriteCmdSilent(`echo -e "Github Plugin: Missing argument(s)"\n\n`) + if len(g.Artifacts) == 0 { + f.WriteCmdSilent(`echo -e "\tartifacts not defined in yaml config" && false`) + } + if g.Tag == "" { + f.WriteCmdSilent(`echo -e "\ttag not defined in yaml config" && false`) + } + if g.Token == "" { + f.WriteCmdSilent(`echo -e "\ttoken not defined in yaml config" && false`) + } + if g.User == "" { + f.WriteCmdSilent(`echo -e "\tuser not defined in yaml config" && false`) + } + if g.Repo == "" { + f.WriteCmdSilent(`echo -e "\trepo not defined in yaml config" && false`) + } + return + } + + // Default name is tag + if g.Name == "" { + g.Name = g.Tag + } + + for _, cmd := range g.Script { + f.WriteCmd(cmd) + } + + f.WriteEnv("GITHUB_TOKEN", g.Token) + + // Install github-release + f.WriteCmd("curl -L -o /tmp/github-release.tar.bz2 https://github.com/aktau/github-release/releases/download/v0.5.2/linux-amd64-github-release.tar.bz2") + f.WriteCmd("tar jxf /tmp/github-release.tar.bz2 -C /tmp/ && sudo mv /tmp/bin/linux/amd64/github-release /usr/local/bin/github-release") + + // Create the release. Ignore 422 errors, which indicate the tag has already been created. + // Doing otherwise would create the expectation that every commit should be tagged and released, + // which is not the norm. + draftStr := "" + if g.Draft { + draftStr = "--draft" + } + prereleaseStr := "" + if g.Prerelease { + prereleaseStr = "--pre-release" + } + f.WriteCmd(fmt.Sprintf(` +result=$(github-release release -u %s -r %s -t %s -n "%s" -d "%s" %s %s || true) +if [[ $result == *422* ]]; then + echo -e "Release already exists for this tag."; + exit 0 +elif [[ $result == "" ]]; then + echo -e "Release created."; +else + echo -e "Error creating release: $result" + exit 1 +fi +`, g.User, g.Repo, g.Tag, g.Name, g.Description, draftStr, prereleaseStr)) + + // Upload files + artifactStr := strings.Join(g.Artifacts, " ") + f.WriteCmd(fmt.Sprintf(` +for f in %s; do + # treat directories and files differently + if [ -d $f ]; then + for ff in $(ls $f); do + echo -e "uploading $ff" + github-release upload -u %s -r %s -t %s -n $ff -f $f/$ff + done + elif [ -f $f ]; then + echo -e "uploading $f" + github-release upload -u %s -r %s -t %s -n $f -f $f + else + echo -e "$f is not a file or directory" + exit 1 + fi +done +`, artifactStr, g.User, g.Repo, g.Tag, g.User, g.Repo, g.Tag)) +} + +func (g *Github) GetCondition() *condition.Condition { + return g.Condition +} diff --git a/plugin/publish/github_test.go b/plugin/publish/github_test.go new file mode 100644 index 000000000..33e995b44 --- /dev/null +++ b/plugin/publish/github_test.go @@ -0,0 +1,103 @@ +package publish + +import ( + "fmt" + "strings" + "testing" + + "gopkg.in/v1/yaml" +) + +var validcfg = map[string]interface{}{ + "artifacts": []string{"release/"}, + "tag": "v1.0", + "token": "github-token", + "user": "drone", + "repo": "drone", +} + +func buildfileForConfig(config map[string]interface{}) (string, error) { + yml, err := yaml.Marshal(map[string]interface{}{ + "publish": config, + }) + if err != nil { + return "", err + } + return setUpWithDrone(string(yml)) +} + +func TestRequiredConfig(t *testing.T) { + for _, required := range []string{"artifacts", "tag", "token", "user", "repo"} { + invalidcfg := make(map[string]interface{}) + for k, v := range validcfg { + if k != required { + invalidcfg[k] = v + } + } + buildfilestr, err := buildfileForConfig(map[string]interface{}{"github": invalidcfg}) + if err != nil { + t.Fatal(err) + } + contains := fmt.Sprintf("%s not defined", required) + if !strings.Contains(buildfilestr, contains) { + t.Fatalf("Expected buildfile to contain error '%s': %s", contains, buildfilestr) + } + } +} + +func TestScript(t *testing.T) { + cmd := "echo run me!" + scriptcfg := make(map[string]interface{}) + scriptcfg["script"] = []string{cmd} + for k, v := range validcfg { + scriptcfg[k] = v + } + buildfilestr, err := buildfileForConfig(map[string]interface{}{"github": scriptcfg}) + if err != nil { + t.Fatal(err) + } + if !strings.Contains(buildfilestr, cmd) { + t.Fatalf("Expected buildfile to contain command '%s': %s", cmd, buildfilestr) + } +} + +func TestDefaultBehavior(t *testing.T) { + buildfilestr, err := buildfileForConfig(map[string]interface{}{"github": validcfg}) + if err != nil { + t.Fatal(err) + } + defaultname := fmt.Sprintf(`-n "%s"`, validcfg["tag"].(string)) + if !strings.Contains(buildfilestr, defaultname) { + t.Fatalf("Expected buildfile to contain name default to tag '%s': %s", defaultname, buildfilestr) + } + if strings.Contains(buildfilestr, "--draft") { + t.Fatalf("Should not create a draft release by default: %s", buildfilestr) + } + if strings.Contains(buildfilestr, "--pre-release") { + t.Fatalf("Should not create a pre-release release by default: %s", buildfilestr) + } + if !strings.Contains(buildfilestr, "github-release release") { + t.Fatalf("Should create a release: %s", buildfilestr) + } + if !strings.Contains(buildfilestr, "github-release upload") { + t.Fatalf("Should upload a file: %s", buildfilestr) + } +} + +func TestOpts(t *testing.T) { + optscfg := make(map[string]interface{}) + optscfg["draft"] = true + optscfg["prerelease"] = true + for k, v := range validcfg { + optscfg[k] = v + } + buildfilestr, err := buildfileForConfig(map[string]interface{}{"github": optscfg}) + if err != nil { + t.Fatal(err) + } + for _, flag := range []string{"--draft", "--pre-release"} { + if !strings.Contains(buildfilestr, flag) { + t.Fatalf("Expected buildfile to contain flag '%s': %s", flag, buildfilestr) + } + } +} diff --git a/plugin/publish/publish.go b/plugin/publish/publish.go index 0b82a2378..19ba9e78a 100644 --- a/plugin/publish/publish.go +++ b/plugin/publish/publish.go @@ -16,6 +16,7 @@ type Publish struct { PyPI *PyPI `yaml:"pypi,omitempty"` NPM *npm.NPM `yaml:"npm,omitempty"` Docker *Docker `yaml:"docker,omitempty"` + Github *Github `yaml:"github,omitempty"` } func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) { @@ -39,6 +40,11 @@ func (p *Publish) Write(f *buildfile.Buildfile, r *repo.Repo) { p.NPM.Write(f) } + // Github + if p.Github != nil && match(p.Github.GetCondition(), r) { + p.Github.Write(f) + } + // Docker if p.Docker != nil && match(p.Docker.GetCondition(), r) { p.Docker.Write(f)