From 975b1da7328e91162212515978429e8bb5a2ea25 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:12:39 +0100 Subject: [PATCH 01/14] feat: add cloudversion analyzer --- .../passes/cloudversion/cloudversion.go | 68 +++++++++++++++++++ pkg/analysis/passes/metadata/types.go | 3 +- 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 pkg/analysis/passes/cloudversion/cloudversion.go diff --git a/pkg/analysis/passes/cloudversion/cloudversion.go b/pkg/analysis/passes/cloudversion/cloudversion.go new file mode 100644 index 00000000..bc248f35 --- /dev/null +++ b/pkg/analysis/passes/cloudversion/cloudversion.go @@ -0,0 +1,68 @@ +package cloudversion + +import ( + "encoding/json" + "fmt" + "strings" + + "golang.org/x/mod/semver" + + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" +) + +var ( + grafanaDependencyMissingCloudPreRelease = &analysis.Rule{ + Name: "grafana-dependency-missing-cloud-pre-release", + Severity: analysis.Warning, + } +) + +var Analyzer = &analysis.Analyzer{ + Name: "cloudversion", + Requires: []*analysis.Analyzer{metadata.Analyzer}, + Run: run, + Rules: []*analysis.Rule{grafanaDependencyMissingCloudPreRelease}, + ReadmeInfo: analysis.ReadmeInfo{ + Name: "Cloud version", + Description: `Ensures the Grafana version specified as Grafana dependency contains a pre-release value, ` + + `to ensure proper support in Grafana Cloud. Runs only for Grafana Labs plugins.`, + }, +} + +func run(pass *analysis.Pass) (interface{}, error) { + metadataBody, ok := pass.ResultOf[metadata.Analyzer].([]byte) + if !ok { + return nil, nil + } + var data metadata.Metadata + if err := json.Unmarshal(metadataBody, &data); err != nil { + return nil, err + } + // Run only for "grafana" plugins for now. + if !strings.EqualFold(data.Info.Author.Name, "grafana labs") && !strings.EqualFold(orgFromPluginID(data.ID), "grafana") { + return nil, nil + } + pre := semver.Prerelease(data.Dependencies.GrafanaDependency) + if pre == "" { + pass.ReportResult( + pass.AnalyzerName, + grafanaDependencyMissingCloudPreRelease, + fmt.Sprintf(`Grafana dependency %q has no pre-release value`, data.Dependencies.GrafanaDependency), + fmt.Sprintf(`The value of grafanaDependency in plugin.json (%q) is missing a pre-release value. `+ + `This may make the plugin uninstallable in Grafana Cloud. `+ + `Please add "-0" as a suffix of your grafanaDependency value ("%s-0")`, + data.Dependencies.GrafanaDependency, data.Dependencies.GrafanaDependency, + ), + ) + } + return nil, nil +} + +func orgFromPluginID(id string) string { + parts := strings.SplitN(id, "-", 3) + if len(parts) < 1 { + return "" + } + return parts[0] +} diff --git a/pkg/analysis/passes/metadata/types.go b/pkg/analysis/passes/metadata/types.go index 663a82c8..458c0f10 100644 --- a/pkg/analysis/passes/metadata/types.go +++ b/pkg/analysis/passes/metadata/types.go @@ -23,7 +23,8 @@ type Info struct { } type Author struct { - URL string `json:"url"` + Name string `json:"name"` + URL string `json:"url"` } type Screenshots struct { From 5667deaaa41b128c9b066168fabba93dd18685c0 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:13:07 +0100 Subject: [PATCH 02/14] register cloudversion analyzer --- pkg/analysis/passes/analysis.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/analysis/passes/analysis.go b/pkg/analysis/passes/analysis.go index 821561e4..3d034010 100644 --- a/pkg/analysis/passes/analysis.go +++ b/pkg/analysis/passes/analysis.go @@ -12,6 +12,7 @@ import ( "github.com/grafana/plugin-validator/pkg/analysis/passes/changelog" "github.com/grafana/plugin-validator/pkg/analysis/passes/checksum" "github.com/grafana/plugin-validator/pkg/analysis/passes/circulardependencies" + "github.com/grafana/plugin-validator/pkg/analysis/passes/cloudversion" "github.com/grafana/plugin-validator/pkg/analysis/passes/codediff" "github.com/grafana/plugin-validator/pkg/analysis/passes/coderules" "github.com/grafana/plugin-validator/pkg/analysis/passes/discoverability" @@ -103,4 +104,5 @@ var Analyzers = []*analysis.Analyzer{ virusscan.Analyzer, circulardependencies.Analyzer, codediff.Analyzer, + cloudversion.Analyzer, } From 67f63636f26bb8a4ffd9518818bb0eca5f8b01f7 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:13:09 +0100 Subject: [PATCH 03/14] gen readme --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 3d9713da..59cdf814 100644 --- a/README.md +++ b/README.md @@ -260,6 +260,7 @@ Run "mage gen:readme" to regenerate this section. | Changelog (exists) / `changelog` | Ensures a `CHANGELOG.md` file exists within the zip file. | None | | Checksum / `checksum` | Validates that the passed checksum (as a validator arg) is the one calculated from the archive file. | `checksum` | | Circular Dependencies / `circulardependencies` | Ensures that there aren't any circular dependencies between plugins (`plugin.json`, `dependencies.plugins` field). | None | +| Cloud version / `cloudversion` | Ensures the Grafana version specified as Grafana dependency contains a pre-release value, to ensure proper support in Grafana Cloud. Runs only for Grafana Labs plugins. | None | | Code Diff / `codediff` | | Google API Key with Generative AI access | | Code Rules / `code-rules` | Checks for forbidden access to environment variables, file system or use of syscall module. | [semgrep](https://github.com/returntocorp/semgrep), `sourceCodeUri` | | Developer Jargon / `jargon` | Generally discourages use of code jargon in the documentation. | None | From bbf2aafdb86a21ee334fdd2c2d2f077664cb10c3 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:22:53 +0100 Subject: [PATCH 04/14] rename to grafanadepepdency --- README.md | 2 +- pkg/analysis/passes/analysis.go | 4 +-- .../grafanadependency.go} | 31 ++++++++++--------- 3 files changed, 20 insertions(+), 17 deletions(-) rename pkg/analysis/passes/{cloudversion/cloudversion.go => grafanadependency/grafanadependency.go} (52%) diff --git a/README.md b/README.md index 59cdf814..bf13f880 100644 --- a/README.md +++ b/README.md @@ -260,13 +260,13 @@ Run "mage gen:readme" to regenerate this section. | Changelog (exists) / `changelog` | Ensures a `CHANGELOG.md` file exists within the zip file. | None | | Checksum / `checksum` | Validates that the passed checksum (as a validator arg) is the one calculated from the archive file. | `checksum` | | Circular Dependencies / `circulardependencies` | Ensures that there aren't any circular dependencies between plugins (`plugin.json`, `dependencies.plugins` field). | None | -| Cloud version / `cloudversion` | Ensures the Grafana version specified as Grafana dependency contains a pre-release value, to ensure proper support in Grafana Cloud. Runs only for Grafana Labs plugins. | None | | Code Diff / `codediff` | | Google API Key with Generative AI access | | Code Rules / `code-rules` | Checks for forbidden access to environment variables, file system or use of syscall module. | [semgrep](https://github.com/returntocorp/semgrep), `sourceCodeUri` | | Developer Jargon / `jargon` | Generally discourages use of code jargon in the documentation. | None | | Discoverability / `discoverability` | Warns about missing keywords and description that are used for plugin indexing in the catalog. | None | | Go Manifest / `go-manifest` | Validates the build manifest. | None | | Go Security Checker / `go-sec` | Inspects source code for security problems by scanning the Go AST. | [gosec](https://github.com/securego/gosec), `sourceCodeUri` | +| Grafana Dependency / `grafanadependency` | Ensures the Grafana dependency specified in plugin.json is valid | None | | JS Source Map / `jsMap` | Checks for required `module.js.map` file(s) in archive. | `sourceCodeUri` | | Legacy Grafana Toolkit usage / `legacybuilder` | Detects the usage of the not longer supported Grafana Toolkit. | None | | Legacy Platform / `legacyplatform` | Detects use of Angular which is deprecated. | None | diff --git a/pkg/analysis/passes/analysis.go b/pkg/analysis/passes/analysis.go index 3d034010..be56b81f 100644 --- a/pkg/analysis/passes/analysis.go +++ b/pkg/analysis/passes/analysis.go @@ -12,7 +12,7 @@ import ( "github.com/grafana/plugin-validator/pkg/analysis/passes/changelog" "github.com/grafana/plugin-validator/pkg/analysis/passes/checksum" "github.com/grafana/plugin-validator/pkg/analysis/passes/circulardependencies" - "github.com/grafana/plugin-validator/pkg/analysis/passes/cloudversion" + "github.com/grafana/plugin-validator/pkg/analysis/passes/grafanadependency" "github.com/grafana/plugin-validator/pkg/analysis/passes/codediff" "github.com/grafana/plugin-validator/pkg/analysis/passes/coderules" "github.com/grafana/plugin-validator/pkg/analysis/passes/discoverability" @@ -104,5 +104,5 @@ var Analyzers = []*analysis.Analyzer{ virusscan.Analyzer, circulardependencies.Analyzer, codediff.Analyzer, - cloudversion.Analyzer, + grafanadependency.Analyzer, } diff --git a/pkg/analysis/passes/cloudversion/cloudversion.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go similarity index 52% rename from pkg/analysis/passes/cloudversion/cloudversion.go rename to pkg/analysis/passes/grafanadependency/grafanadependency.go index bc248f35..91278678 100644 --- a/pkg/analysis/passes/cloudversion/cloudversion.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -1,4 +1,4 @@ -package cloudversion +package grafanadependency import ( "encoding/json" @@ -12,21 +12,20 @@ import ( ) var ( - grafanaDependencyMissingCloudPreRelease = &analysis.Rule{ - Name: "grafana-dependency-missing-cloud-pre-release", + missingCloudPreRelease = &analysis.Rule{ + Name: "missing-cloud-pre-release", Severity: analysis.Warning, } ) var Analyzer = &analysis.Analyzer{ - Name: "cloudversion", + Name: "grafanadependency", Requires: []*analysis.Analyzer{metadata.Analyzer}, Run: run, - Rules: []*analysis.Rule{grafanaDependencyMissingCloudPreRelease}, + Rules: []*analysis.Rule{missingCloudPreRelease}, ReadmeInfo: analysis.ReadmeInfo{ - Name: "Cloud version", - Description: `Ensures the Grafana version specified as Grafana dependency contains a pre-release value, ` + - `to ensure proper support in Grafana Cloud. Runs only for Grafana Labs plugins.`, + Name: "Grafana Dependency", + Description: "Ensures the Grafana dependency specified in plugin.json is valid", }, } @@ -39,15 +38,19 @@ func run(pass *analysis.Pass) (interface{}, error) { if err := json.Unmarshal(metadataBody, &data); err != nil { return nil, err } - // Run only for "grafana" plugins for now. - if !strings.EqualFold(data.Info.Author.Name, "grafana labs") && !strings.EqualFold(orgFromPluginID(data.ID), "grafana") { - return nil, nil - } + isGrafanaLabs := strings.EqualFold(data.Info.Author.Name, "grafana labs") && !strings.EqualFold(orgFromPluginID(data.ID), "grafana") pre := semver.Prerelease(data.Dependencies.GrafanaDependency) - if pre == "" { + if pre == "" && isGrafanaLabs { + // Ensure that Grafana Labs plugin specify a pre-release (-99999999999) in Grafana Dependency. + // If the pre-release part is missing and the grafanaDependency specifies a version that's not + // been released yet, which is often the case for Grafana Labs plugins and not community/commercial plugins, + // the plugin won't be loaded correctly in cloud because it doesn't satisfy the Grafana dependency. + // Example: on a Cloud instance we have Grafana 12.4.0-99999999999. This is a PRE-RELEASE of 12.4.0. + // If the plugin specifies 12.4.0 as grafanaDependency, it's incompatible with 12.4.0-99999999999. + // This is because 12.4.0-x (pre-release) < 12.4.0 ("stable") => the plugin can't be installed in Cloud. pass.ReportResult( pass.AnalyzerName, - grafanaDependencyMissingCloudPreRelease, + missingCloudPreRelease, fmt.Sprintf(`Grafana dependency %q has no pre-release value`, data.Dependencies.GrafanaDependency), fmt.Sprintf(`The value of grafanaDependency in plugin.json (%q) is missing a pre-release value. `+ `This may make the plugin uninstallable in Grafana Cloud. `+ From 1ef364861f166e2cfae695212e76ac857da56d2a Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:27:16 +0100 Subject: [PATCH 05/14] fix isGrafanaLabs logic --- pkg/analysis/passes/grafanadependency/grafanadependency.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index 91278678..fd7ffc76 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -38,7 +38,7 @@ func run(pass *analysis.Pass) (interface{}, error) { if err := json.Unmarshal(metadataBody, &data); err != nil { return nil, err } - isGrafanaLabs := strings.EqualFold(data.Info.Author.Name, "grafana labs") && !strings.EqualFold(orgFromPluginID(data.ID), "grafana") + isGrafanaLabs := strings.EqualFold(data.Info.Author.Name, "grafana labs") || strings.EqualFold(orgFromPluginID(data.ID), "grafana") pre := semver.Prerelease(data.Dependencies.GrafanaDependency) if pre == "" && isGrafanaLabs { // Ensure that Grafana Labs plugin specify a pre-release (-99999999999) in Grafana Dependency. @@ -62,6 +62,8 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// orgFromPluginID extracts and returns the organization prefix from a plugin ID by splitting on the first hyphen. +// Returns an empty string if the plugin ID is empty or invalid. func orgFromPluginID(id string) string { parts := strings.SplitN(id, "-", 3) if len(parts) < 1 { From 8be3d1fca29b5390527ce190a43e9185fd0180d7 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:30:33 +0100 Subject: [PATCH 06/14] add IsGrafanaLabs --- .../grafanadependency/grafanadependency.go | 14 +------------- pkg/analysis/passes/metadata/types.go | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+), 13 deletions(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index fd7ffc76..f7e0c7df 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -3,7 +3,6 @@ package grafanadependency import ( "encoding/json" "fmt" - "strings" "golang.org/x/mod/semver" @@ -38,9 +37,8 @@ func run(pass *analysis.Pass) (interface{}, error) { if err := json.Unmarshal(metadataBody, &data); err != nil { return nil, err } - isGrafanaLabs := strings.EqualFold(data.Info.Author.Name, "grafana labs") || strings.EqualFold(orgFromPluginID(data.ID), "grafana") pre := semver.Prerelease(data.Dependencies.GrafanaDependency) - if pre == "" && isGrafanaLabs { + if pre == "" && data.IsGrafanaLabs() { // Ensure that Grafana Labs plugin specify a pre-release (-99999999999) in Grafana Dependency. // If the pre-release part is missing and the grafanaDependency specifies a version that's not // been released yet, which is often the case for Grafana Labs plugins and not community/commercial plugins, @@ -61,13 +59,3 @@ func run(pass *analysis.Pass) (interface{}, error) { } return nil, nil } - -// orgFromPluginID extracts and returns the organization prefix from a plugin ID by splitting on the first hyphen. -// Returns an empty string if the plugin ID is empty or invalid. -func orgFromPluginID(id string) string { - parts := strings.SplitN(id, "-", 3) - if len(parts) < 1 { - return "" - } - return parts[0] -} diff --git a/pkg/analysis/passes/metadata/types.go b/pkg/analysis/passes/metadata/types.go index 458c0f10..6e9f00a4 100644 --- a/pkg/analysis/passes/metadata/types.go +++ b/pkg/analysis/passes/metadata/types.go @@ -1,5 +1,7 @@ package metadata +import "strings" + type Metadata struct { ID string `json:"id"` Name string `json:"name"` @@ -12,6 +14,13 @@ type Metadata struct { Dependencies MetadataDependencies `json:"dependencies"` } +// IsGrafanaLabs returns true if the plugin was developed by Grafana Labs. +// A plugin is considered developed by Grafana Labs if either +// the author name is "Grafana Labs" or the org name in the slug is "grafana" +func (m Metadata) IsGrafanaLabs() bool { + return strings.EqualFold(m.Info.Author.Name, "grafana labs") || strings.EqualFold(orgFromPluginID(m.ID), "grafana") +} + type Info struct { Author Author `json:"author"` Screenshots []Screenshots `json:"screenshots"` @@ -65,3 +74,13 @@ type MetadataPluginDependency struct { Name string `json:"name"` Type string `json:"type"` } + +// orgFromPluginID extracts and returns the organization prefix from a plugin ID by splitting on the first hyphen. +// Returns an empty string if the plugin ID is empty or invalid. +func orgFromPluginID(id string) string { + parts := strings.SplitN(id, "-", 3) + if len(parts) < 1 { + return "" + } + return parts[0] +} From c899813272c0520ae8803538d76c0b3841f15d43 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:34:35 +0100 Subject: [PATCH 07/14] add TestGrafanaDependencyParse --- .../grafanadependency_test.go | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 pkg/analysis/passes/grafanadependency/grafanadependency_test.go diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go new file mode 100644 index 00000000..535c5f5a --- /dev/null +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -0,0 +1,28 @@ +package grafanadependency + +import ( + "testing" + + "github.com/stretchr/testify/require" + "golang.org/x/mod/semver" +) + +func TestGrafanaDependencyParse(t *testing.T) { + for _, tc := range []struct { + name string + dependency string + expPre string + }{ + {"no pre-release", ">=12.4.0", ""}, + {"no pre-release with space", ">= 12.4.0", ""}, + {"zero pre-release", ">=12.4.0-0", "0"}, + {"zero pre-release with space", ">= 12.4.0-0", "0"}, + {"non-zero pre-release", ">=12.4.0-01189998819991197253", "01189998819991197253"}, + {"non-zero pre-release with space", ">= 12.4.0-01189998819991197253", "01189998819991197253"}, + } { + t.Run(tc.name, func(t *testing.T) { + pre := semver.Prerelease(">=12.4.0") + require.Empty(t, pre) + }) + } +} From 651b1182c9f7372e363538cc9f95b0df6da78a72 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:35:18 +0100 Subject: [PATCH 08/14] refactor --- pkg/analysis/passes/grafanadependency/grafanadependency.go | 6 +++++- .../passes/grafanadependency/grafanadependency_test.go | 3 +-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index f7e0c7df..c0970685 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -37,7 +37,7 @@ func run(pass *analysis.Pass) (interface{}, error) { if err := json.Unmarshal(metadataBody, &data); err != nil { return nil, err } - pre := semver.Prerelease(data.Dependencies.GrafanaDependency) + pre := getPreRelease(data.Dependencies.GrafanaDependency) if pre == "" && data.IsGrafanaLabs() { // Ensure that Grafana Labs plugin specify a pre-release (-99999999999) in Grafana Dependency. // If the pre-release part is missing and the grafanaDependency specifies a version that's not @@ -59,3 +59,7 @@ func run(pass *analysis.Pass) (interface{}, error) { } return nil, nil } + +func getPreRelease(grafanaDependency string) string { + return semver.Prerelease(grafanaDependency) +} diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go index 535c5f5a..0ba3265b 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -4,7 +4,6 @@ import ( "testing" "github.com/stretchr/testify/require" - "golang.org/x/mod/semver" ) func TestGrafanaDependencyParse(t *testing.T) { @@ -21,7 +20,7 @@ func TestGrafanaDependencyParse(t *testing.T) { {"non-zero pre-release with space", ">= 12.4.0-01189998819991197253", "01189998819991197253"}, } { t.Run(tc.name, func(t *testing.T) { - pre := semver.Prerelease(">=12.4.0") + pre := getPreRelease(">=12.4.0") require.Empty(t, pre) }) } From 2e35f64677f19df8ffab5b6b6209ae138904722d Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 12:59:44 +0100 Subject: [PATCH 09/14] fix getPreRelease --- .../grafanadependency/grafanadependency.go | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index c0970685..a0322d11 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -3,13 +3,16 @@ package grafanadependency import ( "encoding/json" "fmt" - - "golang.org/x/mod/semver" + "regexp" "github.com/grafana/plugin-validator/pkg/analysis" "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" ) +// https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string +// Modified to work with semver range operators (>=, >, <, <=) by removing ^ and $ anchors +var semverRegex = regexp.MustCompile(`(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)\.(?P0|[1-9]\d*)(?:-(?P(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\.(?:0|[1-9]\d*|\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\+(?P[0-9a-zA-Z-]+(?:\.[0-9a-zA-Z-]+)*))?`) + var ( missingCloudPreRelease = &analysis.Rule{ Name: "missing-cloud-pre-release", @@ -61,5 +64,14 @@ func run(pass *analysis.Pass) (interface{}, error) { } func getPreRelease(grafanaDependency string) string { - return semver.Prerelease(grafanaDependency) + matches := semverRegex.FindStringSubmatch(grafanaDependency) + if matches == nil { + return "" + } + for i, name := range semverRegex.SubexpNames() { + if name == "prerelease" && i < len(matches) { + return matches[i] + } + } + return "" } From 7cc8869c010dd502da4b5c1d525352094cff1645 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 13:00:11 +0100 Subject: [PATCH 10/14] add more tests --- .../grafanadependency_test.go | 109 +++++++++++++++++- 1 file changed, 108 insertions(+), 1 deletion(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go index 0ba3265b..4e32349a 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -1,12 +1,119 @@ package grafanadependency import ( + "encoding/json" + "fmt" + "path/filepath" "testing" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + + "github.com/grafana/plugin-validator/pkg/analysis" + "github.com/grafana/plugin-validator/pkg/analysis/passes/archive" + "github.com/grafana/plugin-validator/pkg/analysis/passes/metadata" + "github.com/grafana/plugin-validator/pkg/analysis/passes/metadatavalid" + "github.com/grafana/plugin-validator/pkg/testpassinterceptor" ) -func TestGrafanaDependencyParse(t *testing.T) { +func TestGrafanaDependency(t *testing.T) { + for _, tc := range []struct { + name string + pluginJSON string + expFlag bool + }{ + { + name: "non-grafana labs plugin without pre-release shouldn't be flagged", + pluginJSON: `{ + "id": "community-my-app", + "dependencies": { "grafanaDependency": ">=11.6.0" } + }`, + expFlag: false, + }, + { + name: "non-grafana labs plugin with pre-release shouldn't be flagged", + pluginJSON: `{ + "id": "community-my-app", + "dependencies": { "grafanaDependency": ">=11.6.0-0" } + }`, + expFlag: false, + }, + { + name: "grafana org plugin without pre-release should be flagged", + pluginJSON: `{ + "id": "grafana-my-app", + "dependencies": { "grafanaDependency": ">=11.6.0" } + }`, + expFlag: true, + }, + { + name: "grafana labs author plugin without pre-release should be flagged", + pluginJSON: `{ + "id": "adopted-my-app", + "info": { "author": { "name": "Grafana Labs" } }, + "dependencies": { "grafanaDependency": ">=11.6.0" } + }`, + expFlag: true, + }, + { + name: "grafana org plugin with pre-release should not be flagged", + pluginJSON: `{ + "id": "grafana-my-app", + "dependencies": { "grafanaDependency": ">= 11.6.0-0" } + }`, + expFlag: false, + }, + { + name: "grafana labs author plugin with pre-release should not be flagged", + pluginJSON: `{ + "id": "adopted-my-app", + "info": { "author": { "name": "Grafana Labs" } }, + "dependencies": { "grafanaDependency": ">= 11.6.0-0" } + }`, + expFlag: false, + }, + } { + t.Run(tc.name, func(t *testing.T) { + var interceptor testpassinterceptor.TestPassInterceptor + pass := &analysis.Pass{ + RootDir: filepath.Join("./"), + ResultOf: map[*analysis.Analyzer]interface{}{ + metadata.Analyzer: []byte(tc.pluginJSON), + archive.Analyzer: filepath.Join("."), + metadatavalid.Analyzer: nil, + }, + Report: interceptor.ReportInterceptor(), + } + + _, err := Analyzer.Run(pass) + require.NoError(t, err) + if tc.expFlag { + require.Len(t, interceptor.Diagnostics, 1) + + var meta metadata.Metadata + require.NoError(t, json.Unmarshal(pass.ResultOf[metadata.Analyzer].([]byte), &meta)) + grafanaDependency := meta.Dependencies.GrafanaDependency + require.NotEmpty(t, grafanaDependency, "grafana dependency should not be empty") + + d := interceptor.Diagnostics[0] + require.Equal(t, analysis.Warning, d.Severity, "severity should be warning") + require.Equal(t, "missing-cloud-pre-release", d.Name, "name should match") + require.Equal(t, fmt.Sprintf(`Grafana dependency "%s" has no pre-release value`, grafanaDependency), d.Title, "title should match") + require.Equal(t, fmt.Sprintf(`The value of grafanaDependency in plugin.json ("%s") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add "-0" as a suffix of your grafanaDependency value ("%s-0")`, grafanaDependency, grafanaDependency), d.Detail, "detail should match") + } else { + ok := assert.Len(t, interceptor.Diagnostics, 0, "expecting no diagnostics but got %d", len(interceptor.Diagnostics)) + // Log for debugging + if !ok { + for _, d := range interceptor.Diagnostics { + t.Logf("%+v", d) + } + } + } + }) + } +} + +func TestGrafanaDependency_GetPreRelease(t *testing.T) { for _, tc := range []struct { name string dependency string From 01ef05e2bd138d8803df49b777c4d600067c1536 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 13:00:18 +0100 Subject: [PATCH 11/14] fix getPreRelease tests --- .../passes/grafanadependency/grafanadependency_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go index 4e32349a..e6715e8b 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -123,12 +123,12 @@ func TestGrafanaDependency_GetPreRelease(t *testing.T) { {"no pre-release with space", ">= 12.4.0", ""}, {"zero pre-release", ">=12.4.0-0", "0"}, {"zero pre-release with space", ">= 12.4.0-0", "0"}, - {"non-zero pre-release", ">=12.4.0-01189998819991197253", "01189998819991197253"}, - {"non-zero pre-release with space", ">= 12.4.0-01189998819991197253", "01189998819991197253"}, + {"non-zero pre-release", ">=12.4.0-1189998819991197253", "1189998819991197253"}, + {"non-zero pre-release with space", ">= 12.4.0-1189998819991197253", "1189998819991197253"}, } { t.Run(tc.name, func(t *testing.T) { - pre := getPreRelease(">=12.4.0") - require.Empty(t, pre) + pre := getPreRelease(tc.dependency) + require.Equal(t, tc.expPre, pre, "extracted pre-release value should match") }) } } From f9f1830ef3c3ac49805eadd1b68a682cbbc95b3f Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 13:01:42 +0100 Subject: [PATCH 12/14] add docstring and more tests --- pkg/analysis/passes/grafanadependency/grafanadependency.go | 3 +++ .../passes/grafanadependency/grafanadependency_test.go | 2 ++ 2 files changed, 5 insertions(+) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index a0322d11..981486f8 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -63,6 +63,9 @@ func run(pass *analysis.Pass) (interface{}, error) { return nil, nil } +// getPreRelease extracts the pre-release identifier from a Grafana dependency version string. +// It handles semver range operators (>=, >, <, <=) and returns the pre-release part after the dash. +// Examples: ">=12.4.0-0" returns "0", ">=12.4.0" returns "", ">= 12.4.0-pre.1" returns "pre.1". func getPreRelease(grafanaDependency string) string { matches := semverRegex.FindStringSubmatch(grafanaDependency) if matches == nil { diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go index e6715e8b..ef34c8ad 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency_test.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency_test.go @@ -125,6 +125,8 @@ func TestGrafanaDependency_GetPreRelease(t *testing.T) { {"zero pre-release with space", ">= 12.4.0-0", "0"}, {"non-zero pre-release", ">=12.4.0-1189998819991197253", "1189998819991197253"}, {"non-zero pre-release with space", ">= 12.4.0-1189998819991197253", "1189998819991197253"}, + {"non-numeric pre-release", ">=12.4.0-alpha.1", "alpha.1"}, + {"non-numeric pre-release with space", ">= 12.4.0-alpha-2", "alpha-2"}, } { t.Run(tc.name, func(t *testing.T) { pre := getPreRelease(tc.dependency) From 2d8495e8fd415f8e6e9e2b314be45e0b39160256 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 13:01:55 +0100 Subject: [PATCH 13/14] update docstring --- pkg/analysis/passes/grafanadependency/grafanadependency.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/analysis/passes/grafanadependency/grafanadependency.go b/pkg/analysis/passes/grafanadependency/grafanadependency.go index 981486f8..7325d56c 100644 --- a/pkg/analysis/passes/grafanadependency/grafanadependency.go +++ b/pkg/analysis/passes/grafanadependency/grafanadependency.go @@ -65,7 +65,7 @@ func run(pass *analysis.Pass) (interface{}, error) { // getPreRelease extracts the pre-release identifier from a Grafana dependency version string. // It handles semver range operators (>=, >, <, <=) and returns the pre-release part after the dash. -// Examples: ">=12.4.0-0" returns "0", ">=12.4.0" returns "", ">= 12.4.0-pre.1" returns "pre.1". +// Examples: ">=12.4.0-0" returns "0", ">=12.4.0" returns "", ">= 12.4.0-1234" returns "1234". func getPreRelease(grafanaDependency string) string { matches := semverRegex.FindStringSubmatch(grafanaDependency) if matches == nil { From 259a6d6426b9dfd695ff2abcebe81f3da85ec237 Mon Sep 17 00:00:00 2001 From: Giuseppe Guerra Date: Fri, 13 Feb 2026 13:27:02 +0100 Subject: [PATCH 14/14] fix integration tests --- pkg/cmd/plugincheck2/main_test.go | 40 +++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/pkg/cmd/plugincheck2/main_test.go b/pkg/cmd/plugincheck2/main_test.go index 0d8a39e7..8b3e0561 100644 --- a/pkg/cmd/plugincheck2/main_test.go +++ b/pkg/cmd/plugincheck2/main_test.go @@ -61,6 +61,14 @@ func TestIntegration(t *testing.T) { Id: "grafana-clock-panel", Version: "2.1.5", PluginValidator: map[string][]Issue{ + "grafanadependency": { + { + Severity: "warning", + Title: "Grafana dependency \">=8.0.0\" has no pre-release value", + Detail: "The value of grafanaDependency in plugin.json (\">=8.0.0\") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add \"-0\" as a suffix of your grafanaDependency value (\">=8.0.0-0\")", + Name: "missing-cloud-pre-release", + }, + }, "jargon": { { Severity: "warning", @@ -87,6 +95,14 @@ func TestIntegration(t *testing.T) { Id: "alexanderzobnin-zabbix-app", Version: "4.4.9", PluginValidator: map[string][]Issue{ + "grafanadependency": { + { + Severity: "warning", + Title: "Grafana dependency \">=9.3.0\" has no pre-release value", + Detail: "The value of grafanaDependency in plugin.json (\">=9.3.0\") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add \"-0\" as a suffix of your grafanaDependency value (\">=9.3.0-0\")", + Name: "missing-cloud-pre-release", + }, + }, "includesnested": { { Severity: "error", @@ -151,6 +167,14 @@ func TestIntegration(t *testing.T) { Id: "yesoreyeram-infinity-datasource", Version: "2.6.3", PluginValidator: map[string][]Issue{ + "grafanadependency": { + { + Severity: "warning", + Title: "Grafana dependency \">=9.5.15\" has no pre-release value", + Detail: "The value of grafanaDependency in plugin.json (\">=9.5.15\") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add \"-0\" as a suffix of your grafanaDependency value (\">=9.5.15-0\")", + Name: "missing-cloud-pre-release", + }, + }, "sponsorshiplink": { { Severity: "recommendation", @@ -352,6 +376,14 @@ func TestIntegration(t *testing.T) { Id: "grafana-clock-panel", Version: "2.1.5", PluginValidator: map[string][]Issue{ + "grafanadependency": { + { + Severity: "warning", + Title: "Grafana dependency \">=8.0.0\" has no pre-release value", + Detail: "The value of grafanaDependency in plugin.json (\">=8.0.0\") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add \"-0\" as a suffix of your grafanaDependency value (\">=8.0.0-0\")", + Name: "missing-cloud-pre-release", + }, + }, "jargon": { { Severity: "warning", @@ -379,6 +411,14 @@ func TestIntegration(t *testing.T) { Id: "grafana-clock-panel", Version: "2.1.5", PluginValidator: map[string][]Issue{ + "grafanadependency": { + { + Severity: "warning", + Title: "Grafana dependency \">=8.0.0\" has no pre-release value", + Detail: "The value of grafanaDependency in plugin.json (\">=8.0.0\") is missing a pre-release value. This may make the plugin uninstallable in Grafana Cloud. Please add \"-0\" as a suffix of your grafanaDependency value (\">=8.0.0-0\")", + Name: "missing-cloud-pre-release", + }, + }, "jargon": { { Severity: "warning",