diff --git a/.circleci/config.yml b/.circleci/config.yml index 2225e0a16cf..a4bb2d67855 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -158,14 +158,18 @@ jobs: name: sha-sum packages command: 'go run build.go sha-dist' - run: - name: Build Grafana.com publisher + name: Build Grafana.com master publisher command: 'go build -o scripts/publish scripts/build/publish.go' + - run: + name: Build Grafana.com release publisher + command: 'cd scripts/build/release_publisher && go build -o release_publisher .' - persist_to_workspace: root: . paths: - dist/grafana* - scripts/*.sh - scripts/publish + - scripts/build/release_publisher/release_publisher build: docker: @@ -299,8 +303,8 @@ jobs: name: deploy to s3 command: 'aws s3 sync ./dist s3://$BUCKET_NAME/release' - run: - name: Trigger Windows build - command: './scripts/trigger_windows_build.sh ${APPVEYOR_TOKEN} ${CIRCLE_SHA1} release' + name: Deploy to Grafana.com + command: './scripts/build/publish.sh' workflows: version: 2 diff --git a/.gitignore b/.gitignore index 08525d92519..20e8fffb3b1 100644 --- a/.gitignore +++ b/.gitignore @@ -73,3 +73,5 @@ debug.test /devenv/bulk-dashboards/*.json /devenv/bulk_alerting_dashboards/*.json + +/scripts/build/release_publisher/release_publisher diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh new file mode 100755 index 00000000000..feaec604c0d --- /dev/null +++ b/scripts/build/publish.sh @@ -0,0 +1,14 @@ +#/bin/bash + +# no relation to publish.go + +# Right now we hack this in into the publish script. +# Eventually we might want to keep a list of all previous releases somewhere. +_releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-3-x/10244" +_whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-3/" + +./release_publisher/release_publisher \ + --wn ${_whatsNewUrl} \ + --rn ${_releaseNoteUrl} \ + --version ${CIRCLE_TAG} \ + --apikey ${GRAFANA_COM_API_KEY} diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go new file mode 100644 index 00000000000..fde4317bbb9 --- /dev/null +++ b/scripts/build/release_publisher/main.go @@ -0,0 +1,40 @@ +package main + +import ( + "flag" + "fmt" + "log" + "os" +) + +var baseUri string = "https://grafana.com/api" + +func main() { + var version string + var whatsNewUrl string + var releaseNotesUrl string + var dryRun bool + var apiKey string + + flag.StringVar(&version, "version", "", "Grafana version (ex: --version v5.2.0-beta1)") + flag.StringVar(&whatsNewUrl, "wn", "", "What's new url (ex: --wn http://docs.grafana.org/guides/whats-new-in-v5-2/)") + flag.StringVar(&releaseNotesUrl, "rn", "", "Grafana version (ex: --rn https://community.grafana.com/t/release-notes-v5-2-x/7894)") + flag.StringVar(&apiKey, "apikey", "", "Grafana.com API key (ex: --apikey ABCDEF)") + flag.BoolVar(&dryRun, "dry-run", false, "--dry-run") + flag.Parse() + + if len(os.Args) == 1 { + fmt.Println("Usage: go run publisher.go main.go --version --wn --rn --apikey --dry-run false") + fmt.Println("example: go run publisher.go main.go --version v5.2.0-beta2 --wn http://docs.grafana.org/guides/whats-new-in-v5-2/ --rn https://community.grafana.com/t/release-notes-v5-2-x/7894 --apikey ASDF123 --dry-run true") + os.Exit(1) + } + + if dryRun { + log.Println("Dry-run has been enabled.") + } + + p := publisher{apiKey: apiKey} + if err := p.doRelease(version, whatsNewUrl, releaseNotesUrl, dryRun); err != nil { + log.Fatalf("error: %v", err) + } +} diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go new file mode 100644 index 00000000000..60b60ca55f7 --- /dev/null +++ b/scripts/build/release_publisher/publisher.go @@ -0,0 +1,266 @@ +package main + +import ( + "bytes" + "encoding/json" + "fmt" + "io/ioutil" + "log" + "net/http" + "strings" + "time" +) + +type publisher struct { + apiKey string +} + +func (p *publisher) doRelease(version string, whatsNewUrl string, releaseNotesUrl string, dryRun bool) error { + currentRelease, err := newRelease(version, whatsNewUrl, releaseNotesUrl, buildArtifactConfigurations, getHttpContents{}) + if err != nil { + return err + } + + if dryRun { + relJson, err := json.Marshal(currentRelease) + if err != nil { + return err + } + log.Println(string(relJson)) + + for _, b := range currentRelease.Builds { + artifactJson, err := json.Marshal(b) + if err != nil { + return err + } + log.Println(string(artifactJson)) + } + } else { + if err := p.postRelease(currentRelease); err != nil { + return err + } + } + + return nil +} + +func (p *publisher) postRelease(r *release) error { + err := p.postRequest("/grafana/versions", r, fmt.Sprintf("Create Release %s", r.Version)) + if err != nil { + return err + } + err = p.postRequest("/grafana/versions/"+r.Version, r, fmt.Sprintf("Update Release %s", r.Version)) + if err != nil { + return err + } + for _, b := range r.Builds { + err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages", r.Version), b, fmt.Sprintf("Create Build %s %s", b.Os, b.Arch)) + if err != nil { + return err + } + err = p.postRequest(fmt.Sprintf("/grafana/versions/%s/packages/%s/%s", r.Version, b.Arch, b.Os), b, fmt.Sprintf("Update Build %s %s", b.Os, b.Arch)) + if err != nil { + return err + } + } + + return nil +} + +const baseArhiveUrl = "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana" + +type buildArtifact struct { + os string + arch string + urlPostfix string +} + +func (t buildArtifact) getUrl(version string, isBeta bool) string { + prefix := "-" + rhelReleaseExtra := "" + + if t.os == "deb" { + prefix = "_" + } + + if !isBeta && t.os == "rhel" { + rhelReleaseExtra = "-1" + } + + url := strings.Join([]string{baseArhiveUrl, prefix, version, rhelReleaseExtra, t.urlPostfix}, "") + return url +} + +var buildArtifactConfigurations = []buildArtifact{ + { + os: "deb", + arch: "arm64", + urlPostfix: "_arm64.deb", + }, + { + os: "rhel", + arch: "arm64", + urlPostfix: ".aarch64.rpm", + }, + { + os: "linux", + arch: "arm64", + urlPostfix: ".linux-arm64.tar.gz", + }, + { + os: "deb", + arch: "armv7", + urlPostfix: "_armhf.deb", + }, + { + os: "rhel", + arch: "armv7", + urlPostfix: ".armhfp.rpm", + }, + { + os: "linux", + arch: "armv7", + urlPostfix: ".linux-armv7.tar.gz", + }, + { + os: "darwin", + arch: "amd64", + urlPostfix: ".darwin-amd64.tar.gz", + }, + { + os: "deb", + arch: "amd64", + urlPostfix: "_amd64.deb", + }, + { + os: "rhel", + arch: "amd64", + urlPostfix: ".x86_64.rpm", + }, + { + os: "linux", + arch: "amd64", + urlPostfix: ".linux-amd64.tar.gz", + }, + { + os: "win", + arch: "amd64", + urlPostfix: ".windows-amd64.zip", + }, +} + +func newRelease(rawVersion string, whatsNewUrl string, releaseNotesUrl string, artifactConfigurations []buildArtifact, getter urlGetter) (*release, error) { + version := rawVersion[1:] + now := time.Now() + isBeta := strings.Contains(version, "beta") + + builds := []build{} + for _, ba := range artifactConfigurations { + sha256, err := getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(version, isBeta))) + if err != nil { + return nil, err + } + builds = append(builds, newBuild(ba, version, isBeta, sha256)) + } + + r := release{ + Version: version, + ReleaseDate: time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, time.Local), + Stable: !isBeta, + Beta: isBeta, + Nightly: false, + WhatsNewUrl: whatsNewUrl, + ReleaseNotesUrl: releaseNotesUrl, + Builds: builds, + } + return &r, nil +} + +func newBuild(ba buildArtifact, version string, isBeta bool, sha256 string) build { + return build{ + Os: ba.os, + Url: ba.getUrl(version, isBeta), + Sha256: sha256, + Arch: ba.arch, + } +} + +func (p *publisher) postRequest(url string, obj interface{}, desc string) error { + jsonBytes, err := json.Marshal(obj) + if err != nil { + return err + } + req, err := http.NewRequest(http.MethodPost, baseUri+url, bytes.NewReader(jsonBytes)) + if err != nil { + return err + } + req.Header.Add("Authorization", "Bearer "+p.apiKey) + req.Header.Add("Content-Type", "application/json") + + res, err := http.DefaultClient.Do(req) + if err != nil { + return err + } + + if res.StatusCode == http.StatusOK { + log.Printf("Action: %s \t OK", desc) + return nil + } + + if res.Body != nil { + defer res.Body.Close() + body, err := ioutil.ReadAll(res.Body) + if err != nil { + return err + } + + if strings.Contains(string(body), "already exists") || strings.Contains(string(body), "Nothing to update") { + log.Printf("Action: %s \t Already exists", desc) + } else { + log.Printf("Action: %s \t Failed - Status: %v", desc, res.Status) + log.Printf("Resp: %s", body) + log.Fatalf("Quiting") + } + } + + return nil +} + +type release struct { + Version string `json:"version"` + ReleaseDate time.Time `json:"releaseDate"` + Stable bool `json:"stable"` + Beta bool `json:"beta"` + Nightly bool `json:"nightly"` + WhatsNewUrl string `json:"whatsNewUrl"` + ReleaseNotesUrl string `json:"releaseNotesUrl"` + Builds []build `json:"-"` +} + +type build struct { + Os string `json:"os"` + Url string `json:"url"` + Sha256 string `json:"sha256"` + Arch string `json:"arch"` +} + +type urlGetter interface { + getContents(url string) (string, error) +} + +type getHttpContents struct{} + +func (getHttpContents) getContents(url string) (string, error) { + response, err := http.Get(url) + if err != nil { + return "", err + } + + defer response.Body.Close() + all, err := ioutil.ReadAll(response.Body) + if err != nil { + return "", err + } + + return string(all), nil +} diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go new file mode 100644 index 00000000000..9bc350e6a54 --- /dev/null +++ b/scripts/build/release_publisher/publisher_test.go @@ -0,0 +1,43 @@ +package main + +import "testing" + +func TestNewRelease(t *testing.T) { + versionIn := "v5.2.0-beta1" + expectedVersion := "5.2.0-beta1" + whatsNewUrl := "https://whatsnews.foo/" + relNotesUrl := "https://relnotes.foo/" + expectedArch := "amd64" + expectedOs := "linux" + buildArtifacts := []buildArtifact{{expectedOs, expectedArch, ".linux-amd64.tar.gz"}} + + rel, _ := newRelease(versionIn, whatsNewUrl, relNotesUrl, buildArtifacts, mockHttpGetter{}) + + if !rel.Beta || rel.Stable { + t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) + } + + if rel.Version != expectedVersion { + t.Errorf("Expected version to be %s, but it was %s.", expectedVersion, rel.Version) + } + + expectedBuilds := len(buildArtifacts) + if len(rel.Builds) != expectedBuilds { + t.Errorf("Expected %v builds, but got %v.", expectedBuilds, len(rel.Builds)) + } + + build := rel.Builds[0] + if build.Arch != expectedArch { + t.Errorf("Expected arch to be %v, but it was %v", expectedArch, build.Arch) + } + + if build.Os != expectedOs { + t.Errorf("Expected arch to be %v, but it was %v", expectedOs, build.Os) + } +} + +type mockHttpGetter struct{} + +func (mockHttpGetter) getContents(url string) (string, error) { + return url, nil +}