This file provides guidance to AI coding assistants (Claude Code, Cursor, etc.) when working with code in this repository.
This repository contains reusable GitHub Actions workflows and actions for building, testing, and deploying Grafana plugins. It's primarily for Grafana Labs internal teams.
make actionlint # Lint all workflows (run before completing workflow changes)
make act-lint # Lint Go code in tests/act/
make act-test # Run Go tests (timeout: 1h)
make genreadme # Regenerate examples/base/README.md (auto-generated, don't edit directly)
make mockdata # Regenerate test mock data after modifying test plugins
make clean # Clean all temporary files, node_modules, and dist folders- Reusable workflows:
ci.yml,cd.yml(main entry points for plugin users) - Internal to ci/cd:
playwright.yml,playwright-docker.yml,check-release-channel.yml - This repo only:
pr-checks-*,release-please-*
actions/internal/: Actions used internally by ci.yml/cd.yml. NOT for direct user use.actions/plugins/: Standalone reusable actions users can use directly. Each has its own release tag.
tests/act/: Go testing framework using nektos/act in Dockertests/simple-*: Dummy Grafana plugins used by teststests/act/mockdata/: Pre-built plugin artifacts for fast test execution
examples/base/: Core examples. README is auto-generated bygenreadme.goexamples/extra/: Additional helper examples
# Correct
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v4.2.2
# Wrong
uses: actions/checkout@v4uses: grafana/plugin-ci-workflows/.github/workflows/ci.yml@main
uses: grafana/plugin-ci-workflows/actions/internal/plugins/setup@mainThe release process automatically switches references to tagged versions.
# Dangerous - shell injection
- run: echo "${{ inputs.value }}"
# Safe - use env vars
- run: echo "${VALUE}"
env:
VALUE: ${{ inputs.value }}Exception: Safe contexts (like github.action, github.event_name) may be used in if: conditions, but always use env vars in run: blocks.
Never use ubuntu-latest. Use self-hosted runners:
- ARM (preferred):
ubuntu-arm64-small,ubuntu-arm64,ubuntu-arm64-large,ubuntu-arm64-xlarge,ubuntu-arm64-2xlarge - x64 (when needed):
ubuntu-x64-small,ubuntu-x64,ubuntu-x64-large - Add
-iosuffix for I/O intensive workloads
Use actions/github-script instead of complex bash:
- uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
script: |
const value = process.env.INPUT_VALUE;
core.setOutput('result', computedValue);
env:
INPUT_VALUE: ${{ inputs.value }}Safe Input Handling:
- run: |
case "${DEPLOY_ENV}" in
dev|ops|prod-canary|prod)
echo "Deploying to ${DEPLOY_ENV}"
;;
*)
echo "Invalid environment: ${DEPLOY_ENV}"
exit 1
;;
esac
env:
DEPLOY_ENV: ${{ inputs.environment }}Conditional Steps (safe in if: context):
- name: Deploy to production
if: inputs.environment == 'prod'
run: ./deploy.sh
env:
TARGET_ENV: ${{ inputs.environment }}Matrix Strategy:
strategy:
matrix:
os: [linux, darwin, windows]
arch: [amd64, arm64]
steps:
- run: echo "Building for ${TARGET_OS}/${TARGET_ARCH}"
env:
TARGET_OS: ${{ matrix.os }}
TARGET_ARCH: ${{ matrix.arch }}Uses release-please with separate versioning:
- Main workflows:
ci-cd-workflows/vX.Y.Z - Each action in
actions/plugins/: its own version tag
When adding a new shared workflow (internal to ci/cd), add its path to the switch-references step in:
.github/workflows/pr-checks-workflow-references.yml.github/workflows/release-please-pr-update-tagged-references.yml.github/workflows/release-please-restore-rolling-release.yml
When adding a new user-facing action in actions/plugins/, update release-please-config.json:
- Add the new package entry under
packages(with itspackage-name). - Add the package path to the
.package'sexclude-pathslist. This prevents commits in the new plugin from double-counting toward theci-cd-workflowsrelease.
The Go testing framework in tests/act/ is work in progress. Do not write new tests unless explicitly requested.
Uses: testify for assertions, nektos/act in Docker, pre-built mockdata for speed.
When modifying test plugins in tests/simple-*, run make mockdata to regenerate mock data.
Use testify, not stdlib testing:
import "github.com/stretchr/testify/require"
func TestSomething(t *testing.T) {
result, err := DoSomething()
require.NoError(t, err)
require.Equal(t, expected, result)
}Use table-driven tests:
for _, tc := range []testCase{
{name: "case1", input: "a", expected: "A"},
{name: "case2", input: "b", expected: "B"},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
require.Equal(t, tc.expected, Transform(tc.input))
})
}Run independent tests in parallel with t.Parallel().
Workflow testing patterns: See existing tests for current API usage:
- main_smoke_test.go - Basic CI workflow tests
- main_cd_test.go - More complex CD workflow tests, with mocking and test workflow manipulation
- main_backend_build_target_test.go - Example of asserting on custom mage targets via
::act-debug::annotations - internal/workflow/ci/ci.go - CI workflow helpers
- internal/act/act.go - Runner implementation
Asserting that a specific workflow step, configuration or edge case:
Use the ::act-debug:: custom GHA command to emit a debug annotation from within a workflow shell step or a mage target. The test framework intercepts these and stores them as AnnotationLevelDebug annotations, which you can then assert on with require.Contains.
Emit from a workflow step (logfmt format):
- run: printf '::act-debug::msg="%s" key=%s\n' "my step ran" "${SOME_VAR}"
env:
SOME_VAR: ${{ inputs.something }}Emit from a mage target (Go):
fmt.Printf("::act-debug::msg=%q\n", "my target was invoked")Assert in the test:
expMsg, err := logfmt.MarshalKeyvals("msg", "my target was invoked")
require.NoError(t, err)
require.Contains(t, r.Annotations, act.Annotation{
Level: act.AnnotationLevelDebug,
Message: string(expMsg),
})Keeping act tests fast — skip irrelevant jobs and steps:
Act tests run real workflows in Docker, so they are slow by default. Always scope tests to the minimum necessary work using MutateCIWorkflow().With(...):
ci.MutateCIWorkflow().With(
// Keep only the job under test; strip all other jobs and clear its `needs`
workflow.WithOnlyOneJob(t, "test-and-build", true),
// No-op steps that are irrelevant to what you're testing (e.g. frontend build)
workflow.WithNoOpStep(t, "test-and-build", "frontend"),
// Drop all steps after the one you care about (e.g. packaging, signing, GCS upload)
workflow.WithRemoveAllStepsAfter(t, "test-and-build", "backend"),
),WithOnlyOneJob(t, jobID, removeDependencies)— removes all jobs exceptjobID. Passtrueto also clear itsneedsso it can run standalone.WithNoOpStep(t, jobID, stepID)— replaces a step with a shell no-op (:), leaving the step ID intact so subsequent steps that depend on its outputs don't break wiring.WithRemoveAllStepsAfter(t, jobID, stepID)— drops every step that comes afterstepIDin the job, cutting packaging, signing, upload, etc.
When you only need to test backend behaviour, combine all three: skip the frontend step, stop after the backend step, and run only the test-and-build job.
Adding a new CI workflow input:
- Add the input to
WorkflowInputsin internal/workflow/ci/ci.go and wire it inSetCIInputs. - Add the input to
.github/workflows/ci.ymland pass it to the relevant action. - Add the input to
.github/workflows/cd.ymland pass it through toci.yml. - Add the input to the target composite action in
actions/internal/plugins/.