diff --git a/.drone.yml b/.drone.yml index ec7d83230ef..ab6145c037b 100644 --- a/.drone.yml +++ b/.drone.yml @@ -2297,6 +2297,50 @@ environment: image_pull_secrets: - dockerconfigjson kind: pipeline +name: release-whatsnew-checker +node: + type: no-parallel +platform: + arch: amd64 + os: linux +services: [] +steps: +- commands: + - go build -o ./bin/build -ldflags '-extldflags -static' ./pkg/build/cmd + depends_on: [] + environment: + CGO_ENABLED: 0 + image: golang:1.20.4 + name: compile-build-cmd +- commands: + - ./bin/build whatsnew-checker + depends_on: + - compile-build-cmd + image: golang:1.20.4 + name: whats-new-checker +trigger: + event: + exclude: + - promote + ref: + exclude: + - refs/tags/*-cloud* + include: + - refs/tags/v* +type: docker +volumes: +- host: + path: /var/run/docker.sock + name: docker +--- +clone: + retries: 3 +depends_on: [] +environment: + EDITION: oss +image_pull_secrets: +- dockerconfigjson +kind: pipeline name: release-oss-build-e2e-publish node: type: no-parallel @@ -7308,6 +7352,6 @@ kind: secret name: delivery-bot-app-private-key --- kind: signature -hmac: 57a88408eae5719f7ffa85c575c46e5b90ea301627144cb738d2ac8b8396e973 +hmac: c1eaa0e7d7427fd28ec0edba24a89aaa2bcb876391bd7ecde5f5fd55ffc0d0b5 ... diff --git a/pkg/build/cmd/main.go b/pkg/build/cmd/main.go index 225a02f38bc..dff9db8f8b0 100644 --- a/pkg/build/cmd/main.go +++ b/pkg/build/cmd/main.go @@ -82,6 +82,11 @@ func main() { &buildIDFlag, }, }, + { + Name: "whatsnew-checker", + Usage: "Checks whatsNewUrl in package.json for differences between the tag and the docs version", + Action: WhatsNewChecker, + }, { Name: "build-docker", Usage: "Build Grafana Docker images", diff --git a/pkg/build/cmd/whatsnewchecker.go b/pkg/build/cmd/whatsnewchecker.go new file mode 100644 index 00000000000..8e74513c2eb --- /dev/null +++ b/pkg/build/cmd/whatsnewchecker.go @@ -0,0 +1,71 @@ +package main + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/grafana/grafana/pkg/build/config" + "github.com/urfave/cli/v2" + "golang.org/x/mod/semver" +) + +const GrafanaDir = "." + +var whatsNewRegex = regexp.MustCompile(`^.*whats-new-in-(v\d*-[\d+]*)`) + +type PackageJSON struct { + Grafana Grafana `json:"grafana"` + Version string `json:"version"` +} + +type Grafana struct { + WhatsNewUrl string `json:"whatsNewUrl"` +} + +func WhatsNewChecker(c *cli.Context) error { + metadata, err := config.GenerateMetadata(c) + if err != nil { + return err + } + + if metadata.ReleaseMode.IsTest { + fmt.Println("test mode, skipping check") + return nil + } + if metadata.ReleaseMode.Mode != config.TagMode { + return fmt.Errorf("non-tag pipeline, exiting") + } + + tag := fmt.Sprintf("v%s", metadata.GrafanaVersion) + + if !semver.IsValid(tag) { + return fmt.Errorf("non-semver compatible version %s, exiting", tag) + } + + majorMinorDigits := strings.Replace(semver.MajorMinor(tag), ".", "-", 1) + + pkgJSONPath := filepath.Join(GrafanaDir, "package.json") + //nolint:gosec + pkgJSONB, err := os.ReadFile(pkgJSONPath) + if err != nil { + return fmt.Errorf("failed to read %q: %w", pkgJSONPath, err) + } + + var pkgObj PackageJSON + if err := json.Unmarshal(pkgJSONB, &pkgObj); err != nil { + return fmt.Errorf("failed decoding %q: %w", pkgJSONPath, err) + } + + whatsNewSplit := whatsNewRegex.FindStringSubmatch(pkgObj.Grafana.WhatsNewUrl) + whatsNewVersion := whatsNewSplit[1] + + if whatsNewVersion != majorMinorDigits { + return fmt.Errorf("whatsNewUrl in package.json needs to be updated to %s/", strings.Replace(whatsNewSplit[0], whatsNewVersion, majorMinorDigits, 1)) + } + + return nil +} diff --git a/pkg/build/cmd/whatsnewchecker_test.go b/pkg/build/cmd/whatsnewchecker_test.go new file mode 100644 index 00000000000..91d75e7c0e7 --- /dev/null +++ b/pkg/build/cmd/whatsnewchecker_test.go @@ -0,0 +1,83 @@ +package main + +import ( + "encoding/json" + "flag" + "fmt" + "os" + "testing" + + "github.com/grafana/grafana/pkg/build/config" + "github.com/stretchr/testify/require" + "github.com/urfave/cli/v2" +) + +const ( + DroneBuildEvent = "DRONE_BUILD_EVENT" + DroneTag = "DRONE_TAG" + DroneSemverPrerelease = "DRONE_SEMVER_PRERELEASE" +) + +const whatsNewUrl = "https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-" + +func TestWhatsNewChecker(t *testing.T) { + tests := []struct { + envMap map[string]string + packageJsonVersion string + name string + wantErr bool + errMsg string + }{ + {envMap: map[string]string{DroneBuildEvent: config.PullRequest}, packageJsonVersion: "", name: "non-tag event", wantErr: true, errMsg: "non-tag pipeline, exiting"}, + {envMap: map[string]string{DroneBuildEvent: config.Tag, DroneTag: "abcd123"}, packageJsonVersion: "", name: "non-semver compatible", wantErr: true, errMsg: "non-semver compatible version vabcd123, exiting"}, + {envMap: map[string]string{DroneBuildEvent: config.Tag, DroneTag: "v0.0.0", DroneSemverPrerelease: "test"}, packageJsonVersion: "v10-0", name: "skip check for test tags", wantErr: false}, + {envMap: map[string]string{DroneBuildEvent: config.Tag, DroneTag: "v10.0.0"}, packageJsonVersion: "v10-0", name: "package.json version matches tag", wantErr: false}, + {envMap: map[string]string{DroneBuildEvent: config.Tag, DroneTag: "v10.0.0"}, packageJsonVersion: "v9-5", name: "package.json doesn't match tag", wantErr: true, errMsg: "whatsNewUrl in package.json needs to be updated to https://grafana.com/docs/grafana/next/whatsnew/whats-new-in-v10-0/"}, + } + for _, tt := range tests { + app := cli.NewApp() + app.Version = "1.0.0" + context := cli.NewContext(app, &flag.FlagSet{}, nil) + t.Run(tt.name, func(t *testing.T) { + setUpEnv(t, tt.envMap) + err := createTempPackageJson(t, tt.packageJsonVersion) + require.NoError(t, err) + + err = WhatsNewChecker(context) + if tt.wantErr { + require.Error(t, err) + require.Equal(t, tt.errMsg, err.Error()) + } else { + require.NoError(t, err) + } + }) + } +} + +func setUpEnv(t *testing.T, envMap map[string]string) { + t.Helper() + + os.Clearenv() + t.Setenv("DRONE_BUILD_NUMBER", "12345") + t.Setenv("DRONE_COMMIT", "abcd12345") + for k, v := range envMap { + t.Setenv(k, v) + } +} + +func createTempPackageJson(t *testing.T, version string) error { + t.Helper() + + grafanaData := Grafana{WhatsNewUrl: fmt.Sprintf("%s%s/", whatsNewUrl, version)} + data := PackageJSON{Grafana: grafanaData, Version: "1.2.3"} + file, _ := json.MarshalIndent(data, "", " ") + + err := os.WriteFile("package.json", file, 0644) + require.NoError(t, err) + + t.Cleanup(func() { + err := os.RemoveAll("package.json") + require.NoError(t, err) + }) + return nil +} diff --git a/scripts/drone/events/release.star b/scripts/drone/events/release.star index aae0ed6428b..87c1d911005 100644 --- a/scripts/drone/events/release.star +++ b/scripts/drone/events/release.star @@ -64,6 +64,10 @@ load( "scripts/drone/utils/images.star", "images", ) +load( + "scripts/drone/pipelines/whats_new_checker.star", + "whats_new_checker_pipeline", +) ver_mode = "release" release_trigger = { @@ -210,9 +214,12 @@ def oss_pipelines(ver_mode = ver_mode, trigger = release_trigger): memcached_integration_tests_step(), ] + pipelines = [] + # We don't need to run integration tests at release time since they have # been run multiple times before: if ver_mode in ("release"): + pipelines.append(whats_new_checker_pipeline(release_trigger)) integration_test_steps = [] volumes = [] @@ -220,7 +227,7 @@ def oss_pipelines(ver_mode = ver_mode, trigger = release_trigger): "{}-oss-build-e2e-publish".format(ver_mode), "{}-oss-test-frontend".format(ver_mode), ] - pipelines = [ + pipelines.extend([ pipeline( name = "{}-oss-build-e2e-publish".format(ver_mode), edition = "oss", @@ -232,7 +239,7 @@ def oss_pipelines(ver_mode = ver_mode, trigger = release_trigger): ), test_frontend(trigger, ver_mode), test_backend(trigger, ver_mode), - ] + ]) if ver_mode not in ("release"): pipelines.append(pipeline( diff --git a/scripts/drone/pipelines/whats_new_checker.star b/scripts/drone/pipelines/whats_new_checker.star new file mode 100644 index 00000000000..b73acb2be0b --- /dev/null +++ b/scripts/drone/pipelines/whats_new_checker.star @@ -0,0 +1,43 @@ +""" +This module contains logic for checking if the package.json whats new url matches with the in-flight tag. +""" + +load( + "scripts/drone/utils/images.star", + "images", +) +load( + "scripts/drone/steps/lib.star", + "compile_build_cmd", +) +load( + "scripts/drone/utils/utils.star", + "pipeline", +) + +def whats_new_checker_step(): + return { + "name": "whats-new-checker", + "image": images["go_image"], + "depends_on": [ + "compile-build-cmd", + ], + "commands": [ + "./bin/build whatsnew-checker", + ], + } + +def whats_new_checker_pipeline(trigger): + environment = {"EDITION": "oss"} + steps = [ + compile_build_cmd(), + whats_new_checker_step(), + ] + return pipeline( + name = "release-whatsnew-checker", + edition = "oss", + trigger = trigger, + services = [], + steps = steps, + environment = environment, + )