
Migrate Provider
- 1 installs
- 524 repo stars
- Updated August 4, 2026
- grafana/gcx
migrate-provider skill documents Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider migration, or when user says "migrate provider", "port from gcx", "p
About
migrate-provider skill documents Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider migration, or when user says "migrate provider", "port from gcx", "port oncall", "port k6". Not for building providers from scratch - use /add-provider for that.. name: migrate-provider description: Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider migration, or when user says "migrate provider", "port from gcx", "port oncall", "port k6". Not for building providers from scratch - use /add-provider for that.
- Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider m
- When NOT to use: Building a provider from scratch for a product without
- Platform-specific setup patterns for migrate-provider.
- Evidence-backed steps from upstream SKILL.md.
- When-to-use criteria for migrate-provider versus alternatives.
Migrate Provider by the numbers
- 1 all-time installs (skills.sh)
- Ranked #1,172 of 1,435 DevOps & CI/CD skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
migrate-provider capabilities & compatibility
- Capabilities
- migrate provider quick start · migrate provider when to use guidance · migrate provider integration patterns
- Works with
- grafana
What migrate-provider says it does
schema/example registration, CRUD redirect commands, and ancillary subcommands.
npx skills add https://github.com/grafana/gcx --skill migrate-providerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 524 |
| Last updated | August 4, 2026 |
| Repository | grafana/gcx ↗ |
How do I use migrate-provider correctly?
Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider migration, or when user says "migrate provider", "port from gcx", "port oncall",
Who is it for?
Teams implementing migrate-provider workflows from the catalog.
Skip if: Skip when requirements clearly match a different specialized stack.
When should I use this skill?
User asks about migrate-provider, use when porting a grafana cloud product from grafana-cloud-cli (gcx) to gcx, when a bead .
What you get
Working migrate-provider setup with validated configuration and next steps.
Files
Migrate Provider from gcx
Port an existing gcx resource client into a gcx provider — core adapter, schema/example registration, CRUD redirect commands, and ancillary subcommands.
Before starting: Read gcx-provider-recipe.md front to back. The recipe is the source of truth for mechanical steps. This skill wraps it with workflow discipline and orchestration.
Canonical reference: internal/providers/incidents/ — the first full port (adapter + schema + commands + ancillary). Start there for patterns.
When to Use
- Porting a gcx resource client to gcx
- A bead task references gcx provider migration
- User says "migrate provider", "port from gcx", "port oncall", "port k6"
When NOT to use: Building a provider from scratch for a product without a gcx client — use /add-provider instead.
Prerequisites
Before invoking this skill, ensure:
1. gcx binary available — gcx --version must succeed. 2. Grafana context configured — gcx config view must show a working context with server URL and token. 3. Provider directory exists — create internal/providers/{name} before starting the port. 4. Live API access — smoke tests (Phase 4) require a real Grafana instance. Verify connectivity: gcx --context=<ctx> resources schemas.
Pipeline Overview
Phase 0: Requirements Gathering (autonomous)
→ context bundle (source + compliance + pattern ref)
↓ [no gate — feeds Phase 1]
Phase 1: Design Discovery (interactive, 1A–1D)
→ ADR in docs/adrs/{provider}/
↓ [user approval gate]
Phase 2: Spec Planning
→ spec.md + plan.md + tasks.md
↓ [user approval gate]
Phase 3: Build
→ agent team (Core + Commands)
→ code files
↓ [make all gate]
Phase 4: Verification (4A–4E)
→ make all + smoke tests + adapter smoke
→ comparison report + recipe update
↓ [user approval gate]| Phase | Agent Strategy | Receives | Produces | Gate |
|---|---|---|---|---|
| 0: Requirements | Lead (autonomous) | gcx source + compliance docs | Context bundle | None (feeds Phase 1) |
| 1: Design | Lead (interactive) | Context bundle | ADR | User approves ADR |
| 2: Spec Planning | Lead (or /plan-spec) | ADR + context bundle | spec.md, plan.md, tasks.md | User approves spec package |
| 3: Build | Agent team (Core + Commands) or /build-spec | Spec package | Provider code | GCX_AGENT_MODE=false make all passes |
| 4: Verify | Subagent | Comparison report template + spec ACs | Comparison report + recipe update | User approves report |
Phases are strictly sequential. Each phase is separated by a gate that must pass before the next phase begins. Gates are not optional.
Small-provider shortcut: For providers with 3 or fewer subcommands,
Phase 1 stages 1B–1D may be collapsed into a single proposal. Document
this choice in the ADR.
---
Phase 0: Requirements Gathering (Autonomous)
Phase 0 is fully autonomous — no user interaction required. The output is a context bundle, not a design proposal.
0.1: Read gcx Source
Read the grafana-cloud-cli source for the target provider. Identify every subcommand, API endpoint, type definition, and auth mechanism.
0.2: Check Compliance Documents
Read the following project compliance documents and record which rules apply to the target provider. Use the lint compliance checklist from conventions.md as the recording template.
CONSTITUTION.md— CLI grammar, output conventionsdocs/reference/design-guide.md— naming, output formats, exit codesdocs/reference/provider-guide.md— provider interface, adapter wiringdocs/reference/provider-discovery-guide.md— API discovery, design decisions
0.3: Identify Pattern Reference
Identify and read the closest existing gcx provider as a pattern reference:
- Cloud APIs with separate URLs →
fleet - Plugin APIs (standard Grafana SA token) →
slo - gRPC-style POST APIs →
incidents - Token exchange auth →
k6 - Multi-resource providers →
oncall - Plugin proxy APIs →
kg
0.4: Produce Context Bundle
The context bundle contains: 1. Source summary — every gcx subcommand mapped with proposed gcx equivalent or "Deferred" with rationale 2. Compliance notes — applicable rules per document with section references (filled checklist from 0.2) 3. Pattern reference — which existing provider to follow and why
Phase 0 Gate
None. Phase 0 feeds directly into Phase 1. The context bundle is an
internal artifact — it does not require user approval.
---
Phase 1: Design Discovery (Interactive)
Phase 1 uses progressive disclosure with four stages. Each stage MUST receive explicit user approval before the next stage begins.
Stage 1A: CLI UX
Propose a command tree with naming and grammar compliance validated against CONSTITUTION.md's CLI Grammar section.
Present to user: command tree, verb choices, alias conventions, naming rationale.
Gate: User approves Stage 1A before proceeding.
Stage 1B: Resource Adapters
Specify which resources get TypedCRUD adapters, which remain provider-only commands, the GVK mapping for each adapter resource, and the verb choice rationale (list vs show).
Present to user: adapter classification table, GVK mapping, verb rationale.
Gate: User approves Stage 1B before proceeding.
Stage 1C: Auth & Config
Specify ConfigKeys, ConfigLoader usage, environment variable names, and any GCOM/instance lookup requirements.
Present to user: config key table, env var names, auth flow diagram.
Gate: User approves Stage 1C before proceeding.
Stage 1D: Architecture
Specify package layout, client construction pattern, shared helpers, and the auth subpackage structure (if the provider uses multiple subpackages).
Present to user: package tree, client pattern, helper inventory.
Gate: User approves Stage 1D before proceeding.
Small-provider shortcut: For providers with 3 or fewer subcommands,
collapse stages 1B–1D into a single combined proposal. All content must
still be present — only the number of approval rounds is reduced.
Phase 1 Output: ADR
Write an ADR documenting all design decisions from stages 1A–1D to docs/adrs/{provider}/. The ADR MUST be approved by the user before proceeding to Phase 2.
Phase 1 Gate
STOP. Do not begin Phase 2 until:
>
1. All four stages (1A–1D) have received explicit user approval
2. The ADR exists in docs/adrs/{provider}/ and is approved by the user>
If the user has NOT approved a stage, block and re-present it for feedback.
---
Phase 2: Spec Planning
Phase 2 produces three documents in spec format:
1. spec.md — functional requirements + acceptance criteria (Given/When/Then) 2. plan.md — architecture decisions + HTTP client reference section 3. tasks.md — dependency graph + waves + per-task deliverables
plan.md: HTTP Client Reference (MANDATORY)
plan.md MUST include the HTTP client reference section from commands-reference.md. This section contains:
- Endpoint table (method, path, purpose, notes)
- Auth helper signature
- Client construction pattern with exact field names
This prevents response envelope hallucination — the most impactful bug class discovered during provider migrations.
tasks.md: Verification Tasks (MANDATORY)
tasks.md MUST include smoke test design as explicit verification tasks. Each show/list command MUST have a smoke test task entry specifying all four output formats (json, table, wide, yaml).
Optional: /plan-spec Integration
When /plan-spec is available, Phase 2 SHOULD use it. When /plan-spec is not available, Phase 2 MUST produce the same document format manually. /plan-spec is an optional accelerator, not a dependency.
Phase 2 Gate
STOP. Do not begin Phase 3 until:
>
1. All three documents (spec.md, plan.md, tasks.md) exist with YAML
frontmatter, FR-NNN numbering, and Given/When/Then acceptance criteria
2. plan.md contains the HTTP client reference section
3. tasks.md contains smoke test verification tasks for all output formats
4. The user has explicitly approved the spec package
---
Phase 3: Build
Phase 3 executes tasks.md waves in order, with make lint as a checkpoint between each task.
Builder Agent Rules
Builder spawn prompts (see templates/builder-prompts.md) MUST include:
"Do NOT infer response envelope shapes. Copy deserialization code verbatim
from the grafana-cloud-cli source. If the source does
json.Unmarshal(body, &slice), the new client MUST do the same — neverwrap in a struct unless the source does."
Builder spawn prompts MUST NOT include verification task details — no smoke commands, no expected comparison outputs, no pass/fail criteria from the comparison report template.
Agent Team Orchestration
The Build phase uses an agent team with two teammates:
- Build-Core — owns types, client, adapter, resource_adapter files.
Must complete before Build-Commands begins.
- Build-Commands — owns provider registration and CLI command files.
Starts only after Build-Core signals completion.
File Ownership Table
| Recipe Phase | File(s) | Teammate |
|---|---|---|
| Step 2: Types | internal/providers/{name}/types.go | Build-Core |
| Step 3: Client | internal/providers/{name}/client.go, client_test.go | Build-Core |
| Step 4: Adapter + Resource Adapter | internal/providers/{name}/adapter.go, resource_adapter.go | Build-Core |
| Step 5: Provider registration | internal/providers/{name}/provider.go | Build-Commands |
| Step 6: Tests | Command tests (*_test.go) | Build-Commands |
| Step 7: Integration / Wiring | cmd/gcx/providers/{name}/commands.go, cmd/gcx/root/command.go (blank import) | Build-Commands |
Teammates MUST NOT modify files outside their ownership boundary.
Integration/Wiring Task
The integration task MUST explicitly include: 1. Wire Commands() and TypedRegistrations() 2. Add blank import in cmd/gcx/root/command.go 3. Fix import cycles introduced by subpackage references 4. Fix variable name collisions from package aliasing 5. Run make lint and fix all new issues
Optional: /build-spec Integration
When /build-spec is available, Phase 3 SHOULD use it. When /build-spec is not available, Phase 3 MUST use the agent team orchestration described above. /build-spec is an optional accelerator, not a dependency.
Phase 3 Gate
STOP. Do not begin Phase 4 until:
>
GCX_AGENT_MODE=false make all exits 0 with no lint errors and all testspassing.
>
Run this command after both Build teammates complete. If it fails, fix the
root cause before proceeding — do not proceed with a failing build.
---
Phase 4: Verification (4A–4E)
Phase 4 MUST execute in this exact order. No step may be skipped.
Step 4A: Build Gate
Run GCX_AGENT_MODE=false make all and confirm exit 0.
Step 4B: Smoke Tests (MANDATORY)
Smoke tests are MANDATORY for every show/list command. Each command MUST be tested with ALL FOUR output formats: -o json, -o table, -o wide, -o yaml.
Smoke tests MUST NOT be marked "optional" or "if live instance available". If no live instance is available, Phase 4 MUST block and report the blocker to the user.
CTX={context-name}
for fmt in json table wide yaml; do
GCX_AGENT_MODE=false gcx --context=$CTX {resource} list -o $fmt > /dev/null 2>&1 \
&& echo "list $fmt: OK" || echo "list $fmt: FAIL"
doneStep 4C: Adapter Smoke (MANDATORY)
Every TypedCRUD resource MUST be verified via the adapter path:
resources schemas— registration visibleresources get {alias}— envelope + deserialization working
Step 4D: Spec Compliance
Check every acceptance criterion from spec.md. Report SATISFIED or UNSATISFIED with file:line evidence.
Step 4E: Recipe Update (MANDATORY)
Update gcx-provider-recipe.md with: 1. Status tracker entry — a new row for the ported provider 2. Gotchas section — problems discovered during smoke tests (or explicit "No new gotchas" if none) 3. Pattern corrections — if any recipe step was unclear or incorrect
Comparison Report
Produce a structured comparison report using templates/comparison-report.md. Present it to the user for review.
Phase 4 Gate
STOP. Do not declare the migration complete until:
>
1. The comparison report has been produced and presented to the user
2. Every discrepancy is either justified with written rationale or fixed
3. The recipe update (Step 4E) is complete
4. The user has explicitly approved the comparison report
---
Red Flags — STOP and Check
When you notice any of these during execution, stop and take the corrective action before continuing.
| Red Flag | Rationalization | STOP. Do this instead |
|---|---|---|
| Inferring response envelope shapes instead of copying from gcx source | "The response shape is obvious from the type definition" | Copy deserialization code verbatim from gcx. If gcx does json.Unmarshal(body, &slice), do the same. Never wrap in a struct unless the source does. |
Copying gcx client verbatim — embedding *grafana.Client, using c.Get()/c.Post() | "The gcx client already works, adapting it would just introduce bugs" | Translate to a typed HTTP client (plain http.Client + named endpoint methods). Read recipe Step 3. |
| Skipping the source audit — jumping to implementation | "I can see the important commands, a full audit is redundant" | Phase 0 is required. Every gcx subcommand must appear in the source summary. |
| Guessing endpoint names or paths | "The endpoint pattern is obvious from the resource name" | Read the gcx source for exact paths. Never guess. |
| Skipping smoke tests — marking Phase 4 complete without running commands | "The unit tests pass, so the implementation is correct" | Smoke tests are mandatory. Block and tell the user if no live instance is available. |
| Builder reading verification tasks — checking smoke commands during Phase 3 | "I need to check what smoke tests will run to make sure my code will pass" | Builders receive spec + plan + implementation tasks. Not verification tasks. |
| Build-Commands starting before Build-Core completes | "I can start on the command structure while Core finishes types" | Wait for Build-Core to complete. Commands depend on adapter interfaces. |
| Skipping a phase gate | "The previous phase was straightforward, I can proceed" | Every gate must be passed. No exceptions. |
| Producing custom artifact formats instead of spec documents | "A parity table is simpler than a full spec" | Use spec document format (spec.md, plan.md, tasks.md). No custom artifacts. |
Provider Commands Reference
Patterns for implementing CRUD redirect commands and ancillary subcommands. Reference: internal/providers/incidents/commands.go for working examples.
Output Format Compliance
Reference:docs/reference/design-guide.mdSection 1.3 ([ADOPT])
All provider commands must comply with the default format rules:
| Command type | Default format | Required codecs | K8s wrapping for json/yaml |
|---|---|---|---|
list | table | table + wide | Yes — wrap via ToResource |
get | table | table (single-row) | Yes — wrap via ToResource |
create -f | Status message | — | Return created resource if -o specified |
close / operational | Status message | — | No |
Key rules:
listandgetmust callioOpts.DefaultFormat("table")— do NOT leavejsonas defaultlistandgetmust register bothtableandwidecodecs viaRegisterCustomCodectablecolumns: key identifying fields (ID/UID, name/title, status)widecolumns: everything intable+ additional detail (timestamps, labels, counts)- json/yaml output wraps through
ToResourceto produce K8s-style envelope (apiVersion,kind,metadata,spec) - Operational/query commands (activity, severities, etc.) are exceptions — they may use different defaults if the data is not a standard resource
---
CRUD Redirect Pattern
Thin wrappers calling the client directly — NOT full re-implementations.
| Command | What it does |
|---|---|
{provider} list | client.List → table codec for table/wide, ToResource for json/yaml |
{provider} get <id> | client.Get → K8s envelope via ToResource, encode |
{provider} create -f <file> | Read YAML/JSON from file/stdin → parse unstructured → client.Create |
{provider} close <id> | Convenience: client.UpdateStatus with "resolved" (or equivalent) |
Key patterns
- All use
cmdio.Optionsfor output formatting (-o json/yaml/table/wide) listcalls client directly for table output (avoids unstructured
round-trip). For json/yaml, convert through ToResource for K8s envelope.
createaccepts file/stdin only — no flag-based convenience (--titleetc.)- No deprecation warnings — these are canonical paths
Table codec
Export the codec type so _test package can use it:
// IncidentTableCodec renders incidents as a tabular table.
type IncidentTableCodec struct {
Wide bool
}
func (c *IncidentTableCodec) Format() format.Format { ... }
func (c *IncidentTableCodec) Encode(w io.Writer, v any) error { ... }
func (c *IncidentTableCodec) Decode(_ io.Reader, _ any) error {
return errors.New("table format does not support decoding")
}Register in command setup:
opts.IO.RegisterCustomCodec("table", &IncidentTableCodec{})
opts.IO.RegisterCustomCodec("wide", &IncidentTableCodec{Wide: true})
opts.IO.DefaultFormat("table")Create from file/stdin
var reader io.Reader
if opts.File == "-" {
reader = cmd.InOrStdin()
} else {
f, err := os.Open(opts.File)
// ...
reader = f
}
var obj unstructured.Unstructured
yamlCodec := format.NewYAMLCodec()
if err := yamlCodec.Decode(reader, &obj); err != nil { ... }
res, _ := resources.FromUnstructured(&obj)
inc, _ := FromResource(res)
created, _ := client.Create(ctx, inc)Ancillary Subcommands
Map gcx subcommands that don't fit CRUD to provider Commands():
func (p *Provider) Commands() []*cobra.Command {
loader := &providers.ConfigLoader{}
cmd := &cobra.Command{
Use: "{provider}",
Short: p.ShortDesc(),
Aliases: []string{...},
}
loader.BindFlags(cmd.PersistentFlags())
cmd.AddCommand(
// CRUD redirects
newListCommand(loader),
newGetCommand(loader),
newCreateCommand(loader),
newCloseCommand(loader),
// Ancillary
newActivityCommand(loader),
newSeveritiesCommand(loader),
newOpenCommand(loader),
)
return []*cobra.Command{cmd}
}Common ancillary patterns
Activity/timeline — nested subcommand group:
{provider} activity list <id> [--limit N]
{provider} activity add <id> --body "..."Reference data — list-only:
{provider} severities list
{provider} roles listBrowser open — construct URL from restCfg.Host:
url := fmt.Sprintf("%s/a/grafana-{plugin}-app/{resource}s/%s", host, id)
exec.CommandContext(ctx, "open", url).Start()HTTP Client Reference Section Template
Phase 2 plan.md MUST include this section, filled in per provider. Copy this template and replace placeholders with concrete values from the gcx source.
Endpoint Table
| Method | Path | Purpose | Notes |
|---|---|---|---|
| GET | /api/v1/{resource} | List all resources | Pagination: ?page={n}&limit={n} or cursor |
| GET | /api/v1/{resource}/{id} | Get single resource | Returns unwrapped object (not envelope) |
| POST | /api/v1/{resource} | Create resource | Request body = resource JSON |
| PUT | /api/v1/{resource}/{id} | Update resource | Full replace, not PATCH |
| DELETE | /api/v1/{resource}/{id} | Delete resource | Returns 204 on success |
CRITICAL: Copy exact paths from gcx source. Do NOT guess paths — many APIs have non-obvious patterns (org-scoped paths, plugin proxy paths, gRPC-style POST-only endpoints).
Auth Helper Signature
// Standard Bearer token (same Grafana SA token):
func (c *Client) setAuth(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.token)
}
// Separate token (provider-specific):
func (c *Client) setAuth(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.providerToken)
}
// Token exchange (e.g., K6):
func (c *Client) setAuth(req *http.Request) {
req.Header.Set("Authorization", "Bearer "+c.exchangedToken)
}Document which pattern applies and any extra headers (e.g., X-Grafana-Url, X-Scope-OrgID).
Client Construction Pattern
type Client struct {
baseURL string // API base URL (trimmed trailing slash)
token string // Auth token (Bearer or provider-specific)
http *http.Client // Standard HTTP client with timeout
}
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
token: token,
http: &http.Client{Timeout: 30 * time.Second},
}
}Document exact field names — builders MUST use these names, not invent aliases. If the provider needs additional fields (instanceID, orgID, stackID), add them to the struct and constructor.
---
API Endpoint Gotchas
CRITICAL: Always check gcx source for exact endpoint paths. Don't guess.
Known inconsistencies in IRM API:
SeveritiesService.GetOrgSeverities(notSeverityService.GetSeverities)ActivityService.QueryActivity(notActivityService.QueryActivityItems)- Activity query wraps in
{"query": {...}}, not flat{...}
These naming inconsistencies are common in gRPC-style APIs. The ONLY reliable source is the gcx client code.
Go Conventions for Provider Ports
Conventions and linter gotchas discovered during provider migrations. Reference: internal/providers/incidents/ for working examples.
API Group Naming
Use {name}.ext.grafana.app/v1alpha1 — singular name:
incident.ext.grafana.app/v1alpha1 ✓
slo.ext.grafana.app/v1alpha1 ✓
alerting.ext.grafana.app/v1alpha1 ✓
incidents.ext.grafana.app ✗ (plural)
irm-incidents.grafana.app ✗ (no .ext, product prefix)Struct Tags
`omitzero` not `omitempty` for struct-typed fields. Go 1.24+ omitempty has no effect on structs. The modernize linter enforces this. Common case: custom time types wrapping time.Time.
// ✗ omitempty has no effect — FlexTime is a struct
CreatedTime FlexTime `json:"createdTime,omitempty"`
// ✓ omitzero correctly omits zero-valued structs
CreatedTime FlexTime `json:"createdTime,omitzero"`Linter Traps
`errchkjson` — requires checking json.Marshal return even for static maps. In init-time code, use panic:
b, err := json.Marshal(schema)
if err != nil {
panic(fmt.Sprintf("incidents: failed to marshal schema: %v", err))
}`testpackage` — test files must use package {name}_test. This means table codecs need to be exported (IncidentTableCodec, not incidentTableCodec) for tests to construct them.
`nestif` — complex nested ifs trigger this. Extract helper functions (e.g., resolveSchema() from a nested if-else chain).
`gci` — import ordering and struct field alignment. Run gci diff to see what it wants. Common issue: inconsistent spacing before struct tags.
Lint Compliance Checklist
Phase 0 fills this out per provider. Every applicable rule must be listed with its section reference before proceeding to Phase 1.
| Document | Section | Applicable? | Notes |
|---|---|---|---|
CONSTITUTION.md | CLI Grammar | Yes / No / Partial | {which rules apply to this provider's command tree} |
CONSTITUTION.md | Output Conventions | Yes / No / Partial | {format defaults, codec requirements} |
docs/reference/design-guide.md | 1.1 Naming | Yes / No / Partial | {noun choice, alias conventions} |
docs/reference/design-guide.md | 1.3 Output Formats | Yes / No / Partial | {table/wide/json/yaml compliance} |
docs/reference/design-guide.md | 1.4 Exit Codes | Yes / No / Partial | {error handling conventions} |
docs/reference/provider-guide.md | Provider Interface | Yes / No / Partial | {registration, ConfigKeys, TypedCRUD} |
docs/reference/provider-guide.md | Adapter Wiring | Yes / No / Partial | {GVK, aliases, schema/example} |
docs/reference/provider-discovery-guide.md | API Discovery | Yes / No / Partial | {auth model, endpoint patterns, pagination} |
docs/reference/provider-discovery-guide.md | Design Decisions | Yes / No / Partial | {adapter vs provider-only, resource modeling} |
Debug Logging
Use log/slog for debug-level logging in provider clients. Debug logs help diagnose issues during smoke tests without cluttering normal output.
slog.Debug("fleet: listing pipelines", "url", c.baseURL+"/pipelines")
slog.Debug("k6: token exchange complete", "orgID", orgID, "stackID", stackID)
slog.Debug("oncall: auto-discovered URL", "url", onCallURL, "source", source)Guidelines:
- Prefix with provider name:
slog.Debug("{provider}: {action}", ...) - Log request URLs and key parameters at Debug level
- Log auth resolution decisions (which fallback was used)
- Never log tokens or secrets — log presence only:
"hasToken", token != "" - Enable with
GCX_LOG_LEVEL=debugor--log-level=debug
Build Commands
GCX_AGENT_MODE=false make all # REQUIRED — agent mode changes default
# output formats, producing wrong docs
GCX_AGENT_MODE=false make lint # after agent phasesSchema + Example Registration
Add Schema and Example to adapter.Registration in init():
adapter.Register(adapter.Registration{
Factory: NewAdapterFactory(loader),
Descriptor: staticDescriptor,
Aliases: staticAliases,
GVK: staticDescriptor.GroupVersionKind(),
Schema: resourceSchema(), // json.RawMessage
Example: resourceExample(), // json.RawMessage
})Schema: static map[string]any with JSON Schema structure. Include apiVersion (const), kind (const), metadata, and spec with key user-facing fields. No external dependencies needed.
Example: static map[string]any matching gcx's Example{Resource}() output. Include realistic field values — this is what users see when they run gcx resources examples {alias}.
gcx → gcx Provider Migration Recipe
Evergreen document. Update this as providers are ported — add gotchas,
refine patterns, fix mistakes. Each migration agent should read this before
starting and update it after finishing.
Overview
This recipe covers porting a gcx resource client (pkg/grafana/{resource}/) into a gcx provider (internal/providers/{name}/). It's a streamlined path that skips API discovery (gcx already has working clients) and focuses on the mechanical translation.
When to use this recipe: Porting a gcx resource to gcx. When to use `/add-provider` instead: Building a provider from scratch for a product that doesn't have a gcx client.
Skill Structure
This recipe covers the mechanical implementation steps only (Steps 1-8). Workflow orchestration, phase gates, and verification are governed by SKILL.md — read it before starting any migration.
- Orchestration is defined in SKILL.md's five-phase pipeline (Phase 0–4).
- Phase gates in SKILL.md control when you may proceed between phases.
- Phase 0 (Requirements Gathering) produces the context bundle autonomously.
- Phase 1 (Design Discovery) produces the ADR via interactive brainstorming.
- Phase 2 (Spec Planning) produces spec.md + plan.md + tasks.md.
- Phase 3 (Build) executes this recipe's mechanical steps (Steps 1-8).
- Phase 4 (Verification) runs smoke tests and produces the comparison report.
If you are an agent reading this recipe: your orchestration comes from SKILL.md. This recipe provides the mechanical steps only.
Spec Document Format
Phase 2 produces three documents that replace the old custom audit artifacts (parity table, architectural mapping, verification plan):
- spec.md — functional requirements with FR-NNN numbering + acceptance
criteria in Given/When/Then format. Replaces the parity table.
- plan.md — architecture decisions + HTTP client reference section (endpoint
table, auth signature, client construction). Replaces the architectural mapping.
- tasks.md — dependency graph with waves + per-task deliverables including
mandatory smoke test tasks for all four output formats. Replaces the verification plan.
All three documents use YAML frontmatter. See commands-reference.md for the HTTP client reference section template that plan.md must include.
---
Prerequisites
Verify these before starting any port:
# 1. gcx binary is available
gcx --version
# 2. Grafana context is configured and working
gcx config view
gcx --context=<ctx> resources schemas | head -5
# 3. gcx uses the same context (or configure separately)
gcx --context=<ctx> health
# 4. Provider directory structure exists
# Use /add-dir or create manually:
mkdir -p internal/providers/{name}/{resource}If any of these fail, fix them before proceeding. Smoke tests (Phase 4) require live API access to both gcx and gcx against the same Grafana instance.
---
Pre-flight Checklist
Before starting a port, answer these questions:
[ ] 1. Is this resource already on K8s API?
Run: gcx --context=ops resources schemas | grep -i {resource}
If YES → no provider needed, it works via dynamic discovery.
[ ] 2. What's the gcx source?
Client: pkg/grafana/{resource}/client.go
Types: pkg/grafana/{resource}/types.go (or inline in client.go)
Cmd: cmd/resources/{resource}.go (or cmd/observability/ or cmd/oncall/)
[ ] 3. Auth model?
Same Grafana SA token: ConfigKeys = [] (reuse grafana.token)
Separate token: ConfigKeys = [{Name: "token", Secret: true}]
Separate URL + token: ConfigKeys = [{Name: "url"}, {Name: "token", Secret: true}]
[ ] 4. ID scheme?
String UID: metadata.name = uid (standard path)
Integer ID: metadata.name = strconv.Itoa(id) (needs int→string mapping)
Composite: metadata.name = slug-id or similar (document the scheme)
[ ] 5. Does it have cross-references?
e.g., synth checks reference probes by ID. If yes, the adapter needs
resolution logic in CreateFn/UpdateFn.
[ ] 6. Pagination?
gcx uses manual pagination loops. Check if the API has limit/offset,
cursor, or Link headers. The adapter's ListFn must handle this.---
Step-by-Step Port
Step 1: Create provider package
internal/providers/{name}/
├── provider.go # Provider interface + init() registration
├── {resource}/
│ ├── types.go # API structs (copy from gcx, adjust json tags if needed)
│ ├── client.go # HTTP client (adapt from gcx)
│ ├── adapter.go # TypedRegistration[T] wiring
│ └── client_test.go # httptest-based testsIf adding to an existing provider (e.g., adding a resource to grafana or iam), skip creating provider.go — just add the resource subpackage and register in the existing init().
Step 2: Port types.go
Copy structs from gcx/pkg/grafana/{resource}/. Adjustments:
- Keep json tags exactly as gcx has them — these match the API response
format and must round-trip losslessly through pull → edit → push.
- Remove gcx-specific helpers (e.g.,
func (t *Type) ResourceID() string)
— these are replaced by the adapter's NameFn.
- Keep all fields — don't prune "unnecessary" fields. The user may need them.
Step 3: Port client.go
Translate from gcx's grafana.Client to gcx's HTTP pattern:
// gcx pattern (before):
type Client struct {
*grafana.Client // embeds base client with .Get/.Post/.Put/.Delete
}
func NewClient(baseURL, token string) *Client {
return &Client{grafana.NewClient(baseURL, token)}
}
func (c *Client) ListResources(ctx context.Context) ([]Resource, error) {
var result []Resource
err := c.Get(ctx, "/api/path", &result)
return result, err
}// gcx pattern (after):
type Client struct {
baseURL string
token string
http *http.Client
}
func NewClient(baseURL, token string) *Client {
return &Client{
baseURL: strings.TrimRight(baseURL, "/"),
token: token,
http: &http.Client{Timeout: 30 * time.Second},
}
}
func (c *Client) List(ctx context.Context) ([]Resource, error) {
req, err := http.NewRequestWithContext(ctx, "GET", c.baseURL+"/api/path", nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+c.token)
resp, err := c.http.Do(req)
// ... handle response, decode JSON
}Key differences:
- No embedded base client — each provider owns its HTTP calls
- Explicit
context.Contexton all methods - Direct
http.NewRequestWithContextinstead of gcx's.Get()wrapper - Error handling: return
fmt.Errorf("{provider}: {action}: %w", err)with
provider name prefix for debuggability
Pagination: If gcx uses manual pagination loops, port them. If the API returns all results in one call, keep it simple.
Step 4: Wire adapter.go with TypedRegistration[T]
This is the part that TypedResourceAdapter[T] makes trivial:
package {resource}
import (
"context"
"github.com/grafana/gcx/internal/resources/adapter"
)
func Register(loader ConfigLoader) {
adapter.Register(adapter.TypedRegistration[ResourceType]{
Descriptor: Descriptor(),
Aliases: []string{"{alias}"},
GVK: GVK(),
Factory: func(ctx context.Context) (*adapter.TypedCRUD[ResourceType], error) {
cfg, err := loader.Load(ctx)
if err != nil {
return nil, err
}
client := NewClient(cfg.BaseURL, cfg.Token)
return &adapter.TypedCRUD[ResourceType]{
Namespace: cfg.Namespace,
NameFn: func(r ResourceType) string { return r.UID },
ListFn: client.List,
GetFn: client.Get,
CreateFn: client.Create,
UpdateFn: client.Update,
DeleteFn: client.Delete,
}, nil
},
})
}For int-ID resources, the NameFn converts:
NameFn: func(r Resource) string { return strconv.FormatInt(r.ID, 10) },Step 5: Register in init()
In provider.go:
func init() {
providers.Register(&Provider{})
{resource}.Register(&configLoader{})
}Note: The blank import in cmd/gcx/root/command.go is added in Step 7(Integration / Wiring), not here. Step 5 only covers provider.go.Step 6: Write tests
Minimum test coverage per resource:
1. Client tests — httptest server returning known JSON, verify List/Get/Create/Update/Delete parse correctly 2. Adapter round-trip — create a typed object → adapter wraps it → unwrap back → compare (no data loss)
Step 7: Integration / Wiring
After Build-Core and Build-Commands are complete, the integration task MUST:
1. Wire `Commands()` and `TypedRegistrations()` in the provider's init() 2. Add blank import in cmd/gcx/root/command.go 3. Fix import cycles introduced by subpackage references 4. Fix variable name collisions from package aliasing 5. Run `make lint` and fix all new issues
GCX_AGENT_MODE=false make all # MUST exit 0 — this is the Phase 3 gate
gcx providers # new provider listedStep 8: Smoke Test (Phase 4 — MANDATORY)
Phase 4 verification. This step maps to Phase 4 steps 4A–4E in SKILL.md.
Smoke tests are MANDATORY — they MUST NOT be marked "optional" or "if live
instance available". If no live instance is available, block and report.
>
Every show/list command MUST be tested with ALL FOUR output formats:
-o json,-o table,-o wide,-o yaml.
Run every command side-by-side with gcx against a real instance. Don't skip this — wrong endpoint names, wrapped request bodies, and response shape mismatches are invisible in unit tests.
8a. Structured Comparison (jq diff template)
CTX=dev # adjust to your context
# --- List: compare resource IDs ---
GCX_IDS=$(gcx --context=$CTX {resource} list -o json | jq -r '.[].id // .[].uid' | sort)
GCTL_IDS=$(gcx --context=$CTX {resource} list -o json | jq -r '.[].metadata.name' | sort)
echo "=== List ID diff ==="
diff <(echo "$GCX_IDS") <(echo "$GCTL_IDS") && echo "MATCH" || echo "MISMATCH"
# --- Get: compare key fields ---
ID="<pick-an-id-from-list>"
gcx --context=$CTX {resource} get $ID -o json | jq '{title, status, labels}' > /tmp/gcx_get.json
gcx --context=$CTX {resource} get $ID -o json \
| jq '{title: .spec.title, status: .spec.status, labels: .metadata.labels}' > /tmp/gctl_get.json
echo "=== Get field diff ==="
diff /tmp/gcx_get.json /tmp/gctl_get.json && echo "MATCH" || echo "MISMATCH"
# --- Adapter path ---
echo "=== Adapter path ==="
gcx --context=$CTX resources get {alias} > /dev/null 2>&1 && echo "resources get: OK" || echo "resources get: FAIL"
gcx --context=$CTX resources get {alias}/$ID -o json > /dev/null 2>&1 && echo "resources get/id: OK" || echo "resources get/id: FAIL"
# --- Ancillary commands (repeat per ancillary) ---
echo "=== Ancillary: {subcommand} ==="
gcx --context=$CTX {resource} {subcommand} -o json | jq length
gcx --context=$CTX {resource} {subcommand} -o json | jq length
# --- Schema + example ---
echo "=== Schema ==="
gcx --context=$CTX resources schemas -o json | jq 'to_entries[] | select(.key | test("{group}")) | .value' | head -5
echo "=== Example ==="
gcx --context=$CTX resources examples {alias} | head -10
# --- Output format check ---
echo "=== Output formats ==="
for fmt in table wide json yaml; do
GCX_AGENT_MODE=false gcx --context=$CTX {resource} list -o $fmt > /dev/null 2>&1 \
&& echo "$fmt: OK" || echo "$fmt: FAIL"
done8b. Paste Results
Copy the output from 8a into the conversation. For each comparison:
| Check | Expected | Action if fails |
|---|---|---|
| List ID diff | MATCH | Fix ListFn or adapter NameFn |
| Get field diff | MATCH (computed fields like durationSeconds may differ by seconds) | Fix types or ToResource mapping |
| Adapter path | OK | Fix resource_adapter registration |
| Ancillary counts | Equal | Fix endpoint name or response parsing |
| Schema/example | Non-empty | Fix register.go |
| Output formats | All OK | Fix codec registration |
STOP. Do not pass the Phase 4 gate until all checks pass or discrepancies
are explicitly justified (e.g., "durationSeconds differs by 2s — acceptable").
Do NOT skip smoke tests. The incidents port had two wrong endpoint names that only surfaced during smoke testing:
SeverityService.GetSeverities→ actuallySeveritiesService.GetOrgSeveritiesActivityService.QueryActivityItems→ actuallyActivityService.QueryActivity
---
Gotchas & Lessons Learned
Update this section after each provider port.
Auth
- OnCall uses a separate API URL discovered from the IRM plugin settings
(/api/plugins/grafana-irm-app/settings → jsonData.onCallApiUrl). The same Grafana SA token is used, plus an X-Grafana-Url header with the stack URL. The config loader checks GRAFANA_ONCALL_URL env → provider config → auto-discovery. Three-tier fallback avoids mandatory config for most users.
ID Mapping
- Integer IDs (annotations, reports, teams): Store as `metadata.name =
strconv.Itoa(id). The adapter's GetFn parses it back: id, _ := strconv.ParseInt(name, 10, 64)`.
- Slug+ID composites: Some resources use
slug-123patterns. Document the
scheme in the adapter so future maintainers know how to decompose.
Pagination
- gcx's
ListAllpattern uses page+limit loops. Port these directly — don't
try to be clever with streaming or lazy evaluation.
- Some APIs return wrapped responses (
{"items": [...], "totalCount": N}).
Define a listResponse struct per resource — don't try to share across types.
Cross-References
- Synth checks reference probes by numeric ID. The adapter resolves probe
names to IDs during Create/Update by calling the probe client. This logic lives in the adapter's CreateFn/UpdateFn closures.
gRPC-style POST APIs (Incidents/IRM)
- The IRM API uses gRPC-style POST endpoints (
IncidentsService.QueryIncidents,
IncidentsService.GetIncident, etc.) — all operations are POST with JSON bodies, not REST-style GET/POST/PUT/DELETE. The doRequest helper always uses POST.
- gcx's
GetIncidentfetches all incidents (limit 100) and filters client-side.
The actual API has a GetIncident endpoint — use it directly for O(1) lookups.
- The IRM API only supports status updates via
UpdateStatus— there is no
general-purpose PUT/PATCH for incident fields. The adapter's Update method extracts the status field and calls UpdateStatus.
FlexTimeis needed because the IRM API returns empty strings""for
optional time fields instead of null. The omitzero tag (Go 1.24+) replaces omitempty for struct-typed fields to satisfy the modernize linter.
- Delete is not supported — the IRM API has no delete endpoint.
- Cursor-based pagination: the
contextPayloadfield carries the cursor value
between pages, not a separate cursor parameter.
Token Exchange Auth (K6)
- K6 uses a separate API domain (
api.k6.io), not the Grafana stack URL. - Auth requires a two-step token exchange: AP token → k6 v3 token via
PUT /v3/account/grafana-app/start with X-Grafana-Key, X-Stack-Id, X-Grafana-Service-Token headers.
- The stack ID can be parsed from the gcx namespace (
stack-{id}),
avoiding the need for a separate GCOM call.
- The org ID (needed for env vars) comes from the auth response, not config.
- The
perfsprintlinter enforceserrors.Newoverfmt.Errorffor strings
without format verbs — easy to miss when porting fmt.Errorf("...") patterns.
- The
usestdlibvarslinter enforceshttp.StatusCreatedetc. instead of
raw 201/204/404 literals — gcx uses raw numbers everywhere.
- gcx `k6 token` vs gcx `k6 auth token`: gcx exposes token exchange
as a top-level token subcommand; gcx nests it under auth token. Both print the short-lived API token to stdout.
- Schedules `delete` takes `<load-test-id>` not `<schedule-id>`: This
is consistent with the API — delete is keyed on the load test, not the schedule object. This is also how gcx does it.
- `runs` appears in two places:
k6 runs list(top-level) and
k6 testrun runs list (nested under testrun). Both delegate to the same underlying run listing function. The duplication is intentional — the testrun sub-tree groups CRD-related operations together.
- gcx `schema` / `example` subcommands: gcx exposes per-resource
schema
and example subcommands under each resource group. gcx covers these via resources schemas and resources examples at the global level. These are NOT missing — the coverage is different but equivalent.
Multi-Resource Providers (OnCall pattern)
- For providers with many sub-resource types (OnCall has 12), use a generic
subResourceAdapter with a switch dispatch on kind rather than 12 separate adapter files. This keeps the code in one package instead of 12 subpackages.
- Register all sub-resources under the same API group (
oncall.ext.grafana.app)
with different kinds (Integration, Schedule, AlertGroup, etc.).
- Use
oncall-*prefixed aliases to avoid conflicts with core resource types
(e.g., oncall-teams not teams to avoid clashing with K8s-native resources).
- The
X-Grafana-Urlheader must use canonical Go form (X-Grafana-Urlnot
X-Grafana-URL) or the canonicalheader linter will flag it. httptest servers receive the canonical form regardless of how you set it.
Plugin Proxy APIs (Knowledge Graph / Asserts)
- KG/Asserts uses the Grafana plugin resource proxy path:
/api/plugins/grafana-asserts-app/resources/asserts/api-server/...
- Auth: standard Grafana SA token via rest.Config — no separate token needed.
gcx passes X-Scope-OrgID: 0 but this is not required through the plugin proxy.
- The API is operational, not CRUD: many query endpoints (POST), config uploads
(PUT with application/x-yaml), and read endpoints (GET).
- Rules are the closest to a standard resource (list/get/create/delete) and map
well to the ResourceAdapter pipeline. Other sub-resources (datasets, entities, assertions) are best served as provider commands.
- The command tree is large (~20 subcommands) — use inline closures for each
command rather than trying to share RunE builders.
Plugin Proxy APIs (Faro / Frontend Observability)
- Faro uses two different plugin proxy base paths:
- CRUD:
/api/plugin-proxy/grafana-kowalski-app/api-proxy/api/v1/app - Sourcemaps:
/api/plugins/grafana-kowalski-app/resources/api/v1/app/{id}/sourcemaps - Auth: standard Grafana SA token via
rest.HTTPClientFor— no separate token needed. - API quirks preserved from gcx source:
- Create MUST strip
ExtraLogLabels(API returns 409) andSettings(API returns 500). - Update MUST strip
Settings(API returns 500). - Create response is incomplete (missing
collectEndpointURL,appKey) — must re-fetch
via List after creation to get full details.
- Update requires ID in both URL path and request body.
GetByNameis client-side: list all apps, filter by name (no server-side endpoint).- Wire format conversion:
ExtraLogLabelsismap[string]stringin Go but
[]{"key": k, "value": v} on the wire. ID is string in Go but int64 on wire. Internal toAPI()/fromAPI() handles both conversions.
- Sourcemaps are sub-resources (require parent app-id for all operations).
Per CONSTITUTION § Sub-resources, they use alternative verbs (show-sourcemaps, apply-sourcemap, remove-sourcemap) and are NOT adapter-registered.
- Sourcemaps plugin endpoint returns 500 on dev/ops instances as of 2026-04-02.
This is a Faro plugin bug, not a gcx code issue. The request is correctly constructed (verified via -vvv debug logging).
- Resource plural is `apps` (not
faroapps), so the full GVK selector is
apps.v1alpha1.faro.ext.grafana.app. Short form: resources get apps.
Response Shape Differences
- Some gcx clients unwrap response envelopes (e.g.,
response.Data) while
others return the raw response. Check the gcx client carefully — the types you port must match what the API actually returns, not what gcx exposes.
Separate API URLs (Fleet, OnCall)
- Fleet Management uses a separate API URL, not the Grafana instance URL.
Use ConfigKeys with url, instance-id, token for provider config. The configLoader pattern from synth (LoadFleetConfig vs synth's LoadSMConfig) works well — extract credentials from providers["fleet"] config map + env vars.
- Fleet uses basic auth (
instance-id:token) when instance-id is set,
otherwise Bearer token. The NewClient(url, instanceID, token, useBasicAuth) pattern handles both modes via the useBasicAuth flag.
- Discovery and instrumentation commands need additional context (prom cluster/instance IDs)
that currently require GCOM stack info — not ported yet, deferred to GCOM provider.
---
Provider Status Tracker
| Provider | Resources | Status | Ported By | Notes |
|---|---|---|---|---|
| synth | checks, probes | ✅ existing | — | Reference impl, refactored to TypedAdapter in Phase 0 |
| slo | definitions, reports | ✅ existing | — | Reference impl |
| alert | rules, groups | ✅ existing | — | Read-only, expanding in Phase 2 |
| oncall | 12 sub-resources | ✅ done (2026-03-20) | Claude | All 12 sub-resources, iterator pagination, auto-discovery of OnCall URL |
| incidents | incidents | ✅ done (2026-03-20) | Claude | IRM plugin API, gRPC-style POST endpoints |
| k6 | projects, tests, runs, envs, schedules, load-zones, envvars | ✅ done + verified (2026-03-24) | Claude | Token exchange auth, separate API domain. Full command tree verified live against dev context. Schedules, load-zones, and testrun CRD commands added beyond original scope. |
| fleet | pipelines, collectors, tenant | ✅ done (2026-03-20) | Claude | gRPC/Connect API, separate URL + basic auth, 3 resource types |
| kg | datasets, rules, entities, assertions, search | ✅ done (2026-03-20) | Claude | Plugin proxy API, 20+ subcommands, rules as ResourceAdapter |
| ml | jobs, holidays | ⬜ planned | — | Phase 1.6 |
| scim | users, groups | ⬜ planned | — | Phase 1.7 |
| gcom | access policies, stacks, etc. | ⬜ planned | — | Phase 1.8 |
| adaptive | metrics, logs, traces | ⬜ planned | — | Phase 1.9 |
| faro | apps, sourcemaps | ✅ done (2026-04-02) | Claude | Plugin proxy API, TypedCRUD[FaroApp], sourcemaps as sub-resource verbs. Sourcemap smoke blocked by Faro plugin 500. |
| grafana | annotations, lib panels, etc. | ⬜ planned | — | Phase 3 (non-K8s REST) |
| iam | permissions, RBAC, SSO, OAuth | ⬜ planned | — | Phase 3-4 |
---
Tips for Complex Providers
Speculative — written before these providers were ported. Validate
and update during the actual port.
OnCall (12 sub-resources):
- Start with
integrations— simplest, validates the pattern - OnCall API URL discovered via GCOM, not configured directly
- Iterator-based pagination — port the pattern, don't simplify
K6 (multi-tenant auth):
- Two auth modes: org-level and stack-level
- Separate API domain (not Grafana stack URL)
- Check gcx's
k6/client_envvar_test.gofor auth resolution logic
Fleet/Alloy (4 sub-resource types):
- All share same base URL and auth
- Single provider, four subpackages
---
Relationship to /add-provider Skill
This recipe is for porting existing gcx clients. The /add-provider skill is for building providers from scratch. Key differences:
| Aspect | This Recipe | /add-provider Skill |
|---|---|---|
| API discovery | Skip — gcx has working client | Full discovery phase |
| Types | Copy from gcx | Derive from OpenAPI/source |
| Client | Adapt from gcx | Hand-write from scratch |
| Design doc | Optional (pattern is known) | Required per stage |
| Auth | Copy gcx's auth model | Investigate from scratch |
After porting, the provider must pass Phase 4 verification (SKILL.md steps 4A–4E) including mandatory smoke tests with all four output formats.
Builder Spawn Prompt Templates
Templates for Phase 3 builder agents. Each builder receives spec.md (what), plan.md (how), and their assigned implementation tasks — not verification tasks.
---
Build-Core Spawn Prompt
You are Build-Core for the {provider} provider migration.
## Your Task
Implement the core adapter files for the {provider} provider.
**You own ONLY these files:**
- `internal/providers/{name}/types.go`
- `internal/providers/{name}/client.go`
- `internal/providers/{name}/client_test.go`
- `internal/providers/{name}/adapter.go`
- `internal/providers/{name}/resource_adapter.go`
Do NOT create or modify provider.go or any CLI command files. Those are owned by Build-Commands.
## CRITICAL: Response Envelope Shapes
Do NOT infer response envelope shapes. Copy deserialization code verbatim from
the grafana-cloud-cli source. If the source does `json.Unmarshal(body, &slice)`,
the new client MUST do the same — never wrap in a struct unless the source does.
## References
- **Spec**: {spec_path}/spec.md — functional requirements and acceptance criteria
- **Plan**: {spec_path}/plan.md — architecture decisions and HTTP client reference
- **Recipe**: `gcx-provider-recipe.md` Steps 2-4 (types, client, adapter/resource_adapter)
- **Conventions**: `conventions.md` — struct tags, linter traps, debug logging
## Implementation Tasks
{paste assigned task excerpts from tasks.md here}
## Completion
When all files are implemented and `make lint` passes on your files:
1. Mark the Build-Core task complete.
2. Send a message to the lead confirming completion and listing the files you created.---
Build-Commands Spawn Prompt
You are Build-Commands for the {provider} provider migration.
## Your Task
Implement the provider registration and CLI commands for the {provider} provider.
The core adapter (types, client, adapter, resource_adapter) has already been
implemented by Build-Core.
**You own ONLY these files:**
- `internal/providers/{name}/provider.go`
- `cmd/gcx/providers/{name}/commands.go`
- `cmd/gcx/providers/{name}/*_test.go` (command tests)
- The blank import line in `cmd/gcx/root/command.go`
Do NOT modify types.go, client.go, adapter.go, or resource_adapter.go.
Those are owned by Build-Core.
## CRITICAL: Response Envelope Shapes
Do NOT infer response envelope shapes. Copy deserialization code verbatim from
the grafana-cloud-cli source. If the source does `json.Unmarshal(body, &slice)`,
the new client MUST do the same — never wrap in a struct unless the source does.
## References
- **Spec**: {spec_path}/spec.md — functional requirements and acceptance criteria
- **Plan**: {spec_path}/plan.md — architecture decisions and HTTP client reference
- **Recipe**: `gcx-provider-recipe.md` Steps 5-7 (provider registration, tests, integration/wiring)
- **Commands Reference**: `commands-reference.md` — CRUD redirect patterns, codec usage
## Implementation Tasks
{paste assigned task excerpts from tasks.md here}
## Notes
- The adapter interfaces are already implemented by Build-Core. Import and use them;
do not modify them.
- Before starting any command implementation that imports the adapter, confirm that
the Build-Core task is marked complete.
## Completion
When all files are implemented and `make lint` passes on your files:
1. Send a message to the lead confirming completion and listing the files you created.Comparison Report Template
Copy this template and fill it in for every command in the verification tasks from tasks.md. Every row must have a status. Do not omit commands or mark them "skipped".
## Comparison Report: {provider}
### Per-Command Pass/Fail
| command | status | captured output (truncated) |
|---------|--------|-----------------------------|
| gcx {resource} list | PASS / FAIL | {first 3 lines of output or error} |
| gcx {resource} list | PASS / FAIL | {first 3 lines of output or error} |
| gcx {resource} get {id} | PASS / FAIL | {first 3 lines} |
| gcx {resource} get {id} | PASS / FAIL | {first 3 lines} |
| gcx resources get {alias} | PASS / FAIL | {first 3 lines} |
| gcx {resource} {subcommand} | PASS / FAIL | {first 3 lines} |
### Per-Command Output Format Verification (MANDATORY)
Every show/list command MUST be tested with all four output formats.
Do NOT skip any format or mark as "not applicable".
| command | json | table | wide | yaml |
|---------|------|-------|------|------|
| gcx {resource} list | PASS / FAIL | PASS / FAIL | PASS / FAIL | PASS / FAIL |
| gcx {resource} get {id} | PASS / FAIL | PASS / FAIL | PASS / FAIL | PASS / FAIL |
| gcx {resource} {subcommand} | PASS / FAIL | PASS / FAIL | PASS / FAIL | PASS / FAIL |
For each FAIL: capture the error message and root cause.
### Adapter Smoke Results (MANDATORY)
Every TypedCRUD resource MUST be verified via the adapter path.
| resource alias | `resources schemas` visible? | `resources get {alias}` works? | `resources get {alias}/{id} -o json` works? | notes |
|----------------|------------------------------|-------------------------------|---------------------------------------------|-------|
| {alias} | YES / NO | YES / NO | YES / NO | {error details if NO} |
**Fail criteria:** Any NO in the first three columns is a registration or
adapter wiring bug that must be fixed before the report is approved.
### List ID Comparison
=== List ID diff === {paste full diff output here, or "MATCH" if identical}
Verdict: MATCH | MISMATCH
If MISMATCH: {describe which IDs differ and probable cause}
### Get Field Comparison
=== Get field diff === {paste full diff output here, or "MATCH" if identical}
Verdict: MATCH | MISMATCH
If MISMATCH: {describe which fields differ -- note any acceptable differences
such as computed fields that differ by small values}
### Discrepancy Summary
| # | description | verdict | rationale or fix |
|---|-------------|---------|-----------------|
| 1 | {describe any mismatch or unexpected behavior} | justified / fix required | {written rationale or PR link} |
(Leave table empty if no discrepancies found.)
### Overall Verdict
**PASS** / **FAIL** — {one-line summary}
If FAIL: list blocking issues that must be resolved before approval.Verifier Spawn Prompt Template
Template for Phase 4 verification agent. The verifier receives the comparison report template, spec acceptance criteria, and verification tasks — not implementation details.
---
Verify Spawn Prompt
You are the Verify agent for the {provider} provider migration.
## Your Task
Execute the Phase 4 verification steps and produce a structured comparison report.
You test behavior, not implementation structure. Derive all expected behavior
from the spec acceptance criteria and verification tasks below.
## Step 4A: Build Gate
Run `GCX_AGENT_MODE=false make all` and confirm it exits 0 with no lint errors
and all tests passing. If it fails, report the failure and STOP — do not
proceed to smoke tests.
## Step 4B: Smoke Tests (MANDATORY)
Run every show/list command against a live Grafana instance. Each command MUST be
tested with ALL FOUR output formats: `-o json`, `-o table`, `-o wide`, `-o yaml`.
Smoke tests are MANDATORY. If no live instance is available, STOP and report
the blocker to the user. Do NOT skip smoke tests or mark them "optional".
CTX={context-name}
Per-command smoke (repeat for EVERY show/list command):
for fmt in json table wide yaml; do GCX_AGENT_MODE=false gcx --context=$CTX {resource} list -o $fmt > /dev/null 2>&1 \ && echo "list $fmt: OK" || echo "list $fmt: FAIL" GCX_AGENT_MODE=false gcx --context=$CTX {resource} get {id} -o $fmt > /dev/null 2>&1 \ && echo "get $fmt: OK" || echo "get $fmt: FAIL" done
## Step 4C: Adapter Smoke (MANDATORY)
Every TypedCRUD resource MUST be verified via the adapter path:
Registration visible:
gcx --context=$CTX resources schemas -o json | jq 'to_entries[] | select(.key | test("{group}"))'
Envelope + deserialization:
gcx --context=$CTX resources get {alias} -o json | head -5 gcx --context=$CTX resources get {alias}/{id} -o json | head -5
## Step 4D: Spec Compliance
Check every acceptance criterion from spec.md with file:line evidence.
Check every negative constraint. Report SATISFIED or UNSATISFIED for each.
## Step 4E: Recipe Update (MANDATORY)
You MUST update `gcx-provider-recipe.md` before completing:
1. **Status tracker entry** — add a row for the ported provider in the Provider
Status Tracker table. Required even if no issues found.
2. **Gotchas section** — record any problems discovered during smoke tests.
Write "No new gotchas" explicitly if none found.
3. **Pattern corrections** — if any recipe step was unclear or incorrect,
fix it. Document what you changed and why.
## Deliverables
1. **Comparison report** — fill in the template from `templates/comparison-report.md`
and present it to the user. Every section is mandatory.
2. **Recipe update** — the three items from Step 4E above.
## Verification Tasks
{paste verification task excerpts from tasks.md here}
## Completion
Present the comparison report to the user. The user MUST review and approve
the report before the migration is declared complete. Do not declare completion
without user approval.Related skills
FAQ
What does migrate-provider do?
migrate-provider skill documents Use when porting a Grafana Cloud product from grafana-cloud-cli (gcx) to gcx, when a bead task references gcx provider migration, or when user says "migrate provider", "port from gcx", "port oncall", "port k6".
When should I use migrate-provider?
User asks about migrate-provider, use when porting a grafana cloud product from grafana-cloud-cli (gcx) to gcx, when a bead .
Is this skill safe to install?
Review the Security Audits panel on this page before installing in production.