diff --git a/.drone.yml b/.drone.yml index 9d62987..e85dbce 100644 --- a/.drone.yml +++ b/.drone.yml @@ -1,39 +1,29 @@ -build: - image: golang:1.5 - environment: - - CGO_ENABLED=0 - commands: - - make deps - - make vet - - make build - - make test +workspace: + base: /go -publish: - coverage: +pipeline: + test: + image: golang:1.6 + environment: + - CGO_ENABLED=0 + commands: + - go vet + - go test -cover -coverprofile=coverage.out + - go build -ldflags "-s -w -X main.build=$DRONE_BUILD_NUMBER" -a -tags netgo + + latest: + image: docker + repo: plugins/github-release + tags: [ "latest", "1.0", "1" ] when: branch: master - docker: - username: $$DOCKER_USER - password: $$DOCKER_PASS - email: $$DOCKER_EMAIL - repo: plugins/drone-github-release - tag: latest - when: - branch: master - docker: - username: $$DOCKER_USER - password: $$DOCKER_PASS - email: $$DOCKER_EMAIL - repo: plugins/drone-github-release - tag: develop - when: - branch: develop + event: push plugin: name: GitHub Release desc: Publish files and artifacts to GitHub Releases type: publish - image: plugins/drone-github-release + image: plugins/github-release labels: - github - release diff --git a/.drone.yml.sig b/.drone.yml.sig new file mode 100644 index 0000000..57a6885 --- /dev/null +++ b/.drone.yml.sig @@ -0,0 +1 @@ +eyJhbGciOiJIUzI1NiJ9.d29ya3NwYWNlOgogIGJhc2U6IC9nbwoKcGlwZWxpbmU6CiAgdGVzdDoKICAgIGltYWdlOiBnb2xhbmc6MS42CiAgICBlbnZpcm9ubWVudDoKICAgICAgLSBDR09fRU5BQkxFRD0wCiAgICBjb21tYW5kczoKICAgICAgLSBnbyB2ZXQKICAgICAgLSBnbyB0ZXN0IC1jb3ZlciAtY292ZXJwcm9maWxlPWNvdmVyYWdlLm91dAogICAgICAtIGdvIGJ1aWxkIC1sZGZsYWdzICItcyAtdyAtWCBtYWluLmJ1aWxkPSREUk9ORV9CVUlMRF9OVU1CRVIiIC1hIC10YWdzIG5ldGdvCgogIGxhdGVzdDoKICAgIGltYWdlOiBkb2NrZXIKICAgIHJlcG86IHBsdWdpbnMvZ2l0aHViLXJlbGVhc2UKICAgIHRhZ3M6IFsgImxhdGVzdCIsICIxLjAiLCAiMSIgXQogICAgd2hlbjoKICAgICAgYnJhbmNoOiBtYXN0ZXIKICAgICAgZXZlbnQ6IHB1c2gKCnBsdWdpbjoKICBuYW1lOiBHaXRIdWIgUmVsZWFzZQogIGRlc2M6IFB1Ymxpc2ggZmlsZXMgYW5kIGFydGlmYWN0cyB0byBHaXRIdWIgUmVsZWFzZXMKICB0eXBlOiBwdWJsaXNoCiAgaW1hZ2U6IHBsdWdpbnMvZ2l0aHViLXJlbGVhc2UKICBsYWJlbHM6CiAgICAtIGdpdGh1YgogICAgLSByZWxlYXNlCg.5FZW1Auk18CljY9LW5P82r9RM_spPCYMYg82PFj2irM \ No newline at end of file diff --git a/DOCS.md b/DOCS.md index 4259020..07d11ce 100644 --- a/DOCS.md +++ b/DOCS.md @@ -1,30 +1,61 @@ -Use this plugin for publishing files and artifacts to GitHub releases. You -can override the default configuration with the following parameters: +Use this plugin for publishing files and artifacts to GitHub releases. Be aware +that you can use this plugin only for tags, GitHub doesn't support the release +of branches. -* `api_key` - GitHub oauth token with public_repo or repo permission -* `files` - Files to upload to GitHub Release, globs are allowed -* `file_exists` - What to do if an file asset already exists, supported values: **overwrite** (default), **skip** and **fail** -* `checksum` - Checksum takes hash methods to include in your GitHub release for the files specified. Supported hash methods include md5, sha1, sha256, sha512, adler32, and crc32. -* `draft` - create a draft release if set to true -* `base_url` - GitHub base URL, only required for GHE -* `upload_url` - GitHub upload URL, only required for GHE +## Config -Sample configuration: +The following parameters are used to configure the plugin: -```yaml -publish: - github_release: - api_key: my_github_api_key - files: dist/* - checksum: sha1 +* **api_key** - GitHub oauth token with public_repo or repo permission +* **files** - files to upload to GitHub Release, globs are allowed +* **file_exists** - what to do if an file asset already exists, supported values: **overwrite** (default), **skip** and **fail** +* **checksum** - checksum takes hash methods to include in your GitHub release for the files specified. Supported hash methods include md5, sha1, sha256, sha512, adler32, and crc32. +* **draft** - create a draft release if set to true +* **base_url** - GitHub base URL, only required for GHE +* **upload_url** - GitHub upload URL, only required for GHE + +The following secret values can be set to configure the plugin. + +* **GITHUB_RELEASE_API_KEY** - corresponds to **api_key** +* **GITHUB_RELEASE_BASE_URL** - corresponds to **base_url** +* **GITHUB_RELEASE_UPLOAD_URL** - corresponds to **upload_url** + +It is highly recommended to put the **GITHUB_RELEASE_API_KEY** into a secret so +it is not exposed to users. This can be done using the drone-cli. + +```bash +drone secret add --image=plugins/github-release \ + octocat/hello-world GITHUB_RELEASE_API_KEY my_github_api_key ``` -or +Then sign the YAML file after all secrets are added. + +```bash +drone sign octocat/hello-world +``` + +See [secrets](http://readme.drone.io/0.5/usage/secrets/) for additional +information on secrets + +## Examples + +The following is a sample configuration in your .drone.yml file: ```yaml -publish: +pipeline: github_release: - api_key: my_github_api_key + image: plugins/github-release + files: dist/* + when: + event: tag +``` + +An example for generating checksums and upload additional files: + +```yaml +pipeline: + github_release: + image: plugins/github-release files: - dist/* - bin/binary.exe @@ -35,4 +66,6 @@ publish: - sha512 - adler32 - crc32 + when: + event: tag ``` diff --git a/Dockerfile b/Dockerfile index bebbe5d..aa86bb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,9 +1,4 @@ -# Docker image for the Drone GitHub Release plugin -# -# cd $GOPATH/src/github.com/drone-plugins/drone-github-release -# make deps build docker - -FROM alpine:3.3 +FROM alpine:3.4 RUN apk update && \ apk add \ diff --git a/README.md b/README.md index 56ff55f..541f7c0 100644 --- a/README.md +++ b/README.md @@ -1,117 +1,53 @@ # drone-github-release [![Build Status](http://beta.drone.io/api/badges/drone-plugins/drone-github-release/status.svg)](http://beta.drone.io/drone-plugins/drone-github-release) -[![Coverage Status](https://aircover.co/badges/drone-plugins/drone-github-release/coverage.svg)](https://aircover.co/drone-plugins/drone-github-release) -[![](https://badge.imagelayers.io/plugins/drone-github-release:latest.svg)](https://imagelayers.io/?images=plugins/drone-github-release:latest 'Get your own badge on imagelayers.io') +[![Go Doc](https://godoc.org/github.com/drone-plugins/drone-github-release?status.svg)](http://godoc.org/github.com/drone-plugins/drone-github-release) +[![Go Report](https://goreportcard.com/badge/github.com/drone-plugins/drone-github-release)](https://goreportcard.com/report/github.com/drone-plugins/drone-github-release) +[![Join the chat at https://gitter.im/drone/drone](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/drone/drone) -Drone plugin to publish files and artifacts to GitHub Release. For the usage information and a listing of the available options please take a look at [the docs](DOCS.md). +Drone plugin to publish files and artifacts to GitHub Release. For the usage +information and a listing of the available options please take a look at +[the docs](DOCS.md). -## Binary +## Build -Build the binary using `make`: +Build the binary with the following commands: ``` -make deps build -``` - -### Example - -```sh -./drone-github-release < 0 { - var err error - files, err = writeChecksums(files, vargs.Checksum.Slice()) - if err != nil { - fmt.Println(err) - os.Exit(1) - } - } - - baseURL, err := url.Parse(vargs.BaseURL) - if err != nil { - fmt.Printf("Failed to parse base URL\n") - os.Exit(1) - } - - uploadURL, err := url.Parse(vargs.UploadURL) - if err != nil { - fmt.Printf("Failed to parse upload URL\n") - os.Exit(1) - } - - ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: vargs.APIKey}) - tc := oauth2.NewClient(oauth2.NoContext, ts) - - client := github.NewClient(tc) - client.BaseURL = baseURL - client.UploadURL = uploadURL - - rc := releaseClient{ - Client: client, - Owner: repo.Owner, - Repo: repo.Name, - Tag: filepath.Base(build.Ref), - Draft: vargs.Draft, - FileExists: vargs.FileExists, - } - - release, err := rc.buildRelease() - if err != nil { - fmt.Println(err) - os.Exit(1) - } - - if err := rc.uploadFiles(*release.ID, files); err != nil { - fmt.Println(err) - os.Exit(1) + if err := app.Run(os.Args); err != nil { + logrus.Fatal(err) } } -var fileExistsValues = map[string]bool{ - "overwrite": true, - "fail": true, - "skip": true, -} - -// Release holds ties the drone env data and github client together. -type releaseClient struct { - *github.Client - Owner string - Repo string - Tag string - Draft bool - FileExists string -} - -func (rc *releaseClient) buildRelease() (*github.RepositoryRelease, error) { - - // first attempt to get a release by that tag - release, err := rc.getRelease() - if err != nil && release == nil { - fmt.Println(err) - } else if release != nil { - return release, nil - } - - // if no release was found by that tag, create a new one - release, err = rc.newRelease() - if err != nil { - return nil, fmt.Errorf("Failed to retrieve or create a release: %s", err) - } - - return release, nil -} - -func (rc *releaseClient) getRelease() (*github.RepositoryRelease, error) { - release, _, err := rc.Client.Repositories.GetReleaseByTag(rc.Owner, rc.Repo, rc.Tag) - if err != nil { - return nil, fmt.Errorf("Release %s not found", rc.Tag) - } - - fmt.Printf("Successfully retrieved %s release\n", rc.Tag) - return release, nil -} - -func (rc *releaseClient) newRelease() (*github.RepositoryRelease, error) { - rr := &github.RepositoryRelease{ - TagName: github.String(rc.Tag), - Draft: &rc.Draft, - } - release, _, err := rc.Client.Repositories.CreateRelease(rc.Owner, rc.Repo, rr) - if err != nil { - return nil, fmt.Errorf("Failed to create release: %s", err) - } - - fmt.Printf("Successfully created %s release\n", rc.Tag) - return release, nil -} - -func (rc *releaseClient) uploadFiles(id int, files []string) error { - assets, _, err := rc.Client.Repositories.ListReleaseAssets(rc.Owner, rc.Repo, id, &github.ListOptions{}) - if err != nil { - return fmt.Errorf("Failed to fetch existing assets: %s", err) - } - - var uploadFiles []string -files: - for _, file := range files { - for _, asset := range assets { - if *asset.Name == path.Base(file) { - switch rc.FileExists { - case "overwrite": - // do nothing - case "fail": - return fmt.Errorf("Asset file %s already exists", path.Base(file)) - case "skip": - fmt.Printf("Skipping pre-existing %s artifact\n", *asset.Name) - continue files - default: - return fmt.Errorf("Internal error, unkown file_exist value %s", rc.FileExists) - } - } - } - uploadFiles = append(uploadFiles, file) - } - - for _, file := range uploadFiles { - handle, err := os.Open(file) - if err != nil { - return fmt.Errorf("Failed to read %s artifact: %s", file, err) - } - - for _, asset := range assets { - if *asset.Name == path.Base(file) { - if _, err := rc.Client.Repositories.DeleteReleaseAsset(rc.Owner, rc.Repo, *asset.ID); err != nil { - return fmt.Errorf("Failed to delete %s artifact: %s", file, err) - } - fmt.Printf("Successfully deleted old %s artifact\n", *asset.Name) - } - } - - uo := &github.UploadOptions{Name: path.Base(file)} - if _, _, err = rc.Client.Repositories.UploadReleaseAsset(rc.Owner, rc.Repo, id, uo, handle); err != nil { - return fmt.Errorf("Failed to upload %s artifact: %s", file, err) - } - - fmt.Printf("Successfully uploaded %s artifact\n", file) - } - - return nil -} - -func writeChecksums(files, methods []string) ([]string, error) { - - checksums := make(map[string][]string) - for _, method := range methods { - for _, file := range files { - handle, err := os.Open(file) - if err != nil { - return nil, fmt.Errorf("Failed to read %s artifact: %s", file, err) - } - - hash, err := checksum(handle, method) - if err != nil { - return nil, err - } - - checksums[method] = append(checksums[method], hash, file) - } - } - - for method, results := range checksums { - filename := method + "sum.txt" - f, err := os.Create(filename) - if err != nil { - return nil, err - } - - for i := 0; i < len(results); i += 2 { - hash := results[i] - file := results[i+1] - if _, err := f.WriteString(fmt.Sprintf("%s %s\n", hash, file)); err != nil { - return nil, err - } - } - files = append(files, filename) - } - return files, nil +func run(c *cli.Context) error { + if c.String("env-file") != "" { + _ = godotenv.Load(c.String("env-file")) + } + + plugin := Plugin{ + Repo: Repo{ + Owner: c.String("repo.owner"), + Name: c.String("repo.name"), + }, + Build: Build{ + Event: c.String("build.event"), + }, + Commit: Commit{ + Ref: c.String("commit.sha"), + }, + Config: Config{ + APIKey: c.String("api-key"), + Files: c.StringSlice("api-key"), + FileExists: c.String("file-exists"), + Checksum: c.StringSlice("checksum"), + Draft: c.Bool("draft"), + BaseURL: c.String("base-url"), + UploadURL: c.String("upload-url"), + }, + } + + return plugin.Exec() } diff --git a/plugin.go b/plugin.go new file mode 100644 index 0000000..c3282f4 --- /dev/null +++ b/plugin.go @@ -0,0 +1,134 @@ +package main + +import ( + "fmt" + "net/url" + "path/filepath" + "strings" + + "github.com/google/go-github/github" + "golang.org/x/oauth2" +) + +type ( + Repo struct { + Owner string + Name string + } + + Build struct { + Event string + } + + Commit struct { + Ref string + } + + Config struct { + APIKey string + Files []string + FileExists string + Checksum []string + Draft bool + BaseURL string + UploadURL string + } + + Plugin struct { + Repo Repo + Build Build + Commit Commit + Config Config + } +) + +func (p Plugin) Exec() error { + var ( + files []string + ) + + if p.Build.Event != "tag" { + return fmt.Errorf("The GitHub Release plugin is only available for tags") + } + + if p.Config.APIKey == "" { + return fmt.Errorf("You must provide an API key") + } + + if !fileExistsValues[p.Config.FileExists] { + return fmt.Errorf("Invalid value for file_exists") + } + + if !strings.HasSuffix(p.Config.BaseURL, "/") { + p.Config.BaseURL = p.Config.BaseURL + "/" + } + + if !strings.HasSuffix(p.Config.UploadURL, "/") { + p.Config.UploadURL = p.Config.UploadURL + "/" + } + + for _, glob := range p.Config.Files { + globed, err := filepath.Glob(glob) + + if err != nil { + return fmt.Errorf("Failed to glob %s. %s", glob, err) + } + + if globed != nil { + files = append(files, globed...) + } + } + + if len(p.Config.Checksum) > 0 { + var ( + err error + ) + + files, err = writeChecksums(files, p.Config.Checksum) + + if err != nil { + return fmt.Errorf("Failed to write checksums. %s", err) + } + } + + baseURL, err := url.Parse(p.Config.BaseURL) + + if err != nil { + return fmt.Errorf("Failed to parse base URL. %s", err) + } + + uploadURL, err := url.Parse(p.Config.UploadURL) + + if err != nil { + return fmt.Errorf("Failed to parse upload URL. %s", err) + } + + ts := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: p.Config.APIKey}) + tc := oauth2.NewClient(oauth2.NoContext, ts) + + client := github.NewClient(tc) + + client.BaseURL = baseURL + client.UploadURL = uploadURL + + rc := releaseClient{ + Client: client, + Owner: p.Repo.Owner, + Repo: p.Repo.Name, + Tag: filepath.Base(p.Commit.Ref), + Draft: p.Config.Draft, + FileExists: p.Config.FileExists, + } + + release, err := rc.buildRelease() + + if err != nil { + return fmt.Errorf("Failed to create the release. %s", err) + } + + if err := rc.uploadFiles(*release.ID, files); err != nil { + return fmt.Errorf("Failed to upload the files. %s", err) + } + + return nil +} diff --git a/release.go b/release.go new file mode 100644 index 0000000..cd8c833 --- /dev/null +++ b/release.go @@ -0,0 +1,125 @@ +package main + +import ( + "fmt" + "os" + "path" + + "github.com/google/go-github/github" +) + +// Release holds ties the drone env data and github client together. +type releaseClient struct { + *github.Client + Owner string + Repo string + Tag string + Draft bool + FileExists string +} + +func (rc *releaseClient) buildRelease() (*github.RepositoryRelease, error) { + // first attempt to get a release by that tag + release, err := rc.getRelease() + + if err != nil && release == nil { + fmt.Println(err) + } else if release != nil { + return release, nil + } + + // if no release was found by that tag, create a new one + release, err = rc.newRelease() + + if err != nil { + return nil, fmt.Errorf("Failed to retrieve or create a release: %s", err) + } + + return release, nil +} + +func (rc *releaseClient) getRelease() (*github.RepositoryRelease, error) { + release, _, err := rc.Client.Repositories.GetReleaseByTag(rc.Owner, rc.Repo, rc.Tag) + + if err != nil { + return nil, fmt.Errorf("Release %s not found", rc.Tag) + } + + fmt.Printf("Successfully retrieved %s release\n", rc.Tag) + return release, nil +} + +func (rc *releaseClient) newRelease() (*github.RepositoryRelease, error) { + rr := &github.RepositoryRelease{ + TagName: github.String(rc.Tag), + Draft: &rc.Draft, + } + + release, _, err := rc.Client.Repositories.CreateRelease(rc.Owner, rc.Repo, rr) + + if err != nil { + return nil, fmt.Errorf("Failed to create release: %s", err) + } + + fmt.Printf("Successfully created %s release\n", rc.Tag) + return release, nil +} + +func (rc *releaseClient) uploadFiles(id int, files []string) error { + assets, _, err := rc.Client.Repositories.ListReleaseAssets(rc.Owner, rc.Repo, id, &github.ListOptions{}) + + if err != nil { + return fmt.Errorf("Failed to fetch existing assets: %s", err) + } + + var uploadFiles []string + +files: + for _, file := range files { + for _, asset := range assets { + if *asset.Name == path.Base(file) { + switch rc.FileExists { + case "overwrite": + // do nothing + case "fail": + return fmt.Errorf("Asset file %s already exists", path.Base(file)) + case "skip": + fmt.Printf("Skipping pre-existing %s artifact\n", *asset.Name) + continue files + default: + return fmt.Errorf("Internal error, unkown file_exist value %s", rc.FileExists) + } + } + } + + uploadFiles = append(uploadFiles, file) + } + + for _, file := range uploadFiles { + handle, err := os.Open(file) + + if err != nil { + return fmt.Errorf("Failed to read %s artifact: %s", file, err) + } + + for _, asset := range assets { + if *asset.Name == path.Base(file) { + if _, err := rc.Client.Repositories.DeleteReleaseAsset(rc.Owner, rc.Repo, *asset.ID); err != nil { + return fmt.Errorf("Failed to delete %s artifact: %s", file, err) + } + + fmt.Printf("Successfully deleted old %s artifact\n", *asset.Name) + } + } + + uo := &github.UploadOptions{Name: path.Base(file)} + + if _, _, err = rc.Client.Repositories.UploadReleaseAsset(rc.Owner, rc.Repo, id, uo, handle); err != nil { + return fmt.Errorf("Failed to upload %s artifact: %s", file, err) + } + + fmt.Printf("Successfully uploaded %s artifact\n", file) + } + + return nil +} diff --git a/types.go b/types.go deleted file mode 100644 index 8deee02..0000000 --- a/types.go +++ /dev/null @@ -1,14 +0,0 @@ -package main - -import "github.com/drone/drone-go/drone" - -// Params are the parameters that the GitHub Release plugin can parse. -type Params struct { - BaseURL string `json:"base_url"` - UploadURL string `json:"upload_url"` - APIKey string `json:"api_key"` - Files drone.StringSlice `json:"files"` - Checksum drone.StringSlice `json:"checksum"` - Draft bool `json:"draft"` - FileExists string `json:"file_exists"` -} diff --git a/utils.go b/utils.go new file mode 100644 index 0000000..794b0a5 --- /dev/null +++ b/utils.go @@ -0,0 +1,103 @@ +package main + +import ( + "crypto/md5" + "crypto/sha1" + "crypto/sha256" + "crypto/sha512" + "fmt" + "hash/adler32" + "hash/crc32" + "io" + "io/ioutil" + "os" + "os/exec" + "strconv" + "strings" +) + +var ( + fileExistsValues = map[string]bool{ + "overwrite": true, + "fail": true, + "skip": true, + } +) + +func execute(cmd *exec.Cmd) error { + fmt.Println("+", strings.Join(cmd.Args, " ")) + + cmd.Stderr = os.Stderr + cmd.Stdin = os.Stdin + + return cmd.Run() +} + +func checksum(r io.Reader, method string) (string, error) { + b, err := ioutil.ReadAll(r) + + if err != nil { + return "", err + } + + switch method { + case "md5": + return fmt.Sprintf("%x", md5.Sum(b)), nil + case "sha1": + return fmt.Sprintf("%x", sha1.Sum(b)), nil + case "sha256": + return fmt.Sprintf("%x", sha256.Sum256(b)), nil + case "sha512": + return fmt.Sprintf("%x", sha512.Sum512(b)), nil + case "adler32": + return strconv.FormatUint(uint64(adler32.Checksum(b)), 10), nil + case "crc32": + return strconv.FormatUint(uint64(crc32.ChecksumIEEE(b)), 10), nil + } + + return "", fmt.Errorf("Hashing method %s is not supported", method) +} + +func writeChecksums(files, methods []string) ([]string, error) { + checksums := make(map[string][]string) + + for _, method := range methods { + for _, file := range files { + handle, err := os.Open(file) + + if err != nil { + return nil, fmt.Errorf("Failed to read %s artifact: %s", file, err) + } + + hash, err := checksum(handle, method) + + if err != nil { + return nil, err + } + + checksums[method] = append(checksums[method], hash, file) + } + } + + for method, results := range checksums { + filename := method + "sum.txt" + f, err := os.Create(filename) + + if err != nil { + return nil, err + } + + for i := 0; i < len(results); i += 2 { + hash := results[i] + file := results[i+1] + + if _, err := f.WriteString(fmt.Sprintf("%s %s\n", hash, file)); err != nil { + return nil, err + } + } + + files = append(files, filename) + } + + return files, nil +}