The Grafana compatibility check starts from your declared minimum

Why the API compatibility job on Sensor Floor Map failed 12 runs in a row after Grafana 13.3.0, why newer packages did not clear it, and the one-line change that did.

Phan Minh Dang Khoa
4 min readAI draft · AI reviewed
Contents

Sensor Floor Map is the public Grafana panel plugin from the post on release signing. Its “Latest Grafana API compatibility check” job failed on 12 runs between 1 and 4 Nov 2026. Those runs covered eight pull requests, #30 to #37. PR #38 turned it green on 4 Nov without switching the check off.

What the job runs

The workflow builds the plugin, then calls Grafana’s is-compatible action on src/module.ts:

# .github/workflows/is-compatible.yml
- name: Compatibility check
  uses: grafana/plugin-actions/is-compatible@is-compatible/v1.1.0
  with:
    module: ${{ steps.find-module-ts.outputs.modulets }}
    comment-pr: 'no'
    fail-if-incompatible: 'yes'

The action runs npx @grafana/levitate@latest is-compatible. Levitate looks up every Grafana API that module.ts uses and compares it between two versions of the Grafana packages. If the output contains “not fully compatible”, the job fails.

Grafana 13.3.0 changed one signature

npm shows @grafana/data 13.3.0 published on 27 Oct. The last green run of the job was on 26 Oct. The next run, on 1 Nov, failed with this:

Comparing @grafana/[email protected] to @grafana/[email protected]
1) Changed `PanelPlugin.useFieldConfig` used in `src/module.ts:169`
-useFieldConfig(config?: SetFieldConfigOptionsArgs<TFieldConfigOptions>): this;
+useFieldConfig(config?: SetFieldConfigOptionsArgs<TFieldConfigOptions, TOptions>): this;

The new second type parameter types context.options for field config editors. It defaults to unknown. The method itself did not change. The compiled useFieldConfig in PanelPlugin.mjs is the same in 13.2.4 and 13.3.0:

useFieldConfig(config = {}) {
  this._initConfigRegistry = () => createFieldConfigRegistry(config, this.meta.name);
  return this;
}

So the plugin ran the same on both versions. Levitate still reports the changed signature, and the job treats any report as a failure.

Where the 13.2.0 comes from

On 1 Nov two versions of the action ran against the same Grafana release. They compared from different starting points.

The first runs on PRs #31, #32 and #33 used action 1.0.4. They compared @grafana/[email protected], the installed version, to 13.3.0. Under 1.0.4, moving the packages to 13.3.0 would have cleared the check.

Dependabot’s PR #30 moved the action to 1.1.0, and it merged the same day. Version 1.1.0 adds a step before levitate:

# grafana/plugin-actions, is-compatible/action.yml at is-compatible/v1.1.0
PLUGIN_JSON="$(dirname "$LEVITATE_PATH")/plugin.json"
GRAFANA_DEP=$(jq -r '.dependencies.grafanaDependency // ""' "$PLUGIN_JSON")
MIN_PACKAGE_VERSION=$(echo "$GRAFANA_DEP" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)

The first version in grafanaDependency goes to levitate as --min-package-version. The plugin declared >=13.2.0, so the baseline was 13.2.0, whatever package.json installed. PR #38 records a local run with every @grafana/* package on 13.3.0 and the declared minimum left at 13.2.0. Levitate reported the same change.

The fix: declare 13.3 as the minimum

PR #38 changed one line in src/plugin.json:

-    "grafanaDependency": ">=13.2.0",
+    "grafanaDependency": ">=13.3.0",

The five Grafana runtime packages moved from 13.2.4 to 13.3.0 with it, as exact pins. The local Docker server now defaults to Grafana 13.3.0. The scaffold default, 13.2.4, is below the new minimum and would not load the plugin. The PR’s own run of the job printed Detected minimum Grafana version: 13.3.0 and appears to be compatible.

The cost is Grafana 13.2 support. The end-to-end matrix is built from grafanaDependency, so its 13.2.5 entry no longer runs. The latest release, 0.2.2, still declares >=13.2.0. The new minimum reaches users with the next release.

A test that keeps three versions in step

The declared minimum, the package pins and the local server version now live in three files. src/grafanaVersion.test.ts checks that they agree. One of its three tests:

// src/grafanaVersion.test.ts
const min = semver.minVersion(grafanaDependency);
const built = semver.parse(dependencies['@grafana/data']);
expect(`${min!.major}.${min!.minor}`).toBe(`${built!.major}.${built!.minor}`);

The others check that all five packages share one exact version, and that the Docker default satisfies grafanaDependency. In the PR’s mutation runs, reverting the minimum to >=13.2.0 failed this test. Removing the Docker override failed the server test.

Seven merges past a red job

PRs #30 to #36 all merged on 1 Nov with this job red. The job was not a required check, so it blocked nothing. All 12 failed runs report the same single change, useFieldConfig. None of them came from the PR under test. The job stayed red until PR #38 merged on 4 Nov.

Lessons

  • Since is-compatible 1.1.0, the check starts from the first version in grafanaDependency, not from your lockfile.
  • A new Grafana release can turn every open PR red with no change in the PRs.
  • A type-only change still fails the check. Here, raising the declared minimum was the only fix that kept the check on.
  • Keep the declared minimum, the package pins and the local server version under one unit test.
  • A check that is not required gets merged past. Read the first red log before the next merge.
Phan Minh Dang Khoa
Principal Engineer · AI, IoT and industrial automation.
← Previous postA green release can still ship an unsigned plugin