Переглянути джерело

build: automatically publish releases to grafana.com.

bergquist 7 роки тому
батько
коміт
add6cee742

+ 7 - 3
.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

+ 2 - 0
.gitignore

@@ -73,3 +73,5 @@ debug.test
 
 /devenv/bulk-dashboards/*.json
 /devenv/bulk_alerting_dashboards/*.json
+
+/scripts/build/release_publisher/release_publisher

+ 14 - 0
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} 

+ 40 - 0
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 <v> --wn <what's new url> --rn <release notes url> --apikey <api key> --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)
+	}
+}

+ 266 - 0
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
+}

+ 43 - 0
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
+}