AGENTS.md@public/files/airflow/registry · git:20260915.997c05b · 2026-09-15 · sha256 d219018bc06914ea
AGENTS.md@public/files/airflow/registry git:20260915.997c05bA
Immutable. This exact content is served forever at /api/v1/blob/d219018bc06914ea.
<!-- SPDX-License-Identifier: Apache-2.0
https://www.apache.org/licenses/LICENSE-2.0 -->
# Agent Guidelines for Airflow Registry
This document contains rules, patterns, and guidelines for working on the Airflow Registry project.
## Core Principles
### 0. Minimal JavaScript
- Keep JavaScript to minimum necessary
- Theme toggle + progressive enhancement (filters, search)
- No frameworks (no React, Vue, etc.)
- Exception: swagger-ui is vendored from `node_modules/swagger-ui-dist` for the API Explorer
## 1. Quality Standards
- **Visual consistency**: Changes should match existing design system and styling
- **Semantic HTML**: Use proper elements and structure
- **Maintainable CSS**: Descriptive class names, leverage cascade
- **Light/Dark mode**: Full support with smooth transitions
- **Responsive**: Mobile-first, works on all screen sizes
### 2. Semantic CSS & HTML
Follow the principles from https://css-tricks.com/semantic-class-names/
#### 2.1 Class Names Describe CONTENT, Not Appearance
Class names should answer "what is this?" not "how should it look?"
**Good examples:**
- `.provider-card` - it's a card displaying provider information
- `.connection-types` - a section showing connection types
- `.dependencies` - shows dependency information
- `.stats` - displays statistical data
**Bad examples:**
- `.blue-box` - describes appearance (what if design changes to green?)
- `.flex-row` - describes layout implementation
- `.mt-4` - describes spacing value
- `.text-lg` - describes typography
**Why this matters:**
- Design changes shouldn't require HTML updates
- Code is self-documenting (you understand structure without seeing CSS)
- Multiple elements can share appearance without sharing meaning
#### 2.2 Use CSS Cascade to Avoid Redundant Prefixes
When an element only makes sense within its parent, use descendant selectors instead of prefixing class names.
**Good - leveraging cascade:**
```html
<section class="technical">
<div class="python-version">...</div>
<div class="connections">...</div>
</section>
```
```css
.technical { /* section styles */ }
.technical h3 { /* heading styles */ }
.technical .python-version { /* specific section */ }
.technical code { /* all code in technical section */ }
```
**Bad - redundant prefixes:**
```html
<section class="technical">
<div class="technical-python">...</div>
<div class="technical-connections">...</div>
</section>
```
```css
.technical { /* section styles */ }
.technical-heading { /* redundant prefix */ }
.technical-python { /* redundant prefix */ }
.technical-code { /* redundant prefix */ }
```
**Why this matters:**
- Less typing, less repetition
- Clearer hierarchy in CSS
- If `.python-version` only appears in `.technical`, the prefix adds no information
#### 2.3 Leverage Semantic HTML5 Elements
Use the right HTML element for the job. This improves accessibility, SEO, and reduces need for classes.
**Semantic elements and their uses:**
- `<header>` - Page or section header
- `<footer>` - Page or section footer
- `<nav>` - Navigation menus
- `<main>` - Main content of the page
- `<section>` - Thematic grouping of content
- `<article>` - Self-contained content
- `<aside>` - Side content/sidebars
- `<dl>`, `<dt>`, `<dd>` - Definition lists (key-value pairs)
- `<details>`, `<summary>` - Collapsible content
**Example - Statistics:**
```html
<!-- Good: Semantic -->
<dl class="stats">
<div>
<dt>1,000+</dt>
<dd>Providers</dd>
</div>
</dl>
```
```css
.stats dt { font-size: var(--text-3xl); }
.stats dd { font-size: var(--text-sm); }
```
**Example - Provider detail header:**
```html
<!-- Good: Semantic structure -->
<header class="card">
<div class="top">
<div class="logo">...</div>
<div class="details">
<h1>Amazon</h1>
<p class="description">...</p>
</div>
</div>
<div class="stats">...</div>
</header>
```
```css
.provider-detail-page header { /* styles */ }
.provider-detail-page header h1 { /* title */ }
.provider-detail-page header .stats { /* grid of stats */ }
```
#### 2.4 When to Add Classes vs Use Element Selectors
**Use element selectors when:**
- There's only one of that element in the scope
- The element's meaning is clear from context
- Example: `.provider-detail-page header h1` - there's only one main heading
**Add a class when:**
- Multiple instances need different styling
- The role isn't obvious from the element alone
- You need to target it from JavaScript
- Example: `.python-version`, `.connections`, `.dependencies` - all are `<div>` but serve different purposes
**Prefer simpler selectors:**
- `section code` over `section .code-snippet`
- `details summary` over `.details-trigger`
- `.connections a` over `.connection-badge`
#### 2.5 No Utility Classes
Utility classes describe HOW something looks, not WHAT it is.
**Remove these patterns:**
- Spacing: `.mb-2`, `.py-12`, `.gap-4`, `.px-6`, `.space-y-4`
- Layout: `.flex`, `.grid`, `.grid-cols-2`, `.items-center`
- Display: `.block`, `.inline-block`, `.hidden`
- Text: `.text-center`, `.text-lg`, `.font-bold`, `.uppercase`
- Colors: `.text-blue-500`, `.bg-gray-100`
**Instead, style contextually:**
```css
/* Good */
.technical h3 {
font-size: var(--text-sm);
font-weight: var(--font-semibold);
margin-bottom: var(--space-3);
display: flex;
align-items: center;
}
/* Bad */
<h3 class="text-sm font-semibold mb-3 flex items-center">
```
#### 2.6 Contextual Component Styling
Components should be styled based on WHERE they appear, not through generic modifier classes.
**No generic button classes:**
```css
/* Bad */
.btn { }
.btn-primary { }
.btn-secondary { }
```
**Style contextually instead:**
```css
/* Good */
.hero .popular-providers a {
padding: var(--space-2) var(--space-4);
background: var(--bg-tertiary);
border: 1px solid var(--border-primary);
border-radius: var(--radius-lg);
}
.provider-detail-page .actions .btn-primary {
background: var(--accent-primary);
color: var(--color-navy-900);
}
```
**Why this matters:**
- Buttons in hero look different from buttons in provider detail
- Context determines appearance
- Reduces need for modifier classes
## CSS Architecture
### File Structure
```
src/css/
├── tokens.css # Design tokens (colors, spacing, fonts)
├── main.css # Main styles with imports
└── base/
├── reset.css # CSS reset
└── typography.css # Base typography
```
### Semantic Section Pattern
Each major section follows this pattern:
```css
/* Section container */
.section-name {
padding: var(--space-20) 0;
background: var(--bg-primary);
}
/* Nested elements using cascade */
.section-name header {
/* Header styles */
}
.section-name h2 {
/* Title styles */
}
.section-name .some-element {
/* Element styles */
}
```
### Design Tokens
Use CSS custom properties defined in `tokens.css`:
- Colors: `--color-navy-900`, `--color-cyan-400`, `--color-gray-700`, etc.
- Spacing: `--space-4` (1rem), `--space-8` (2rem), etc.
- Typography: `--text-base`, `--text-lg`, `--font-semibold`, etc.
- Theme variables: `--bg-primary`, `--text-primary`, `--border-primary`
**IMPORTANT**: Always use CSS custom properties instead of hardcoded color values:
- ✅ Good: `border: 1px solid var(--color-gray-700);`
- ❌ Bad: `border: 1px solid #334155;`
#### When to Use Semantic vs Direct Color Variables
When defining new variables in `tokens.css`, follow this hierarchy:
1. **Prefer semantic variables** (with -light/-dark suffixes) over direct color variables:
- ✅ Good: `--color-input-bg: light-dark(var(--bg-tertiary-light), var(--bg-tertiary-dark));`
- ❌ Bad: `--color-input-bg: light-dark(var(--color-gray-100), var(--color-navy-700));`
2. **Only use direct color variables** when:
- Defining the base semantic variables themselves (e.g., `--bg-tertiary-light: var(--color-gray-100);`)
- No appropriate semantic variable exists for your use case
- The color combination is intentionally different from existing semantic patterns
3. **In component CSS** (main.css):
- Use computed semantic variables: `background: var(--bg-primary);`
- Or use inline `light-dark()` with semantic variables for contextual colors
This creates a clear hierarchy: base colors → semantic variables → computed variables → usage
### Light/Dark Mode
The theme system uses the CSS `color-scheme` property and `light-dark()` function:
- Default theme is dark mode
- Theme switching sets `document.documentElement.style.colorScheme = 'light' | 'dark'`
- Theme computed variables defined in `tokens.css` with progressive enhancement:
```css
/* Progressive enhancement: fallback then light-dark() */
--bg-primary: var(--bg-primary-light);
--bg-primary: light-dark(var(--bg-primary-light), var(--bg-primary-dark));
```
- Most elements use computed theme variables:
```css
.element {
background: var(--bg-primary);
color: var(--text-primary);
}
```
- Contextual colors use inline `light-dark()`:
```css
.kbd {
border-color: light-dark(var(--border-primary-light), var(--color-navy-600));
}
```
**Progressive Enhancement**: Each theme variable is declared twice in tokens.css:
1. First with light value as fallback for browsers without `light-dark()` support
2. Then with `light-dark()` which overrides in supporting browsers
## Development Workflow
### Feature Development Checklist
When adding or modifying features:
1. **Plan semantic HTML structure** using appropriate elements
2. **Write semantic CSS** using cascade pattern
3. **Add light/dark mode styles** for all elements
4. **Test responsiveness** across different screen sizes
5. **Verify consistency** with existing design system
### Common Issues
- **Colors not switching in dark mode**: Ensure element uses computed theme variables like `var(--bg-primary)` or inline `light-dark()`
- **Missing dark mode colors**: Check that theme variable has both `-light` and `-dark` variants defined in tokens.css
- **Wrong border colors**: Check color tokens are indigo not gray
## CSS Deletion Guidelines
When removing non-semantic CSS:
1. **Search for usage first**: Use grep to find where classes are used
2. **Confirm removal**: If only in CSS and not in refactored templates, delete it
3. **Test visually**: Take screenshots before and after
4. **Common deletions**:
- Grid/flex utilities: `.grid`, `.flex`, `.items-center`, etc.
- Spacing utilities: `.mb-*`, `.py-*`, `.gap-*`, etc.
- Button classes: `.btn`, `.btn-primary`, etc.
- Text utilities: `.text-center`, `.text-lg`, etc.
## Quality Standards
- **Visual consistency**: Changes should match existing design system and styling
- **Semantic HTML**: Use proper elements and structure
- **Maintainable CSS**: Descriptive class names, leverage cascade
- **Light/Dark mode**: Full support with smooth transitions (except during screenshots)
- **Responsive**: Mobile-first, works on all screen sizes
## Reference Files
Key files in the project:
- `src/css/main.css` - Main styles
- `src/css/tokens.css` - Design tokens
- `src/` - All page templates (index, provider-detail, providers, explore, stats, api-explorer)
- `../dev/registry/registry_contract_models.py` - Pydantic contracts for all JSON payloads
- `../dev/registry/export_registry_schemas.py` - Generates OpenAPI spec from contracts
## Development Server
Local development server runs at http://localhost:8080
## Deployment Architecture
The registry is built in the `apache/airflow` repo and served at `airflow.apache.org/registry/`.
### How it works
1. **Build**: `registry-build.yml` extracts metadata, builds the 11ty site, and syncs to S3.
Supports two modes:
- **Full build** (no `provider` input): extracts all ~99 providers (~12 min)
- **Incremental build** (`provider=amazon`): extracts one provider (~30s), merges
with existing data from S3 via `merge_registry_data.py`, then builds the full site.
The S3 sync step excludes the entire `api/providers/` subtree for non-target
providers to avoid overwriting real data with Eleventy's incomplete/empty
stubs (Eleventy 3.x `permalink: false` does not work with pagination).
2. **S3 buckets**: `{live|staging}-docs-airflow-apache-org/registry/` (same bucket as docs, different prefix)
3. **Serving**: Apache HTTPD at `airflow.apache.org` rewrites `/registry/*` to CloudFront, which serves from S3
4. **Auto-trigger**: When `publish-docs-to-s3.yml` publishes provider docs, its
`update-registry` job calls `registry-build.yml` with the provider ID for an
incremental registry update
### Path prefix
- Production: `REGISTRY_PATH_PREFIX=/registry/` (default)
- Local dev: `REGISTRY_PATH_PREFIX=/` (set automatically by `pnpm dev`)
- All internal links use 11ty's `| url` filter, which prepends the prefix
### Changes needed in `apache/airflow-site` repo (separate PR)
1. **`.htaccess` rewrite rule** - Add a rule to proxy `/registry/*` through CloudFront:
```apache
RewriteRule ^registry/(.*)$ https://<cloudfront-distribution>.cloudfront.net/registry/$1 [P,L]
```
2. **Navigation link** - Add a "Registry" entry to the site header/nav in the Hugo templates,
pointing to `/registry/`.
These changes mirror the existing `/docs/*` rewrite pattern.
## Data Extraction (`dev/registry/`)
The registry's JSON data is produced by four extraction scripts in `dev/registry/`,
which is a Python package (workspace member) with shared code in `registry_tools/`.
**Module type definitions** live in `dev/registry/registry_tools/types.py` — this is
the single source of truth for all module types (operator, hook, sensor, trigger, etc.).
The three Python extraction scripts and the frontend data file (`types.json`) all derive
from this module. To add a new type, add it to `MODULE_TYPES` in `types.py` and run
`generate_types_json.py`.
When modifying these scripts, understand the design decisions below.
### Why four separate scripts?
| Script | Runs on | Needs providers installed? | What it does |
|---|---|---|---|
| `extract_metadata.py` | Host | No | Provider metadata, class names (AST), PyPI stats, logos |
| `extract_versions.py` | Host | No | Per-version metadata from git tags |
| `extract_parameters.py` | Breeze | Yes | `__init__` parameter inspection via MRO |
| `extract_connections.py` | Breeze | Yes | Connection form metadata via ProvidersManager |
`extract_parameters.py` and `extract_connections.py` need runtime access to provider
classes (to inspect `__init__` signatures and call `get_connection_form_widgets()`).
They run inside Breeze where all providers are installed. `extract_metadata.py` and
`extract_versions.py` only need filesystem access and run on the host. This split means
the CI workflow can run the fast scripts (metadata, ~30s per provider) without spinning
up Breeze, while parameter/connection extraction is a separate step.
### Relationship to `run_provider_yaml_files_check.py`
`scripts/in_container/run_provider_yaml_files_check.py` (run by the
`check-provider-yaml-valid` pre-commit hook inside Breeze) validates that everything
listed in `provider.yaml` is correct: modules exist, classes are importable, and every
Python file in `operators/`/`hooks/`/`sensors/`/`triggers/` directories is accounted
for. This guarantees `provider.yaml` is the correct and complete manifest.
`extract_metadata.py` builds on that guarantee but solves a different problem: it needs
individual class names within each module. `provider.yaml` lists entries at two
granularities:
- **Module-level** (operators, hooks, sensors, triggers, transfers, bundles): e.g.,
`airflow.providers.amazon.operators.s3` — a single module can contain many classes
(`S3CopyObjectOperator`, `S3DeleteObjectsOperator`, `S3ListOperator`, etc.)
- **Class-level** (notifications, secrets-backends, logging, executors, task-decorators):
e.g., `airflow.providers.amazon.notifications.chime.ChimeNotifier` — full class path
already provided
For module-level entries, AST parsing discovers the individual classes and their
inheritance chains. For class-level entries, `extract_metadata.py` uses the paths from
`provider.yaml` directly — no discovery needed. The validation script ensures both
categories are correct before `extract_metadata.py` ever runs.
### Why AST parsing instead of runtime import?
`extract_metadata.py` runs on the CI host without installing 100+ provider packages.
It reads `.py` files and extracts class names, base classes, and docstrings from the
AST. This means it works with just `pyyaml` as a dependency.
The trade-off: AST parsing can't resolve dynamic class definitions or runtime-computed
attributes. For the 99 providers currently in the repo, AST parsing captures everything
because provider classes use standard `class Foo(Base):` definitions.
An approach using only Sphinx inventory files (without AST) was considered but rejected:
inventory files only exist for published providers, and they don't contain inheritance
information. The registry needs to know whether a class is an operator, hook, sensor,
etc., which requires resolving the inheritance chain back to base classes like
`BaseOperator` or `BaseHook`. AST parsing provides this.
### Module filtering via base class inheritance
Classes are discovered by checking if they inherit (transitively) from type-specific
base classes defined in `get_module_type_base_classes()`:
- **operators**: `BaseOperator`, `BaseSensorOperator`, `DecoratedOperator`, ...
- **hooks**: `BaseHook`, `DbApiHook`, `DiscoverableHook`
- **sensors**: `BaseSensorOperator`, `PokeSensorOperator`
- **triggers**: `BaseTrigger`, `TriggerEvent`
- **transfers**: `BaseOperator` (transfers are operators)
- **bundles**: `BaseDagBundle`
Inheritance resolution is transitive and cross-file. `build_global_inheritance_map()`
scans all provider `src/` directories to build a global `class_name → {base_names}`
map. This handles chains like `S3ListOperator → AwsBaseOperator → BaseOperator` where
the intermediate class lives in a different file.
After inheritance filtering, a post-filter skips private (`_`-prefixed), `Base*`,
`Abstract*`, and `*Mixin` classes to avoid indexing internal helper classes.
There is no suffix-based matching (e.g., checking if a class name ends with
"Operator"). The filtering is purely inheritance-based.
### Documentation URLs via Sphinx Inventory
Module `docs_url` values come from Sphinx `objects.inv` inventory files, not manually
constructed paths. The inventory is the canonical mapping of every documented class to
its URL, published on S3 alongside the docs.
- `read_inventory()` parses an `objects.inv` file, returning `{qualified_name: url_path}`
for `py:class` entries
- `fetch_provider_inventory()` downloads the inventory from S3 with local file caching
(12-hour TTL in `dev/registry/.inventory_cache/`)
- `extract_modules_from_yaml()` accepts an `inventory` dict and uses it for URL lookup,
falling back to manual construction when a class isn't in the inventory
The fallback exists because unpublished providers (new, not yet released to PyPI) won't
have inventory files on S3.
If you're adding a new module type or changing how `docs_url` is constructed, prefer
extending the inventory lookup rather than hardcoding URL patterns.
### Why Eleventy for the static site?
Static site generators produce zero-JS pages by default. The registry works without
JavaScript — filtering and search are layered on top progressively. Eleventy has no
opinion on frontend frameworks, which keeps the dependency surface small (~30 packages
in the lockfile).
### Path prefix handling
The site deploys at `/registry/` on airflow.apache.org but runs at `/` during local
dev. Eleventy's `pathPrefix` config handles this via the `REGISTRY_PATH_PREFIX` env var.
Templates use the `| url` filter, and client-side JS reads `window.__REGISTRY_BASE__`
(injected in `base.njk`).
## Troubleshooting
### Can't find where a class is used
```bash
# Search in templates
grep -r "class-name" src/
# Search in CSS
grep "\.class-name" src/css/
```
### Color issues
1. Use browser DevTools to inspect computed styles
2. Check both light and dark mode
3. Verify color tokens in `tokens.css`
4. Ensure computed theme variables are used correctly