Guidance for AI agents working in this monorepo. Keep changes surgical and idiomatic to
the surrounding code. This file complements — and does not restate — the Copilot-specific
guidance in .github/instructions/ and the human-facing CONTRIBUTING.md.
| Path | Contents |
|---|---|
charms/ |
Four Juju charms (see below) plus shared integration tests in charms/tests/integration/. |
cmd/ |
Go application entry points: planner, webhook-gateway. |
internal/ |
Shared Go packages (database, github, planner, queue, server, telemetry, webhook, …) — the application logic the paas charms package and deploy. |
*-rockcraft.yaml, build-*-rock.sh (repo root) |
Rock/image build definitions and their build scripts. |
docs/ |
Diátaxis-structured docs (Read the Docs). ADRs in docs/adr/. |
actions/, runner_grafana_dashboards/ |
A GitHub Action and runner-host dashboards. |
parts, prime, stage, **/lib/charms/** |
Generated or vendored — do not edit. lib/charms/** is auto-updated; see .github/instructions/charms-lib-updates.instructions.md. |
The Go layout follows the community Go project layout.
README.md has the canonical layout (note: it predates the garm and garm-configurator
charms — there are four charms, not two).
| Charm | Base | Role |
|---|---|---|
garm |
paas_charm.go.Charm |
GARM (GitHub Actions Runner Manager) — 12-factor, PostgreSQL, OpenStack provider. |
planner-operator |
paas_charm.go.Charm |
Runner planner API — 12-factor Go app. |
webhook-gateway-operator |
paas_charm.go.Charm |
GitHub webhook receiver/forwarder — 12-factor Go app. |
garm-configurator |
ops.CharmBase |
Config broker for GARM scalesets. The only direct-ops charm. |
- Per-charm Python checks — from the charm directory,
tox -c tox.toml(envsfmt,lint,complexity,static,unit,coverage-report; ruff, codespell, pyright, pytest+coverage). CI runs these per charm viatox -c tox.toml. - Integration tests (root
tox.ini) —tox -e <charm>-integration(garm,webhook-gateway,planner,garm-configurator) ortox -e charms-integrationfor all. Requires a live Juju model (jubilant + pytest-operator). actions/Python —tox -e actions-lint,tox -e actions-static,tox -e actions-unit.- Go —
go test ./.... charmcraft pack— build a charm (run from the charm dir; not wired into tox).- Docs spellcheck — CI runs Vale over
docs/withCanonical.000-US-spellcheckat error level, so an unknown technical term (e.g.deserialize) fails the build. Add project-specific terms — regex forms like[Dd]eserializ(e|es|ed|ing|ation)are supported — todocs/.custom_wordlist.txt(the docsMakefileappends it to the Canonical accept vocabulary); verify withmake -C docs spellcheckbefore pushing adocs/change. - Gates from
CONTRIBUTING.md: ≥ 85% coverage on internal packages, cyclomatic complexity < 10 per function.
These are K8s 12-factor charms on the go-framework charmcraft extension: a thin Python
charm layer wraps a Go workload (CONTRIBUTING.md §"12 factor"; each charmcraft.yaml).
garm-configurator is the exception — a plain ops charm.
Read full current state, act idempotently, and set unit status once.
For paas_charm charms (garm, planner-operator, webhook-gateway-operator) — the base class already runs the holistic flow (PaasCharm.restart()):
- DON'T implement a separate reconcile —
restart()already is the reconcile. A thin_reconciledispatcher that just callsself.restart()is fine for routing multiple hook observers through one entry point; a_reconcilethat implements its own reconcile logic is not. - DO inject behaviour by overriding a framework hook and calling
super()— e.g.restart()(garm: write config + first-run;planner-operator: sync relation endpoints) or_create_app()(planner-operator/webhook-gateway-operator: inject OTel env). - DO gate on readiness with an early return (
if not self.is_ready(): return); DON'T callevent.defer().
For garm-configurator (plain ops):
- DO keep the single
_reconcilethat every event observes (GarmConfiguratorCharm._reconcile): build state viaCharmState.from_charm(self), write relation data, set status once at the end.
- Secrets — owner vs observer.
refresh=Trueis an observer concept: it advances the unit's tracked revision to the latest.- DO pass
get_content(refresh=True)only when consuming a secret you don't own (an operator-supplied config secret, or one granted over a relation), especially insecret-changedhandlers — e.g. the*.from_charmresolvers incharms/garm-configurator/src/charm_state.py. - DON'T pass
refresh=Truefor a secret you own (created viaadd_secret): plainget_content()already returns the latest revision andset_content()invalidates the cache — e.g.garm's_get_secrets/_get_admin_credentials. Usepeek_content()to read the latest without changing tracking. - DO create labelled secrets on the leader so other units can fetch them by
label.
- DO pass
- DO put the secret id/URI in relation data; DON'T put the content:
add_secret(..., label=...)→secret.grant(relation)→relation.data[self.app]["token"] = str(secret.id)(planner-operator's_create_relation_credentials). - DO serialise structured values (e.g.
json.dumps) — relation databags are string-only.
- New unit tests use Scenario (
scenario.Context/State/Relation/Secret), notHarness— seecharms/garm-configurator/tests/unit/. - DO give every Python test an arrange/act/assert docstring — the convention
STYLE.mddocuments for Go (// Arrange:comments), spelled as a docstring:State the why in the assert line where it isn't obvious ("so other units can fetch them by label"), not just the mechanical result. For adef test_leader_creates_garm_secrets(): """ arrange: A leader unit with no GARM secrets yet. act: Run install. assert: Both labelled secrets are created, so other units can fetch them by label. """
parametrized test, write one docstring covering the cases — DON'T write one per case. - DO fix a missing AAA docstring on any test you move or edit. It's followed unevenly
(
planner-operatorandgarmyes;garm-configuratorandwebhook-gateway-operatornot yet), so imitating the nearest neighbour is not a reliable guide. - Integration tests live in the shared
charms/tests/integration/.
We borrow from the canonical
charm-engineer.agent.md,
but some of its rules assume a hand-written ops charm and do not apply to our paas charms:
- No separate reconcile logic —
restart()is the reconcile (see above). A thin_reconciledispatcher that just callsself.restart()is fine; don't implement reconcile logic outsiderestart(). - No hand-authored
workload.py/ Pebble layer — thego-frameworkextension owns the workload; touch Pebble only inside arestart()override when strictly necessary. - A
CharmStateabstraction is optional but encouraged where it cutscharm.pycomplexity (per ISD014).garm-configuratoruses a PydanticCharmState;garmuses adataclasses-basedcharm_state.py(CharmState.from_charm) for relation-derived desired state. Don't force one onto a trivial paas charm, and keep it dependency-free ofcharm.py(no import cycle).
Still applicable: holistic state handling, idempotent install, explicit port handling, small
try/except blocks scoped to custom exceptions, no Canonical-internal references in charm
code, and avoid level=alive health checks in rockcraft.yaml.
- Idiomatic Go; standard library first, third-party only when it's the established choice.
- Table-driven tests; keep functions under the complexity gate; meet the coverage gate.
internal/is the workload thatplanner-operatorandwebhook-gateway-operatordeploy — changes here ripple into those charms.
.github/instructions/— guidance in GitHub Copilot's custom-instructions format (applyTofrontmatter), auto-synced from upstreamcanonical/copilot-collections(pinned in.copilot-collections.yaml) by the weeklycopilot-collections-update.ymlworkflow. The format/delivery is Copilot-specific, but most of the content (code-commenting style, documentation rules, the charm-library-update review protocol) is general guidance worth following regardless of tool.CONTRIBUTING.md— dev workflow, coverage/complexity gates, the 12-factor reference.
- Stays in sync with the code via
scripts/check_agents_md.py(run in CI byagents_md_check.yaml): it fails if a cited path, private method, ortox -eenv no longer exists. Cite code by symbol name, not line number, so references survive refactors. - Stays in sync with copilot-collections: this file is a hand-curated adaptation of the
upstream guidance (it deliberately diverges for 12-factor — see above), so it can't be
auto-generated. When the weekly bump PR changes
.copilot-collections.yamlor.github/instructions/, re-read the "12-factor divergences" section above (the PR-template checklist prompts this).