Skip to content

Latest commit

 

History

History
153 lines (121 loc) · 11.8 KB

AGENTS.md

File metadata and controls

153 lines (121 loc) · 11.8 KB

AGENTS.md

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.

Repository map

Path Contents
charms/ Four Juju charms (see below) plus shared integration tests in charms/tests/integration/, the GARM end-to-end test in charms/tests/e2e/, and model-free tests of that shared test-support code in charms/tests/unit/.
cmd/ Go application entry points: planner, waiting-p80-report, 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.

Charms

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.

Build & test

  • Per-charm Python checks — from the charm directory, tox -c tox.toml (envs fmt, lint, complexity, static, unit, coverage-report; ruff, codespell, pyright, pytest+coverage). CI runs these per charm via tox -c tox.toml. Unit-test dependencies live in each charm's test-requirements.txt, which the TICS workflow also installs, so add new test imports there, not inline in tox.toml.
  • Integration tests (root tox.ini) — tox -e <charm>-integration (garm, webhook-gateway, planner, garm-configurator) or tox -e charms-integration for all. Requires a live Juju model (jubilant + pytest-operator).
  • GARM end-to-end test (root tox.ini) — tox -e garm-e2e. Runs against a real OpenStack tenant from garm_e2e.yaml, is manually dispatched, one run at a time repository-wide, and is not a merge gate. See CONTRIBUTING.md §"GARM E2E".
  • Shared test-support unit tests (root tox.ini) — tox -e charms-tests-unit. Model-free tests for the helpers under charms/tests/ (dispatch/credential helpers, the diagnostic redactor). A merge gate, unlike the suites that consume those helpers.
  • 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/ with Canonical.000-US-spellcheck at 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 — to docs/.custom_wordlist.txt (the docs Makefile appends it to the Canonical accept vocabulary); verify with make -C docs spellcheck before pushing a docs/ change.
  • Gates from CONTRIBUTING.md: ≥ 85% coverage on internal packages, cyclomatic complexity < 10 per function, and — for garm and garm-configurator only — cognitive complexity ≤ 15 per function (enforced by flake8-cognitive-complexity in the per-charm complexity env; exceptions carry # noqa: CCR001 plus a tracking-issue reference).

Charm conventions

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.

Holistic state handling

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 _reconcile dispatcher that just calls self.restart() is fine for routing multiple hook observers through one entry point; a _reconcile that 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 call event.defer().

For garm-configurator (plain ops):

  • DO keep the single _reconcile that every event observes (GarmConfiguratorCharm._reconcile): build state via CharmState.from_charm(self), write relation data, set status once at the end.

Ops / Juju lifecycle idioms to take care of

  • Secrets — owner vs observer. refresh=True is 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 in secret-changed handlers — e.g. the *.from_charm resolvers in charms/garm-configurator/src/charm_state.py.
    • DON'T pass refresh=True for a secret you own (created via add_secret): plain get_content() already returns the latest revision and set_content() invalidates the cache — e.g. garm's _get_secrets / _get_admin_credentials. Use peek_content() to read the latest without changing tracking.
    • DO create labelled secrets on the leader so other units can fetch them by label.
  • 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.

Testing

  • New unit tests use Scenario (scenario.Context / State / Relation / Secret), not Harness — see charms/garm-configurator/tests/unit/.
  • DO give every Python test an arrange/act/assert docstring — the convention STYLE.md documents for Go (// Arrange: comments), spelled as a docstring:
    def 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.
        """
    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 a 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-operator and garm yes; garm-configurator and webhook-gateway-operator not yet), so imitating the nearest neighbour is not a reliable guide.
  • Integration tests live in the shared charms/tests/integration/; the GARM end-to-end test lives in charms/tests/e2e/, kept separate so it stays out of the PR test matrix.
  • DO put a model-free test of shared test-support code in charms/tests/unit/, not beside the suite it supports: charms-integration collects the whole charms/tests/integration/ directory, so a unit test parked there only ever runs on a live-model job.

Function ordering — the step-down rule

Applies to Python and Go alike: order functions and methods by the "step-down" rule — a module or class reads top-to-bottom, from general to specific, so a caller sits above its callee, with closely related helpers grouped adjacently. The common violation is extracting a helper (e.g. to get under the complexity gates above) and parking it at the top of the module away from its caller: DO place each extracted helper directly below its first caller instead.

12-factor divergences from the canonical charm-engineer guidance

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 _reconcile dispatcher that just calls self.restart() is fine; don't implement reconcile logic outside restart().
  • No hand-authored workload.py / Pebble layer — the go-framework extension owns the workload; touch Pebble only inside a restart() override when strictly necessary.
  • A CharmState abstraction is optional but encouraged where it cuts charm.py complexity (per ISD014). garm-configurator uses a Pydantic CharmState; garm uses a dataclasses-based charm_state.py (CharmState.from_charm) for relation-derived desired state. Don't force one onto a trivial paas charm, and keep it dependency-free of charm.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.

Go code (cmd/, internal/)

  • 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 that planner-operator and webhook-gateway-operator deploy — changes here ripple into those charms.

Existing guidance (read, don't duplicate)

  • .github/instructions/ — guidance in GitHub Copilot's custom-instructions format (applyTo frontmatter), auto-synced from upstream canonical/copilot-collections (pinned in .copilot-collections.yaml) by the weekly copilot-collections-update.yml workflow. 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.

Keeping this file honest

  • Stays in sync with the code via scripts/check_agents_md.py (run in CI by agents_md_check.yaml): it fails if a cited path, private method, or tox -e env 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.yaml or .github/instructions/, re-read the "12-factor divergences" section above (the PR-template checklist prompts this).