| <!-- 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 |