12factor-charm
Installation
SKILL.md
12factor Charm
Always build the charm in charm/. Treat paas-charm as part of the contract.
Skill Order
- Run
$12factor-fitfirst when possible. - Run this skill before
$12factor-juju-terraform. - This skill and
$12factor-rockcan proceed independently once the fit verdict and framework are confirmed.
Workflow
- Reuse the fit verdict from
$12factor-fitif available. If not, confirm the framework and deployment context yourself. - Create or use a dedicated
charm/subdirectory. Do not generate the charm at project root. - If the framework is FastAPI, Go, ExpressJS, or Spring Boot, verify the user
accepted the experimental extension path and that
charmcraftis on an edge channel withCHARMCRAFT_ENABLE_EXPERIMENTAL_EXTENSIONS=true. - Run the exact profile from
references/framework-charm-contracts.md, for examplecharmcraft init --profile fastapi-framework --name <charm-name>insidecharm/. Always use this command to generate the charm — never copy from templates, previously generated files, or example charms. - Inspect the generated
charmcraft.yaml,requirements.txt, andsrc/charm.py. - Load
references/framework-charm-contracts.mdandreferences/paas-charm-runtime.md. - Run
scripts/inspect_env_keys.py <repo> --framework <framework>to inventory application env expectations before adding config or workload-side env bridges. - If the repo already has a
rockcraft.yaml, inspectservices.*.environmentfor the main app service and any confirmed worker or scheduler services. Assume those rock-defined env vars will not survive automatically oncepaas-charmrenders the workload Pebble layer. - Add only user-confirmed relations and config options. Do not add relations that are already embedded in the charmcraft extension (ingress, grafana-dashboard, metrics-endpoint, logging) — those are already declared with fixed optionality and must not be re-declared or questioned.
- Do not decide relation optionality yourself. For each declared relation,
set
optionalfrom the fit handoff or the user's explicit answer. If that choice is missing, stop and ask instead of defaulting to non-optional. - For ExpressJS, verify whether the generated charm injects
app-portwhile currentpaas-charmaliases still expectport. If so, add a non-conflictingportconfig option instead of rewriting charm logic. - Treat generated
paas-charmenv output as the baseline contract, not the whole workload contract. Any required env var defined in the rock service must also be re-established under charm control. Prefer non-conflicting charm config defaults for deploy-time values and a tiny workload-side defaulting bridge in the image, startup wrapper, or app entrypoint for static app-facing defaults before editing charm Python. - For built-in dynamic OAuth or OIDC config, prefer deploy-time config such as
<endpoint>-redirect-pathinstead of charm subclassing or relation-hook overrides. - If live Juju or Pebble logs show service exit loops, CLI usage output, or
permission deniedon workload paths, treat that as a rock/runtime issue first and inspect the rock service command, runtime user, and writable paths before considering charm Python changes. - If
src/charm.pychanges still seem necessary after exhausting viable workload-side adaptations, stop and ask for approval before modifying it. - If the app needs static-asset preparation or a frontend build, push that work back into the rock build instead of customizing charm runtime behavior.
- Build with
charmcraft pack. Never use--destructive-mode.
Extension-Embedded Relations
The charmcraft framework extensions already declare these relations in the
generated charmcraft.yaml with fixed optionality:
- ingress (interface:
ingress) — required - grafana-dashboard (interface:
grafana_dashboard) — optional - metrics-endpoint (interface:
prometheus_scrape) — optional - logging (interface:
loki_push_api) — optional
Do not re-declare these in charmcraft.yaml. Do not ask the user whether they
want these relations or whether they should be optional — the extension owns
that decision. If the user mentions one of these, confirm it is already provided
by the extension and move on.
Non-Negotiables
- Use
charm/, not the app root. - Always generate the charm via
charmcraft init --profile <framework> --name <charm-name>insidecharm/. Never copy from discovered templates, previously generated files, or example charms. - Keep source changes minimal and justified.
- Keep
charmcraft.yamledits minimal. - Add only user-confirmed relations that are not already embedded in the extension.
- Do not re-declare or change the optionality of extension-embedded relations (ingress, grafana-dashboard, metrics-endpoint, logging).
- Set each declared relation's
optionalfield from explicit user input or fit handoff, not your own assessment. - Use secret-typed config for secrets where appropriate.
- Treat generated
paas-charmbehavior as the runtime truth. - Treat generated
paas-charmenv output as the baseline contract. - When
rockcraft.yamldefines service env, mirror the required keys into the charm-managed workload too. - Keep generated
src/charm.pystock unless there is no viable workload-side adaptation and the user approved the exception. - Never use
charmcraft pack --destructive-mode.
Preferred Adaptations
- add non-conflicting charm config options for deployment-facing toggles
- mirror required rock-defined service env through non-conflicting charm config defaults when the values should stay operator-visible
- add a minimal workload-side env bridge or defaulting wrapper in the image,
startup wrapper, or app entrypoint when
paas-charmand the app use different names or when a static rock-defined env default must survive - add a migration script only when the app needs one and the workflow supports it
Avoid
- do not add every possible relation
- do not mark a declared relation non-optional by default when the user has not explicitly chosen that
- do not collide with reserved framework config prefixes
- do not hard-code deployment-specific values into the app if a charm config option is the cleaner fit
- do not assume every framework uses the same env prefixes or metrics model
- do not assume
rockcraft.yamlserviceenvironment:entries survive oncepaas-charmowns the workload Pebble layer - do not treat Pebble restart failures caused by wrong service commands or
unwritable workload paths as a default reason to edit
src/charm.py - do not override
_init_*or_on_*relation methods just to rename env vars or tweak default values - do not subclass the charm for built-in OAuth or OIDC relation behavior when deploy-time config already covers the requirement
- do not modify
src/charm.pywithout explicit approval after you explain why a workload-side adaptation is not viable - do not override private or semi-private
paas-charminternals unless the user explicitly chooses that tradeoff after you surface the risk - do not use
charmcraft pack --destructive-mode
Deployment Reminder
When deploying a local charm artifact later:
- use the full
.charmfilename - keep the file in a Juju-accessible path, typically under the user home directory for snap-confined Juju
Output Contract
Produce:
- a minimal
charmcraft.yaml - a built
.charmartifact or a Charmhub publication path - a clear config, secret, and relation contract for the deploy step, including relation optionality