AGENTS.md — Canonical Instructions for This Repository
Repository Context#
This is pulumi/registry — the Hugo-based static site for Pulumi Registry, the public index of Pulumi packages. The deployed site lives at pulumi.com/registry.
For comprehensive details on the build system, CI/CD workflows, and deployment infrastructure, see BUILD-AND-DEPLOY.md. IMPORTANT: BUILD-AND-DEPLOY.md is large; to conserve tokens, read only as needed and limit your focus to the sections relevant to your task.
Build / Test / Lint Workflow#
Install tools via mise before anything else:
mise trust && mise installCommon commands (all defined in the root Makefile):
| Task | Command |
|---|---|
| Install dependencies | make ensure |
| Build theme CSS/JS | make build-assets |
| Local dev server | make serve |
| Dev server + asset hot reload | make serve-all |
| Generate API docs for one provider | make api-docs/<package> |
| Full local build (all providers) | make build |
| Run Go unit tests | make test |
| Run all linters | make lint |
| Run Go linter only | make lint-go |
| Run Markdown linter only | make lint-markdown |
| Check the generated dark logo variants | make lint-dark-logos |
| Run provider API docs tests | make test_provider_api_docs |
| Run browser tests | make run-browser-tests |
make lint must pass before merging. make build requires 32 GB+ RAM for a complete build; for single-provider work, use make api-docs/<package> instead.
Architecture#
Hugo Structure#
Hugo reads from themes/default/:
| Path | Contents |
|---|---|
themes/default/content/ |
Markdown pages (generated at build time — do not edit by hand) |
themes/default/data/registry/packages/ |
One YAML file per provider — the source of truth for the registry |
themes/default/layouts/ |
Hugo templates |
themes/default/static/ |
Static assets, generated nav JSON, generated schema JSON |
themes/default/theme/ |
JS/CSS source (compiled by yarn --cwd ./themes/default/theme run build) |
config/ |
Hugo configuration (includes custom output formats for CLI docs) |
public/ |
Build output (git-ignored) |
Provider YAML Files#
Every package in the registry is described by a YAML file at themes/default/data/registry/packages/<name>.yaml. These files drive:
- API doc generation (
resourcedocsgen) - Registry publication (
scripts/ci/publish_to_registry.py, withscripts/ci/push-registry.pyas a fallback) - The nightly community package update workflow
Go Tools#
| Tool | Location | Purpose |
|---|---|---|
resourcedocsgen |
tools/resourcedocsgen/ |
Generates provider API reference docs and LLM-friendly JSON bundles (llm-docs.json) from Pulumi provider schemas |
resourcedocsgen is compiled to bin/ by the Makefile.
Content Generation: Local vs. CI#
make api-docs/<pkg> writes output to content/registry/packages/ (repo root).
The CI build script (scripts/ci/build.sh) writes to themes/default/content/registry/packages/.
This difference is intentional — local API doc generation stays out of the Hugo theme tree. Within themes/default/content/registry/packages/<pkg>/, the api-docs/ subdirectory is regenerated on every build and is git-ignored, so never commit it. The landing pages are different: _index.md — plus installation-configuration.md for the packages that have one, which is optional, since only _index.md is required — is committed and maintained by the generate-package-metadata.yml publish workflow, and bundled when onboarding a package so it renders before the next nightly run. Do not hand-edit any of these files; regenerate them with resourcedocsgen.
Additionally, resourcedocsgen writes LLM docs to llm-docs-out/registry/packages/ (repo root, git-ignored). These are terminal-friendly markdown bundles (llm-docs.json) uploaded to S3 separately from the Hugo site. The LLM docs format is specified in docs/llm-markdown-spec.md.
Deployment Model#
Each CI build syncs to a uniquely named S3 bucket. A Pulumi IaC program in infrastructure/ reads origin-bucket-metadata.json to update the CloudFront origin. This means:
- Every PR commit gets its own preview URL (in the
testingAWS account) - Production deploys atomically swap the CloudFront origin (in the
productionAWS account) - Preview buckets are deleted when a PR closes
Adding or Updating a Provider#
To add or update a community provider package:
- Edit (or create) its YAML file in
themes/default/data/registry/packages/. - The nightly
generate-package-metadata.ymlworkflow handles version bumps automatically for community packages tracked incommunity-packages/package-list.json. - First-party Pulumi provider repos trigger
publish-provider-update.ymlviarepository_dispatch.
On every push to master, publish_to_registry.py publishes packages to the live Pulumi registry service — the Pulumi Cloud store behind pulumi package add, which is separate from the rendered site. push-registry.py runs only if that step fails.
Dark Mode#
The registry has a light/dark/system theme toggle, pinned to the bottom of the left sidebar. Dark is light-first: light is the baseline and dark is a pure override. The whole system lives in themes/default/theme/src/scss/docs/_docs-theme.scss (read its header comment first) and is driven by semantic --docs-* tokens defined on body.section-registry and re-pointed under html[data-theme="dark"].
The preference is stored under the same localStorage key as pulumi.com/docs (pulumi-docs-theme), so it is one setting across pulumi.com. Picking dark on /docs flips the registry too.
| File | Role |
|---|---|
theme/src/scss/docs/_docs-theme.scss |
Token layer + every dark override + the toggle's own styles |
theme/src/scss/docs/_code-light.scss |
Min Light code theme, gated on html:not([data-theme="dark"]) |
theme/src/ts/docs-theme.ts |
Toggle behavior, system tracking |
layouts/partials/head.html |
Pre-paint inline script (writes data-theme on <html>; prevents FOUC) |
layouts/partials/docs/theme-toggle.html |
The three-button control |
scripts/generate-dark-logos.py |
Generates -on-dark.svg variants of local package logos |
scripts/classify-external-logos.py |
Decides which external logo_url marks need a light chip |
You must check both modes whenever you add or restyle a visible element — new partials, cards, callouts, buttons, icons, or any markup that introduces its own colors, backgrounds, borders, or images. Pure content changes (prose, YAML metadata, code samples) don't need a dark-mode pass.
When something needs dark-mode work, prefer the existing levers over hand-written one-off colors:
- Use the semantic tokens. Paint with
var(--docs-fg),--docs-fg-muted,--docs-bg,--docs-bg-alt,--docs-surface,--docs-border,--docs-card,--docs-card-border,--docs-link,--docs-ringrather than raw--color-*scales or hex literals — they flip automatically. Two caveats when you're retrofitting a token onto an existing light rule rather than writing a new one:- The fallback form doesn't protect light mode. The tokens are declared on
body.section-registry, so anywhere inside the registry they always resolve —var(--docs-TOKEN, ORIGINAL)never falls back, and the element silently picks up the token's light value instead of the hex it had.--docs-fg-mutedin light is gray-700 (#6a6675), not whatever grey the rule used before. The fallback only earns its keep on markup that also renders outside the section (the 404 page, standalone components). Compare the two values before you swap. - A layered file often can't opt in at all. Almost everything under
theme/src/scss/is inside@layer components, and_docs-theme.scssis unlayered, so its blanket dark rules —p { color: var(--docs-fg) }, the per-surface overrides — win regardless of specificity. Pointing a layered rule at a token therefore changes light and does nothing in dark. Declare the dark value in_docs-theme.scssand leave the layered file's light value alone.
- The fallback form doesn't protect light mode. The tokens are declared on
- Use Tailwind
dark:variants. Thedark:variant is wired todata-theme(@custom-variant darkintheme/src/scss/main.scss), sodark:bg-gray-900,dark:text-white, etc. work directly in templates.bg-docs-bg/text-docs-fg/border-docs-borderutilities are generated too. - Lean on the automatic flips. Brand violet (
--color-violet-primaryand everytext-violet-primary/bg-violet-primary/border-violet-primaryutility) and the default border color are already remapped in the dark block, so markup authored with those gets dark mode for free. Surfaces styled via@apply(the.btn-*variants, for example) carry no literal utility class and need their own override in_docs-theme.scss. - Package logos are handled in two tiers by
layouts/partials/registry/package/icon.html, and neither tier is hand-maintained:- Local marks. The partial looks for
themes/default/assets/fingerprinted/logos/pkg/<name>-on-dark.svgand, when it finds one, emits both images with thedocs-logo-light/docs-logo-darkclasses the theme swaps. Runpython3 scripts/generate-dark-logos.pyafter adding or replacing a local mark; it lightens only the paints that are too dark for the dark page, skips full-color marks entirely, and skips any package whose YAML declares alogo_url(icon.htmlshort-circuits on that before it ever looks for a local variant).make lint-dark-logosruns--check, which reports staleness without writing; it's a PR-CI job, so a stale variant fails the build. - External
logo_urlmarks. These can't be recolored, so the ones that would disappear get a near-white chip instead. Which ones those are is measured bypython3 scripts/classify-external-logos.py, which downloads each logo, samples it, and writesthemes/default/data/registry/external_logo_treatment.yaml. Re-run it when you add a package with alogo_urlor when a vendor changes their logo (--checkin CI-style mode). It needs the network — and macOSsipsfor non-PNG rasters — so it is deliberately not in CI;--checkexits 2 rather than reporting staleness if any logo couldn't be measured. Don't apply the chip by hand or across the board — it hides light marks and boxes in full-color ones.
- Local marks. The partial looks for
- Masked icons in
docs/_icons.scsstint withbackground: var(--docs-fg-muted);background: url()colored marks do not and need a per-asset variant.
Note that _docs-theme.scss is imported unlayered, after everything else, so its rules beat anything in @layer components or @layer utilities regardless of specificity. That is deliberate (much of the chrome is painted with Tailwind utilities), but it means a dark rule there can't be walked back by a more specific layered rule.
The one deliberate exception is the dark default border color, which sits in @layer base inside that file. Unlayered it would outrank @layer utilities too and flatten every colored border-{color}-{step} a template authored down to a gray hairline; in @layer base it still replaces Tailwind's gray-200 default while any layered rule with its own border color keeps it. If you write another blanket * rule, put it in @layer base for the same reason.
Conventions#
- Package manager: Yarn only. Do not use npm or pnpm.
- Go modules:
tools/resourcedocsgen/is a separate Go module. Rungo test ./...andgolangci-lint runfrom within that directory (or use the Makefile targets). - Generated content: Files under
themes/default/content/registry/packages/are generated — regenerate withresourcedocsgen, never hand-edit. Theapi-docs/subdirectories are git-ignored build output (never committed); the_index.mdpage — andinstallation-configuration.md, when the package has one — is committed metadata maintained by the publish workflow. Only_index.mdis required; a package with just an overview is complete. - Markdown line wrapping: Put each paragraph and each list item on a single line. Do not hard-wrap prose to a column — let the editor soft-wrap it. A hard-wrapped paragraph re-flows when you change a word in the middle, so a one-word edit shows up as a multi-line diff and reviewers can't see what actually changed. This applies to every hand-written Markdown file in the repo:
README.md,AGENTS.md,BUILD-AND-DEPLOY.md, everything underdocs/, the PR and issue templates, and the skill files under.claude/commands/. Code inside fences, tables, and front matter keep whatever line structure they need;CODE-OF-CONDUCT.mdis imported upstream and is left as-is. - Branch naming: Use
<GitHub Username>/<descriptive-name>for branches in this repository.