dkhoa-dev/grafana-sensor-floor-map is the public Grafana panel plugin from the post on SVG floor plans. It shipped four releases between 26 Oct and 1 Nov 2026: 0.1.0, 0.2.0, 0.2.1 and 0.2.2. This post covers the workflow that builds them, and two gaps that let a green run produce the wrong archive.
A version bump creates the tag
Releases used to start from a v* tag pushed by hand. PR #26, merged on 26 Oct, moved the trigger to the version in package.json. The Tag release workflow runs on each push to main that changes that file:
# .github/workflows/tag-release.yml
tag="v$(jq -r .version package.json)"
if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" --silent 2>/dev/null; then
echo "${tag} already exists, nothing to release."
exit 0
fi
gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${tag}" -f sha="${GITHUB_SHA}" --silent
gh workflow run release.yml --repo "${GITHUB_REPOSITORY}" --ref "${tag}"
The Release workflow listens for tag pushes, but this tag push never reaches it. GitHub starts no new workflow run for events caused by GITHUB_TOKEN. workflow_dispatch and repository_dispatch events are an exception. So Release also accepts workflow_dispatch, and the job dispatches it on the tag ref. Running on the tag keeps the tag/version check in Grafana’s build-plugin action passing.
- triggerVersion bumppackage.json on main
- tagTag releasecreates v<version>
- dispatchReleaseworkflow_dispatch on the tag
- buildbuild-pluginNode 24, sign, attest
- outputDraft releasezip and its SHA1
Attestation and signature are two files
Grafana’s review of 0.2.0 recommended a build provenance attestation. PR #29 set attestation: true on build-plugin and gave the job the id-token and attestations permissions. Anyone can check a downloaded archive against the repository:
gh attestation verify dkhoa-sensorfloormap-panel-0.2.2.zip --repo dkhoa-dev/grafana-sensor-floor-map
For the 0.2.2 zip this passes, and the signer workflow is release.yml at refs/tags/v0.2.2.
The attestation says where the zip was built. The Grafana signature is something else: a MANIFEST.txt inside the zip, added by sign-plugin with an access policy token. Until 1 Nov the policy_token input in release.yml was commented out.
Green release, unsigned zip
The v0.2.1 release ran without a signing token. The run went green and the zip it published had no MANIFEST.txt. Grafana had already reviewed that exact archive, so a rebuild would have given the review different bytes.
On 1 Nov a one-off workflow on branch sign/v0.2.1 signed the published zip instead. It downloads the archive, checks its SHA1 against the reviewed one and stops if a MANIFEST.txt is already there. Then it signs with the sign-plugin version locked at the tag, re-zips and attests the result. The 0.2.1 release assets were replaced the same day. Their attestation names sign-release.yml on that branch.
For the next releases, PR #36 passes the token to the build step:
# .github/workflows/release.yml (comments removed)
- uses: grafana/plugin-actions/build-plugin@build-plugin/v1.2.1
with:
attestation: true
node-version: '24'
policy_token: ${{ secrets.GRAFANA_ACCESS_POLICY_TOKEN }}
src/release.test.ts reads that file. It fails when the build step loses the token, when the deprecated grafana_token input returns or when attestation is off. It also fails when CHANGELOG.md has no heading for the version in package.json. The PR records six mutations, each run alone, and each fails exactly one test.
A bump that broke only on main
Dependabot’s PR #32 moved @grafana/sign-plugin from 3.3.3 to 4.0.0. Version 4 needs Node 24.5.0 or newer, and CI ran Node 22. The PR’s CI run still passed, because the signing step in ci.yml runs only when the token is set:
# .github/workflows/ci.yml
- name: Sign plugin
run: npm run sign
if: ${{ env.GRAFANA_ACCESS_POLICY_TOKEN != '' }}
Workflows that Dependabot pull requests trigger get no Actions secrets, so on PR #32 the step was skipped. After the merge on 1 Nov, the next CI run on main failed at that step:
Node.js >=24.5.0 is required. Current: v22.23.4
PR #35 moved every workflow to Node 24. build-plugin defaults to Node 20, so release.yml now passes node-version: '24'. The engines field became >=24.5.0.
The PR also adds src/nodeVersion.test.ts. It reads the engines.node range of the installed sign-plugin and checks every node-version in .github/workflows against it, plus .nvmrc and engines:
// src/nodeVersion.test.ts
const tooOld = versions.filter((v) => !semver.intersects(v.version, signRange));
expect(tooOld).toEqual([]);
It is a unit test and needs no secret. The next sign-plugin bump that raises its Node floor fails on its own pull request. In the PR’s mutation run, putting Node 22 back in any one of the five workflows, .nvmrc or engines failed one test.
0.2.2
PR #36 also set the version to 0.2.2. Before the merge, the PR’s CI build was signed with the same token, and @grafana/plugin-validator -strict with its signature and manifest analyzers reported no findings on it. When the PR merged on 1 Nov, Tag release created v0.2.2 and dispatched Release on it. The published dkhoa-sensorfloormap-panel-0.2.2.zip carries a PGP-signed MANIFEST.txt with signatureType: community, and its attestation verifies.
Lessons
- A step behind
if: <secret> != ''is untested on pull requests that get no secrets, Dependabot’s among them. - Check a tool’s Node floor in a unit test, which runs on every pull request.
- A green release job does not prove the archive is signed. Test the workflow inputs that sign it.
- A tag created with
GITHUB_TOKENstarts no workflow. Dispatch the next one on the tag ref. - To sign an archive that was already reviewed, sign those bytes and check their hash first. Do not rebuild.