This document provides comprehensive documentation of the entire build, test, deployment, and infrastructure system for the Pulumi documentation site.
- Overview
- Architecture Overview
- Local Development
- Build System
- GitHub Actions Workflows
- Deployment Infrastructure
- Testing Strategy
- Environment Management
- Troubleshooting
- Infrastructure Change Review
- Maintenance Tasks
- Reference
This guide serves as the definitive reference for understanding and working with the Pulumi documentation site's build and deployment infrastructure. Whether you're a developer making changes, an operations engineer troubleshooting deployments, or a contributor adding content, this document will help you understand the complete lifecycle from source to production.
Common commands for daily development:
# Initial setup
make ensure # Install all dependencies
# Local development
make serve # Start local server at http://localhost:1313
make serve-all # Serve with asset rebuilding (webpack watch)
# Building
make build # Full production build
make build-assets # Build CSS/JS assets only
# Quality checks
make lint # Run linting checks
make format # Format code with Prettier
make test # Run example program tests
# Cleanup
make clean # Remove build artifacts and dependencies- Hugo: Static site generator that transforms markdown content into HTML
- Atomic Deployments: Each deployment creates a new S3 bucket, enabling instant rollbacks
- Bundle IDs: Unique identifiers (git SHA or PR number) for cache busting assets
- Ephemeral Buckets: Temporary S3 buckets created for PR previews
- CloudFront: CDN that serves the production site globally
- Pulumi ESC: Environment, Secrets, and Config service for credential management
- OIDC: OpenID Connect authentication for AWS (no static credentials)
Required Tools:
- Node.js 24.x
- Hugo 0.157.0
- Yarn 1.22.x (not strictly enforced in CI)
- Go 1.26.x (for documentation generation)
- Python 3.9 (for testing workflows) and 3.13 (for SDK documentation generation)
- Pulumi CLI (for infrastructure deployments)
Optional Tools:
- AWS CLI (for debugging deployments)
- GitHub CLI (
gh) (for PR operations) - Docker (for dev container)
┌─────────────────────────────────────────────────────────────────┐
│ Content Authors │
│ (Markdown, Code Examples) │
└──────────────────────┬──────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ GitHub Repository │
│ (docs, blog, examples, static assets) │
└──────────────────────┬──────────────────────────────────────────┘
│
┌───────────────┴───────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ PR Build │ │Master Build │
│ (Testing) │ │(Production) │
└──────┬───────┘ └──────┬───────┘
│ │
▼ ▼
┌──────────────────────────────────────────────────────────────┐
│ Build Pipeline │
│ 1. Asset Compilation (Webpack, Tailwind CSS) │
│ 2. Documentation Generation (TypeDoc, Sphinx, CLI) │
│ 3. Hugo Build (Markdown → HTML) │
│ 4. CSS Minification (cssnano) │
│ 5. Search Index Generation (Algolia) │
└──────────────────┬───────────────────────────────────────────┘
│
┌───────────┴────────────┐
│ │
▼ ▼
┌──────────────┐ ┌──────────────┐
│ S3 Preview │ │ S3 Production│
│ Bucket │ │ Bucket │
│(PR-specific) │ │ (versioned) │
└──────────────┘ └──────┬───────┘
│
▼
┌───────────────┐
│ CloudFront │
│ Distribution │
│ +Lambda@Edge │
└───────┬───────┘
│
▼
┌───────────────┐
│ www.pulumi │
│ .com │
└───────────────┘
Build System:
- Hugo: Transforms content/ markdown into static HTML
- Webpack: Bundles TypeScript and CSS from theme/
- TypeDoc: Generates Node.js SDK documentation
- Sphinx: Generates Python SDK documentation
- Pulumi CLI: Generates CLI reference documentation
Deployment Infrastructure:
- S3: Origin buckets for static content
- CloudFront: Global CDN with caching
- Lambda@Edge: Request routing and redirects
- Route53: DNS management
- Pulumi: Infrastructure as Code for deployment
CI/CD:
- GitHub Actions: 24 workflows for build, test, deploy
- Pulumi ESC: Secrets and environment management
- OIDC: Secure AWS authentication without static keys
Source Files → Asset Compilation → Hugo Build → Optimization → S3 Sync → CloudFront → Users
│ │ │ │ │
│ │ │ │ └─ Cache invalidation
│ │ │ └─ CSS minification, search indexing
│ │ └─ HTML generation, shortcode processing
│ └─ JS bundling, CSS compilation
└─ Markdown, images, code examples
The docs site integrates with several other Pulumi repositories:
- pulumi/registry: Package registry UI (external repository, served via CloudFront origin routing at /registry path)
- pulumi/answers: AI answers feature (embedded at /answers)
- pulumi/guides: Interactive guides (embedded at /guides)
These are integrated via CloudFront origin routing and Pulumi stack references.
Note: The registry is NOT part of the Hugo build. Only content/registry.md exists locally as a landing page. The actual registry application runs from a separate repository and is integrated via CloudFront origin routing.
The repository includes a dev container configuration with all required tools:
# Open in VS Code with Dev Containers extension
code .
# Select "Reopen in Container"Install Required Tools:
This project uses mise to manage tool versions for local development. Install mise first, then run the following to install all mise-managed tools (see mise.toml):
mise install
# Hugo 0.157.0
# macOS:
brew install hugo@0.157.0
# Linux: Download from https://github.com/gohugoio/hugo/releases/tag/v0.157.0
# Pulumi CLI
curl -fsSL https://get.pulumi.com | sh
# Python 3.13+
# macOS:
brew install python@3.13
# Linux: Use your package managerInstall Dependencies:
make ensureThis will:
- Run
clean.shto remove old artifacts - Install Node.js dependencies for root, theme, theme/stencil, and infrastructure
- Build theme assets with webpack
- Warn about expected TypeDoc peer dependency warnings (these are safe to ignore)
# Basic dev server (fast, uses built assets)
make serve
# Dev server with asset rebuilding (watches for CSS/JS changes)
make serve-all
# Serve the built site (test production build locally)
make serve-staticThe site will be available at http://localhost:1313.
Note: The dev server uses
--buildDraftsand--buildFutureflags, showing content not visible in production.
Note: Hugo's dev server (
make serve,make serve-all) only serves content fromcontent/andstatic/. It does not servestatic-prebuilt/, which is where the auto-generated SDK reference docs live. As a result, any link under/docs/reference/pkg/{nodejs,python,dotnet,java}/...will 404 in dev mode. To preview SDK reference pages locally, runmake build(which copiesstatic-prebuilt/intopublic/viamake copy_static_prebuilt) and thenmake serve-static.
# Full production build
make build
# This runs:
# 1. make build-assets (webpack compilation)
# 2. build-site.sh (Hugo build + optimization)Output directory: public/
In normal operation, SDK and CLI reference docs are regenerated automatically by the per-surface GitHub Actions workflows whenever an upstream source repo cuts a release. See the Generating SDK and CLI documentation section of the README for the full table of workflows and output paths.
To regenerate locally (e.g. when modifying a generator script):
# TypeScript SDK (pulumi package)
NOBUILD=true PKGS=pulumi ./scripts/run_typedoc.sh
# Python SDK — one package per invocation
PACKAGE=pulumi ./scripts/generate_python_docs.sh
PACKAGE=pulumi_policy ./scripts/generate_python_docs.sh
PACKAGE=pulumi_esc_sdk ./scripts/generate_python_docs.sh
# Pulumi CLI reference (uses the currently-installed `pulumi` binary)
PULUMI_EXPERIMENTAL=true pulumi gen-markdown ./content/docs/iac/cli/commandsGenerated docs go to static-prebuilt/docs/reference/pkg/ (SDK) and content/docs/{iac,esc}/cli/commands/ (CLI).
# Run linting checks
make lint
# Auto-format code
make format
# Check for broken links
make check_links# New blog post
make new-blog-post
# New tutorial
make new-tutorial
# New example program
make new-example-program./
├── content/ # All content (docs, blog, etc.)
│ ├── docs/ # Documentation
│ ├── blog/ # Blog posts
│ ├── registry.md # Registry landing page (redirect)
│ └── ...
├── layouts/ # Hugo templates
├── data/ # YAML data files (menu sections, etc.)
├── theme/ # CSS/JS source code
│ ├── src/ # TypeScript and Sass source
│ ├── stencil/ # Web components
│ └── package.json # Theme dependencies
├── assets/ # Compiled assets (generated)
│ ├── js/ # Compiled JavaScript
│ └── css/ # Compiled CSS
├── static/ # Static files (copied as-is)
├── static-prebuilt/ # Generated SDK documentation
│ └── docs/reference/pkg/
├── public/ # Final built site (generated)
├── scripts/ # Build and deployment scripts
├── infrastructure/ # Pulumi IaC for deployment
├── config/ # Hugo configuration
│ ├── _default/ # Base configuration
│ └── production/ # Production overrides
├── .github/workflows/ # GitHub Actions (24 workflows)
├── Makefile # Build targets
└── BUILD-AND-DEPLOY.md # This document
| Target | Description | Dependencies |
|---|---|---|
default / all |
Runs complete build | banner, generate, build |
build |
Full site build | build-assets, build-site.sh |
build-assets |
Compile theme JS/CSS | yarn build in theme/ |
generate |
Generate SDK/CLI docs | TypeDoc, Sphinx, pulumi CLI |
serve |
Local dev server (port 1313) | serve.sh |
serve-all |
Dev server + asset watch | serve.sh + webpack watch |
serve-static |
Serve built public/ dir | http-server on port 8080 |
| Target | Description |
|---|---|
ensure |
Install all dependencies |
clean |
Remove node_modules, public/, resources/ |
update-repos |
Sync external Pulumi repositories |
| Target | Description |
|---|---|
lint |
Run markdown linting and Prettier checks |
format |
Auto-format with Prettier |
test / test-programs |
Run example program tests (preview mode) |
test-review-pipeline |
Run the review pipelines' own test suites (pytest + standalone harnesses + every --self-test) |
check_links |
Validate all links in production site |
check_search_urls |
Validate search index URLs |
| Target | Description | Used By |
|---|---|---|
ci_push |
Production deployment workflow | build-and-deploy.yml |
ci_pull_request |
PR validation and preview | pull-request.yml |
ci_pull_request_closed |
Cleanup PR resources | pr-closed.yml |
ci_bucket_cleanup |
Remove old S3 buckets | bucket-cleanup.yml |
ci_update_search_index |
Update Algolia search | update-search-index.yml |
| Target | Description |
|---|---|
new-blog-post |
Create new blog post |
new-tutorial |
Create new tutorial |
new-template |
Create new template |
new-example-program |
Create new example program |
All scripts are located in scripts/.
The primary build script that orchestrates the entire build process.
What it does:
-
Sets environment variables for bundle IDs:
ASSET_BUNDLE_ID=${git-sha-short or pr-{num}-{sha}} CSS_BUNDLE_ID=${ASSET_BUNDLE_ID}
-
Exports asset paths for Hugo templates:
REL_CSS_BUNDLE=/css/styles.${ASSET_BUNDLE_ID}.css REL_JS_BUNDLE=/js/bundle.min.${ASSET_BUNDLE_ID}.js
-
Copies prebuilt documentation:
make copy_static_prebuilt
-
Runs Hugo build with optimization:
hugo --minify --buildFuture --templateMetrics # For preview/testing # Production omits --buildFuture
-
Generates search index data:
node scripts/content/generate-docs-content.js
-
Minifies and optimizes CSS:
yarn run minify-css
Usage:
# Production build
./scripts/build-site.sh
# Preview build (for PRs)
./scripts/build-site.sh previewInstalls and verifies all required dependencies.
What it does:
-
Checks for required tools:
- Node.js 24.x
- Hugo 0.157.0
- Yarn 1.22.x
-
Installs dependencies for:
- Root package.json
- infrastructure/package.json
- theme/package.json
- theme/stencil/package.json
-
Warns about expected TypeDoc peer dependency conflicts (safe to ignore)
Usage:
make ensure
# or
./scripts/ensure.shStarts the Hugo development server with live reload.
What it does:
- Sets
ASSET_BUNDLE_IDfor development - Runs Hugo with:
--renderToMemory: No disk writes--buildDrafts: Show draft content--buildFuture: Show future-dated content (can be disabled withBUILD_FUTURE=false)- Fast render is on by default; set
DISABLE_FAST_RENDER=trueto pass--disableFastRender(re-renders the full site on every change — slower, but accurate for list pages, menus, and the search index)
- Connects to tf2pulumi conversion service
- Uses Hugo's default binding (localhost:1313)
Usage:
make serve
# or
./scripts/serve.shComplete production deployment pipeline.
Steps:
- Build site:
./scripts/build-site.sh - Sync to S3:
./scripts/sync-and-test-bucket.sh update - Generate search index
- Wait for in-progress operations:
node await-in-progress.js(records the time spent waiting in.build-queue-wait-seconds, whichscripts/ci-build-duration-alert.shsubtracts so a queued run isn't reported as a slow build) - Pulumi infrastructure update:
./scripts/run-pulumi.sh - Generate S3 redirects:
./scripts/make-s3-redirects.sh
Usage:
make ci_push
# or
./scripts/ci-push.shBuilds and deploys PR preview environments.
Steps:
- Check for AWS/Pulumi credentials (skip for forks)
- Build site:
./scripts/build-site.sh preview - Sync to preview bucket:
./scripts/sync-and-test-bucket.sh preview - Generate search index for preview
- Pulumi preview (non-destructive):
./scripts/run-pulumi.sh - Generate S3 redirects
- Run Lighthouse audits (Mobile + Desktop) on preview and post metrics to PR comment (only when the PR includes UI-related changes — e.g., layouts, theme, assets, static resources, or Hugo config):
./scripts/run-lighthouse-pr.sh
Usage:
make ci_pull_request
# or
./scripts/ci-pull-request.shCreates S3 bucket, syncs content, and validates deployment.
What it does:
-
Creates S3 bucket with atomic naming:
www-{environment}-pulumi-docs-origin-{build-id} -
Configures bucket:
- Website hosting (index.html, 404.html)
- Public access (ACL enabled)
- CORS configuration
-
Syncs content using
s5cmdfor parallel uploads:s5cmd sync public/ s3://{bucket}/CI/CD workflows install
s5cmdv2.3.0 for significantly faster S3 uploads compared toaws s3 sync. -
Validates deployment:
- Checks for at least 1000 index.html files
- Verifies bucket accessibility
-
Generates metadata:
{ "bucket": "bucket-name", "commit": "git-sha" } -
Translates Hugo redirects to S3 format
Usage:
# Production update
./scripts/sync-and-test-bucket.sh update
# PR preview
./scripts/sync-and-test-bucket.sh previewThe asset pipeline transforms source files into optimized bundles for production.
Location: theme/webpack.config.js
Entry Points:
{
bundle: './src/ts/main.ts', // Main site JavaScript
marketing: './src/ts/marketing.ts', // Marketing pages
algolia: './src/ts/algolia-entry.ts', // Search (Algolia)
'consent-manager': './src/ts/consent-manager/index.ts', // Cookie consent (vanilla TS)
'header-nav': './src/ts/header-nav.ts', // Site header navigation
}Output:
Entry bundles use content hashing for cache busting: [name].[contenthash:8].js.
Async chunks use a similar pattern: chunk-[contenthash:8].js.
static/js/bundle.<hash>.js
static/js/marketing.<hash>.js
static/js/algolia.<hash>.js
static/js/consent-manager.<hash>.js
static/js/header-nav.<hash>.js
static/js/chunk-<hash>.js
assets/css/bundle.css
assets/css/marketing.css
A manifest is written to data/js_manifest.json mapping entry names to hashed filenames so Hugo can emit the correct <script> tags.
Loaders:
- TypeScript:
ts-loader - Sass:
sass-loader→css-loader - PostCSS: @tailwindcss/postcss (Tailwind v4), Autoprefixer
Plugins:
MiniCssExtractPlugin: Extract CSS to separate filesLimitChunkCountPlugin: Keep each entry point as a single chunkWebpackShellPluginNext: Runs Stencil build before webpack- Custom
JsManifestPlugin: Writesdata/js_manifest.jsonafter each build
Multi-stage CSS optimization pipeline:
-
Tailwind CSS v4 Compilation
- Entry points:
theme/src/scss/main.scss(docs) andtheme/src/scss/_marketing.scss(marketing pages) - Uses
@import "tailwindcss"(v4 CSS-first config) instead of@tailwinddirectives - Theme tokens (colors, fonts, breakpoints) defined via
@theme {}blocks in CSS - Content sources specified via
@sourcedirectives (replaces JScontentarray) - Responsive breakpoints use
@include screen-*SCSS mixins (which emit@media screen(...), replacing the removed@screendirective) - PostCSS plugin:
@tailwindcss/postcss(replaces the oldtailwindcssPostCSS plugin)
- Entry points:
-
PostCSS Processing
- Autoprefixer for browser compatibility
- Custom PostCSS plugins
-
Unused-class purging is handled by Tailwind v4 itself via
@sourcedirectives intheme/src/scss/main.scssand_marketing.scss— Tailwind only emits utilities it finds in the scanned files. PurgeCSS used to run as a second pass but was removed: it is incompatible with Tailwind v4's nested-CSS variant output (.foo { &:hover { ... } }) and silently dropped everyhover:,focus:,focus-visible:,space-y-*, anddata-[...]arbitrary-variant rule. -
CSSNano Minification (Production Only, via
scripts/minify-css.js)- Removes whitespace and comments
- Optimizes declarations
- Merges rules
Script: scripts/minify-css.js
Output:
public/css/bundle.{CSS_BUNDLE_ID}.css
public/css/marketing.{CSS_BUNDLE_ID}.css
- Critical CSS inlining
- Uses beasties to extract above-the-fold CSS and inline it into the HTML
- Runs after minification so it operates on final stylesheets
- Currently applied to the homepage only (
public/index.html) - Original CSS files are preserved (
pruneSource: false); the full stylesheet is still loaded async
Script: scripts/inline-critical-css.js
Dependencies:
- jQuery (provided globally)
- Stencil web components (separate build)
- Custom TypeScript modules
Output:
public/js/bundle.min.{ASSET_BUNDLE_ID}.js
public/js/marketing.min.{ASSET_BUNDLE_ID}.js
Assets are versioned with bundle IDs for cache busting:
Format:
- Production:
{git-sha-short}(e.g.,a1b2c3d) - PR Preview:
pr-{number}-{git-sha-short}(e.g.,pr-123-a1b2c3d)
Environment Variables:
ASSET_BUNDLE_ID=a1b2c3d
CSS_BUNDLE_ID=a1b2c3d
REL_CSS_BUNDLE=/css/styles.a1b2c3d.css
REL_JS_BUNDLE=/js/bundle.min.a1b2c3d.jsHugo Integration:
Templates access bundle paths via:
{{ getenv "REL_CSS_BUNDLE" }}
{{ getenv "REL_JS_BUNDLE" }}
This ensures every deployment has unique asset URLs, preventing cache issues.
The site uses a single fingerprinted SVG sprite for all UI icons (Phosphor, custom Figma-exported, and brand logos). The sprite and all per-icon source SVGs are build artifacts — they are generated by make ensure and are not committed to the repo.
Generated, gitignored paths:
assets/icons/phosphor/ # Synced from @phosphor-icons/core (regular, bold, fill, duotone)
assets/icons/brand/ # Synced from simple-icons + svglogos.dev fallbacks
assets/icons/sprite.svg # Combined sprite, fingerprinted on Hugo publish
assets/icons/sprite-manifest.json
assets/icons/custom/_inbox/ # Staging dir for raw Figma exports (not the normalized output)
The normalized custom icons under assets/icons/custom/*.svg ARE committed (they are the canonical source for the custom set); only the _inbox/ staging dir is ignored.
Build steps (run as part of make ensure, in this order):
node scripts/sync-icons.js— copies Phosphor weights intoassets/icons/phosphor/and downloads brand logos intoassets/icons/brand/.node scripts/normalize-custom-icons.js— converts Figma exports underassets/icons/custom/_inbox/<FolderName>/Format=Outline, Weight=<X>.svginto normalized files underassets/icons/custom/<slug>{,-bold,-duotone}.svg(strips fixed dimensions, setsfill="currentColor", preserves duotone opacity).node scripts/build-icon-sprite.js— tokenizeslayouts/,content/,data/, andarchetypes/to determine which icons are referenced, then writes a singleassets/icons/sprite.svgcontaining the matching<symbol>elements plusassets/icons/sprite-manifest.json. Symbol IDs follow a fixed scheme:p-{name}-{weight}(Phosphor),c-{name}-{weight}(custom),b-{name}(brand).
Templates render icons through the icon.html partial, which emits <svg><use href="/icons/sprite.<hash>.svg#<id>"/></svg>.
Reviewer notes:
- If you see
assets/icons/phosphor/,assets/icons/brand/,assets/icons/sprite.svg, orassets/icons/sprite-manifest.jsonin a diff, something is wrong — these are gitignored. Re-runmake ensurelocally to regenerate. - A missing icon at runtime usually means
build-icon-sprite.jsdidn't see the icon name in the source tree. Check that the name is referenced literally (the tokenizer doesn't follow variables).
Adding a new icon:
- Phosphor or brand icon already in the source libraries: just reference it in a template/partial via
{{ partial "icon.html" (dict "name" "<name>") }}(or"brand/<name>") and re-runmake ensure. The sprite builder will pick it up. - New brand logo not in
simple-icons: add an entry to theBRAND_ICONSallowlist inscripts/sync-icons.jswith asvglogosUrlfallback. - New custom (Figma) icon: drop the Figma-exported folder into
assets/icons/custom/_inbox/<FolderName>/, add aFolderName → slugentry toICON_MAPinscripts/normalize-custom-icons.js, and runmake ensure. Commit the resulting normalized files underassets/icons/custom/.
Base Configuration: config/_default/config.yml
Key settings:
baseURL: https://www.pulumi.com/
timeout: 300000ms # 300 seconds
enableGitInfo: true
enableRobotsTXT: true
markup:
goldmark:
renderer:
unsafe: true # Allow HTML in markdownProduction Overrides: config/production/config.yml
baseURL: https://www.pulumi.com/Development:
hugo server --buildDrafts --buildFuture --renderToMemoryProduction:
hugo --minify --templateMetrics # Note: --buildFuture is omitted in productionPreview (PRs):
hugo --minify --buildFuture --baseURL={preview-url}Hugo processes 46+ content directories:
/content/docs/→ Documentation/content/blog/→ Blog posts/content/product/→ Product pages/content/case-studies/→ Customer stories
Note: content/registry.md is a single landing page file, not a content directory. The full registry application is served from the separate pulumi/registry repository via CloudFront origin routing.
/learn(tutorials, official templates, community examples, glossary) is served the same way, from pulumi/marketing-web.
Templates are in /layouts/ with various shortcodes for:
- Code examples
- Videos and images
- API references
- UI components
Many shortcodes have paired .html and .markdown.md versions — the HTML version renders for web browsers, and the markdown version produces clean markdown for content negotiation output.
Navigation menu: The docs left-nav menu sections are data-driven via data/docs_menu_sections.yml. The menu partial (layouts/partials/docs/menu.html) iterates over this data file rather than using hardcoded section names. The LLM sitemap JSON (layouts/partials/llm-sitemap-walk.json) also uses this data to generate a hierarchical navigation index.
Hugo generates:
- HTML pages from markdown
- RSS feeds
- Sitemap.xml
- robots.txt
- Meta-refresh redirect pages (from aliases)
- Markdown output (
.md) for/docs/pages, the homepage,/what-is/,/product/, and/pricing/(for content negotiation) - LLM sitemap JSON (
llmsitemap) — hierarchical JSON index of docs navigation, served at/docs/llm-sitemap.json - LLM index (
llms) — curated text overview at/llms.txtfor AI agents
Markdown output format: Hugo generates clean markdown versions of documentation pages alongside HTML. These are served via CloudFront content negotiation when clients send Accept: text/markdown. The conversion is handled by an 8-phase pipeline in layouts/partials/docs/markdown-pipeline.md that converts rendered HTML back to markdown (Chroma → fenced code blocks, HTML tags → markdown syntax, choosable options → chooser comment blocks, etc.).
The same negotiation covers the marketing front door: the homepage, /what-is/, /product/, and /pricing/ emit index.md artifacts (enabled via outputs/cascade front matter), served by a second viewer-request CloudFront Function on the default cache behavior (marketing-markdown-negotiation in infrastructure/cloudfrontFunctions.ts). That function rewrites only an allowlist of prefixes — a rewrite on a path with no .md artifact would 404, so extending coverage to a new section means BOTH enabling the markdown output for that section AND adding its prefix to the function. Template-driven pages (frontmatter sections: arrays) render markdown via layouts/partials/markdown/sections.md, a type-agnostic walker over the sections' textual fields.
Layout files:
layouts/docs/single.md— Markdown output for single pageslayouts/docs/list.md— Markdown output for list pageslayouts/index.md— Markdown output for the homepagelayouts/page/template-page.md— Markdown output for template-driven pages (frontmattersections:)layouts/page/pricing.md— Markdown output for/pricing/(tiers, edition comparison, FAQ)layouts/_default/single.md,layouts/_default/list.md— generic markdown fallbacks for markdown-enabled sections whose pages use bespoke layoutslayouts/docs/list.llmsitemap.json— Hierarchical JSON sitemaplayouts/index.llms.txt— Curated text overview at/llms.txt
Output formats are defined in config/_default/config.yml under outputFormats and outputs.
Context7 indexes our content for AI coding assistants. Each indexed surface has its own context7.json pointing at the corresponding Context7 project. Three copies live in this repo — each served at a different URL because Context7 looks for the file at a specific path per indexed site:
context7.json(repo root) — served on GitHub atgithub.com/pulumi/docs/blob/master/context7.json. Indexes this repository itself.static/context7.json— published athttps://www.pulumi.com/context7.json. Points Context7 at thellms.txtproject that covers the marketing site and/llms.txtindex.static/docs/context7.json— published athttps://www.pulumi.com/docs/context7.json. Points Context7 at the docs-site project (context7.com/websites/pulumi).
All three share the same public key and are safe to commit. If the key or project URLs need to rotate, update all three together. Current maintainer for all Context7 onboarding: csoper@pulumi.com.
With --minify flag, Hugo minifies:
- HTML (remove whitespace, comments)
- CSS (via external process)
- JS (via external process)
- JSON
- XML (sitemap, RSS)
The docs site includes auto-generated API reference documentation from multiple sources.
Script: scripts/run_typedoc.sh
Packages Generated:
-
pulumi - Core Pulumi SDK
- Source: pulumi/pulumi repository
- Output:
static-prebuilt/docs/reference/pkg/nodejs/pulumi/
-
policy - Pulumi Policy SDK
- Source: pulumi/pulumi-policy repository
- Output:
static-prebuilt/docs/reference/pkg/nodejs/pulumi/policy/
-
esc-sdk - ESC SDK
- Source: pulumi/esc-sdk repository
- Output:
static-prebuilt/docs/reference/pkg/nodejs/pulumi/esc-sdk/
Configuration:
- TypeDoc version: 0.28.15
- Plugin:
typedoc-plugin-script-injectfor custom scripting - Format: HTML
Usage:
# Generate the pulumi TypeScript SDK docs (this is what the workflow runs)
NOBUILD=true PKGS=pulumi ./scripts/run_typedoc.shOr trigger the workflow directly:
gh workflow run pulumi-sdk-typescript-docs.yml --repo pulumi/docs --ref master -f version=<pulumi-version>Script: scripts/generate_python_docs.sh
Packages Generated (one per invocation, via PACKAGE env var):
pulumi(Pulumi SDK)pulumi_policy(Pulumi Policy SDK)pulumi_esc_sdk(Pulumi ESC SDK)
Each package is built by a dedicated workflow that calls this script with the appropriate PACKAGE value. pulumi_terraform was previously built here but moved to the Pulumi Registry; see scripts/redirects/pulumi-terraform-python-redirects.txt.
Configuration:
- Sphinx theme: ReadTheDocs
- Format: dirhtml
- Output:
static-prebuilt/docs/reference/pkg/python/<PACKAGE>/
Usage:
PACKAGE=pulumi ./scripts/generate_python_docs.sh
PACKAGE=pulumi_policy ./scripts/generate_python_docs.sh
PACKAGE=pulumi_esc_sdk ./scripts/generate_python_docs.shOr trigger a workflow:
gh workflow run pulumi-sdk-python-docs.yml --repo pulumi/docs --ref master -f version=<version>
gh workflow run pulumi-policy-sdk-python-docs.yml --repo pulumi/docs --ref master -f version=<version>
gh workflow run pulumi-esc-sdk-python-docs.yml --repo pulumi/docs --ref master -f version=<version>Command: pulumi gen-markdown
Generates markdown documentation for all Pulumi CLI commands.
Output: content/docs/iac/cli/commands/
Format:
pulumi.md
pulumi-cancel.md
pulumi-config.md
pulumi-destroy.md
...
Automation:
Updated automatically via pulumi-cli.yml workflow when new CLI versions are released.
The repository uses 24 GitHub Actions workflows organized into categories. All workflows are in .github/workflows/.
Purpose: Deploy the site to production (<www.pulumi.com>)
Triggers:
- Push to
masterbranch - Scheduled: Daily at 6 AM Eastern (7 AM during DST), noon Pacific (1 PM during DST)
- Manual:
workflow_dispatch
Environment: Production (AWS Account: 388588623842)
Jobs:
-
buildSite
- Checkout code
- Fetch secrets from Pulumi ESC
- Setup: Node.js 24, Go 1.26, Hugo 0.157.0
- Configure AWS credentials via OIDC (role: ContinuousDelivery, 2-hour session)
- Install Pulumi CLI
- Run
make ci_push:- Build site
- Create S3 bucket with atomic naming
- Sync content to S3
- Run Cypress browser tests
- Generate search index
- Update CloudFront via Pulumi
- Apply S3 redirects
- Archive browser test videos and bucket metadata
-
notify
- Sends Slack alert to
docs-opschannel on failure
- Sends Slack alert to
Infrastructure Deployed:
- S3 origin bucket (versioned by commit SHA)
- CloudFront distribution (updated to point to new bucket)
- Lambda@Edge functions
- Route53 records
- Response headers policies
Typical Duration: 8-12 minutes
Purpose: Deploy to testing environment (<www.pulumi-test.io>)
Triggers:
- Push to
masterbranch - Manual:
workflow_dispatch
Environment: Testing (AWS Account: 571684982431)
Differences from Production:
- Deploys to separate AWS account
- Uses testing CloudFront distribution
- Sends failures to
docs-ops-testSlack channel - Parallel testing environment for validation
Usage: Test infrastructure changes before production deployment
Purpose: Build and validate PRs, create preview environments
Triggers:
- Pull requests to
masterorrelease/*branches - PR synchronize (new commits pushed)
Environment: Testing (AWS Account: 571684982431)
Security: Only runs deployment for PRs from the main repository (not forks)
Jobs:
-
buildSite
-
Check if PR is from fork (skip deployment if true)
-
Build site in preview mode
-
Create PR-specific S3 bucket:
www-testing-pulumi-docs-origin-pr-{PR_NUMBER}-{SHA} -
Sync built site to preview bucket
-
Run Cypress browser tests
-
Generate search index
-
Run Pulumi preview (non-destructive)
-
Post preview URL to PR comments:
http://www-testing-pulumi-docs-origin-pr-123-abc1234.s3-website.us-west-2.amazonaws.com -
Run Lighthouse performance audits (Mobile + Desktop) and post results as a separate PR comment (skipped for content-only PRs; only runs when UI-related files are changed)
-
Archive test results and metadata
-
-
notify
- Slack alert on failure
Preview Lifecycle:
- Created on first PR commit
- Updated on subsequent commits
- Deleted when PR is closed
Purpose: Clean up PR preview resources
Triggers:
- Pull request closed (merged or abandoned)
Environment: Testing
Jobs:
- do_cleanup
-
Find all S3 buckets matching
*-pr-{PR_NUMBER}-* -
Delete buckets and all contents
-
Post cleanup notification to PR:
Site previews for this pull request have been removed.
-
Why It Matters: Prevents accumulation of abandoned preview buckets, reducing AWS costs.
Purpose: Auto-generate CLI documentation when Pulumi CLI is released
Triggers:
- Repository dispatch event from pulumi/pulumi repository
- Triggered automatically on Pulumi CLI release
Jobs:
-
build-pulumi-cli-docs
- Checkout docs and pulumi repositories
- Install: pulumictl, Pulumi CLI, Go, Hugo, Node, Python, .NET
- Generate TypeScript SDK docs with TypeDoc
- Generate Python SDK docs with Sphinx
- Generate CLI command docs with
pulumi gen-markdown - Update version files:
static/latest-versionstatic/latest-dev-version
- Create feature branch:
pulumi/{run-id}-{run-number} - Commit changes with bot credentials
- Push branch
-
pull-request
- Create PR with auto-merge label
- Link to triggering pulumi/pulumi release
- Auto-merge if tests pass
-
notify
- Slack alert on failure
Why It Matters: Keeps CLI documentation synchronized with releases automatically.
Purpose: Update CMDA CLI version
Triggers:
- Repository dispatch from CMDA repository
Process:
- Updates version file for customer-managed-deployment-agent
- Creates automated PR
Purpose: Run comprehensive tests on example programs
Triggers:
- Daily at 8:00 AM UTC
- Pull requests to master
- Manual:
workflow_dispatch
Platform: GitHub-hosted runner (ubuntu-latest), with jlumbroso/free-disk-space to reclaim disk space before tests run
Setup:
- Disk space reclaimed via
jlumbroso/free-disk-space(tool-cache: false,dotnet: falseto preserve caches used by later setup steps) - Multi-language runtimes:
- Go 1.26
- Node.js 20
- Python 3.9
- .NET 8.0
- Java 11
- Hugo 0.157.0
- Latest Pulumi CLI
- Kubernetes KinD cluster
Cloud Authentication:
- AWS via OIDC (gets credentials from Pulumi ESC)
- GCP via workload identity federation
- Azure credentials from ESC
Tests: Runs make test on ~425 example programs across:
- Languages: TypeScript, Python, Go, C#, Java, YAML
- Clouds: AWS, GCP, Azure, Kubernetes
- Scenarios: Simple deployments, complex architectures
Notification: Slack alert for scheduled failures only (not PRs)
Typical Duration: 2-2.5 hours (scheduled runs), 3-5 minutes (PR runs)
Status:
Purpose: Keep example program dependencies up to date
Triggers:
Daily at 6:00 AM UTC(schedule disabled)- Manual:
workflow_dispatch(but will likely fail without fixes)
Jobs:
- Upgrade Go module dependencies in example programs
- Run tests to verify upgrades work
- Create PR with branch
examples/upgrade - Uses PULUMI_BOT_TOKEN for authentication
Why It Matters: Prevents example programs from using outdated dependencies with security vulnerabilities.
Note: The workflow consistently fails due to GitHub Actions runner disk space exhaustion when testing 385+ example programs. The schedule has been disabled while we investigate proper fixes.
Purpose: Clean up old S3 buckets in production
Triggers:
- Daily at 3:00 PM UTC
- Manual: Not currently configured
Environment: Production (AWS Account: 388588623842)
Jobs:
- Run
make ci_bucket_cleanup - Identify buckets older than retention period
- Delete old origin buckets
- Clean up AWS Parameter Store records
Retention Policy: Configurable (typically 7-30 days)
Purpose: Clean up old S3 buckets in testing
Triggers:
- Daily at 3:00 PM UTC
- Manual:
workflow_dispatch
Environment: Testing (AWS Account: 571684982431)
Process: Same as production cleanup but for testing environment
Purpose: Verify all internal and external links
Triggers:
- Daily at 3:00 PM UTC
- Manual:
workflow_dispatch
Jobs:
- Run
make check_links - Crawl production site (<www.pulumi.com>)
- Check all links (internal and external)
- Merge real-404 server-log hits from the reader-signals export into
.broken-links.json(scripts/link-checker/merge-404-signal.py; no-op until the data-team export exists) - Report broken links
Output: Slack notification with broken link report
Purpose: Validate search index integrity
Triggers:
- Daily at 3:00 PM UTC
- Manual:
workflow_dispatch
Jobs:
- Run
make check_search_urls - Query Algolia search index
- Verify all indexed URLs are accessible
- Report missing or broken URLs
Why It Matters: Ensures search results don't link to 404 pages.
Purpose: Monitor site performance and accessibility
Triggers:
- Daily at 3:00 PM UTC
- Manual:
workflow_dispatch
Pages Tested:
- Homepage (<www.pulumi.com>)
- Product page
- Pricing page
- Get Started guide
- Documentation (concepts)
- Registry homepage
- Registry package page (AWS S3 bucket)
Metrics:
- Performance
- Accessibility
- Best Practices
- SEO
Output: Lighthouse scores and recommendations
Purpose: Update Algolia search index on demand
Triggers:
- Hourly (every 60 minutes)
- Manual:
workflow_dispatch
Environment: Production
Jobs:
- Run
make ci_update_search_index - Extract content from built site
- Update Algolia indices
- Apply index settings and ranking rules
Indices Updated:
- pulumi (main documentation)
- blog posts
- registry packages
Purpose: Sync private fork with upstream
Triggers:
- Every 15 minutes
- Manual:
workflow_dispatch
Target: Only runs on private fork repositories (not pulumi/docs)
Jobs:
- Sync latest commits from pulumi/docs to downstream fork
- Uses Fork-Sync-With-Upstream action
- Preserves private fork changes
Why It Matters: Keeps private documentation fork synchronized with public repository.
Purpose: Automatically schedule social media posts (X, LinkedIn, Bluesky) for new blog content.
Triggers:
- Push to
masterbranch - Manual:
workflow_dispatch
Environment: Production (AWS Account: 388588623842)
How It Works:
- Detects blog posts changed since the last processed commit (tracked in S3 state)
- Reads
social.twitter,social.linkedin,social.blueskyfrom frontmatter - Posts dated today or in the past are posted immediately; future-dated posts are scheduled for 10 AM Eastern
- Posts older than 2 days are skipped
- State is tracked in S3 (
posted.json) for idempotency — if state can't be loaded, the script aborts rather than risk double-posting
Required Secrets (from ESC):
UPLOAD_POST_API_KEY— API key for upload-post.comPULUMI_ACCESS_TOKEN— For reading thesocialStateBucketNamePulumi stack output
Required Infrastructure:
- S3 bucket for state tracking (name read from Pulumi stack output
socialStateBucketName)
Rollout Status: Currently in test mode (PROD_MODE = False), posting to test accounts. Flip to prod once validated.
Typical Duration: < 1 minute
The repository includes 10 additional utility workflows for automation and project management:
Automation and Auto-merge:
- Native auto-merge: Bot PR workflows (
pulumi-cli.yml,pulumi-cli-dev-version.yml,esc-cli.yml,customer-managed-workflow-agent-cli.yml) enable GitHub's native auto-merge viagh pr merge --auto --squashafter creating the PR. This replaces the former polling-basedautomerge-workflow.yml. - auto-approve-for-auto-merge.yml: Auto-approve PRs that meet auto-merge criteria (trusted bots, dependency updates). Uses the
automation/mergelabel to gate approval — note that this label now drives auto-approval only; auto-merge is handled natively by GitHub.
AI-Assisted Development:
- claude.yml: AI-assisted code analysis and suggestions (triggered by @claude mentions in issues/PRs)
- claude-code-review.yml: AI-powered code review automation for pull requests
- claude-social-review.yml: AI-powered review of social media post copy generated for blog post PRs
- review-existing-content.yml / content-review-article.yml: Daily existing-content review — deterministic selection fans out one per-article worker per page. Three lanes, each with its own count variable:
fix(CONTENT_REVIEW_COUNT, unset = 3/run) reviews an editable page and opens a PR for what it fixed;glowup(GLOWUP_COUNT, unset = 1/run) rehabs one page from its banked findings backlog;report(REPORT_REVIEW_COUNT, unset = off) fact-checks a page a generator owns — it runs the claim pipeline, writes the page's claims to the S3 claims index, and changes nothing. The report lane has no model step at all (nothing to fix, no PR body to write) and its verdict is written by the workflow; contradictions it finds are reported to #docs-ops with a prefilled upstream issue, never as stale-claims markers no PR here could retire. Which lane a page belongs to comes fromeditable/reviewableinstrategic-tiers.yaml(#20996 — before that split, "a generator owns this file" also meant "never look at it", hiding 30% ofcontent/docs/from the fact-check entirely). - blog-review-index.yml: Daily blog known-issues indexing — deterministic selection (
scripts/blog-review/select-posts.py), one unprivileged model review per post (matrix), one deterministic record job. FLAG-ONLY: findings land in S3 (blog-review/prefix in the content-review ledger bucket:ledger/,index/,runs/,index/_summary.json); no content edits, no PRs. On/off/cadence via theBLOG_REVIEW_COUNTrepo variable (unset = 5/run,'0'= off). The index is evidence for a future noindex decision process (block_external_search_index: trueon rotted, low-value posts).
The first two workflows include a permission check step that verifies the triggering user has write access to the repository before running Claude. Users without write access will see the workflow skip Claude execution. The social review workflow runs only on internal PRs from non-bot authors.
Content-review worker privilege model (content-review-article.yml): the per-article worker is split into two jobs with opposite privilege profiles, because the review model consumes artifacts derived from fetched external URLs (a prompt-injection surface):
- The
reviewjob runs the model unprivileged: read-scoped default token,persist-credentials: falseon checkout, noenvironment: production, no ESC or AWS credentials, and a preflight step that fails the job if credentials are detected in the model's environment. The model edits the working tree only and hands its changes to the next job as a patch in a run artifact. - The
publishjob is deterministic only (no model) and holds the production credentials (pulumi-bot token, AWS role for the S3 ledger). Before pushing anything it runsscripts/content-review/publish-gate.py, which enforces the verdict schema, the diff scope (a fix may touch only the reviewed article plus shared render-time sources; a retirement onlycontent/,scripts/redirects/, and the docs menu data), and theno_retireveto from the selection queue. The branch name is derived from the queue slug, never chosen by the model.
This mirrors the pre-merge review's posture (claude-code-review.yml runs its model with no push credentials); the accepted residual risk in the review job is the Anthropic API key the model inherently runs on.
Project Management:
- add-triage-label.yml: Automatically apply triage labels to new issues
- add-to-project.yml: Add issues and PRs to GitHub Projects for tracking
Secret Management:
- export-repo-secrets.yml: Export repository secrets for CI/CD consumption
- export-secrets.yml: General-purpose secrets export utility for workflows
Development Versions:
- pulumi-cli-dev-version.yml: Handle development and pre-release versions of Pulumi CLI documentation
These workflows support repository maintenance, automation, and developer experience but are not part of the core build and deployment pipeline documented in detail above.
| Workflow | Trigger | Environment | Duration | Purpose |
|---|---|---|---|---|
| build-and-deploy | Push to master, Scheduled | Production | 8-12 min | Production deployment |
| testing-build-and-deploy | Push to master, Manual | Testing | 8-12 min | Testing deployment |
| pull-request | PRs to master | Testing | 10-15 min | PR validation & preview |
| pr-closed | PR closed | Testing | <1 min | Cleanup preview resources |
| pulumi-cli | Repository dispatch | N/A | 5-10 min | Auto-generate CLI docs |
| esc-cli | Repository dispatch | N/A | <1 min | Update static/esc/latest-version pointer (read by pulumi/esc-action v1/v2) |
| scheduled-test | Daily 8 AM UTC, PRs | Testing | 2-2.5 hrs (scheduled), 3-5 min (PR) | Test example programs |
| scheduled-upgrade-programs | N/A | N/A (fails) | Update dependencies | |
| bucket-cleanup | Daily 3 PM UTC | Production | 2-5 min | Delete old buckets |
| bucket-cleanup-testing | Daily 3 PM UTC | Testing | 2-5 min | Delete old buckets |
| check-links | Daily 3 PM UTC | N/A | 5-10 min | Verify links |
| check-search-urls | Daily 3 PM UTC | N/A | 2-5 min | Validate search index |
| check-lighthouse | Daily 3 PM UTC | N/A | 3-8 min | Performance monitoring |
| update-search-index | Hourly | Production | 2-5 min | Update Algolia |
| schedule-social | Push to master, Manual | Production | < 1 min | Social media scheduling |
Note: The table above shows the 15 core deployment and testing workflows. An additional 11 utility workflows (automation, AI review, project management, secret management, dev versions) are listed in the "Other Workflows" section, bringing the total to 26 workflows.
All deployment infrastructure is managed as code using Pulumi (TypeScript). Infrastructure code is in infrastructure/.
Primary File: infrastructure/index.ts
Pulumi Stack Configuration:
- Production:
Pulumi.www-production.yaml - Testing:
Pulumi.www-testing.yaml
Stack References:
The docs infrastructure integrates with other Pulumi projects via stack references:
const registryStack = new pulumi.StackReference('pulumi/registry/production');
const answersStack = new pulumi.StackReference('pulumi/answers/production');
const aiAppStack = new pulumi.StackReference('pulumi/pulumi-ai-app-infra/prod');
const guidesStack = new pulumi.StackReference('pulumi/guides/production');These provide outputs like domain names, ALB ARNs, and distribution IDs for integration.
The docs CloudFront distribution uses StackReferences to dynamically configure origins from external stacks:
- Registry: Reads
cloudFrontDomainfrompulumi/registry/{environment} - Guides: Reads
cloudFrontDomainfrompulumi/guides/{environment} - Answers: Reads
cloudFrontDomainfrompulumi/answers/{environment}
When external stacks are updated:
- External stack (for example, registry) deploys → creates new CloudFront distribution with new domain
- Docs infrastructure automatically reads the updated output via StackReference on next deployment
- Docs CloudFront distribution origins are updated with the new domain
- CloudFront changes propagate globally (15-20 minutes)
Important: StackReferences always read the latest outputs from referenced stacks. No manual refresh is needed.
Origin Bucket (Ephemeral):
Created for each deployment with atomic naming:
www-{environment}-pulumi-docs-origin-{identifier}
Examples:
- Production:
www-production-pulumi-docs-origin-a1b2c3d4 - Testing:
www-testing-pulumi-docs-origin-pr-123-abc1234
Configuration:
- Website hosting enabled (index.html, 404.html)
- Public read access via ACL
- Object ownership: BucketOwnerPreferred
- Versioning: Disabled (ephemeral buckets)
- Lifecycle: Manual cleanup via bucket-cleanup workflows
Uploads Bucket (Persistent):
Stores user-uploaded assets and large files.
Bundles Bucket (Temporary):
Stores CSS/JS bundles during deployment.
Fallback Bucket (Optional):
Direct S3 website serving (not used in production).
Logs Bucket:
Stores CloudFront access logs.
Name: {website-domain}-website-logs
Format: Parquet
Delivery: CloudWatch Logs infrastructure v2
Purpose: Global CDN serving the production site
Domain Aliases:
- Production: <www.pulumi.com>
- Testing: <www.pulumi-test.io>
SSL/TLS:
- ACM Certificate (us-east-1)
- ARN from Pulumi config
- SNI-only (no dedicated IP)
- Minimum protocol: TLSv1.2_2021
Origins:
-
Main Origin - S3 website bucket
- Path: /
- Origin ID: S3-{bucket-name}
- Custom origin (website endpoint, not S3 endpoint)
-
Uploads Origin - Uploads bucket
- Path: /uploads
- Origin ID: uploads-bucket
-
Registry Origin - From pulumi/registry stack
- Path: /registry
- ALB from stack reference
-
Answers Origin - From pulumi/answers stack
- Path: /answers
- ALB from stack reference
-
Guides Origin - From pulumi/guides stack
- Path: /guides
- ALB from stack reference
Cache Behaviors:
| Path Pattern | Origin | TTL | Notes |
|---|---|---|---|
| Default | S3 Main | 10 min | General content |
| /css/*.css | S3 Main | 1 year | Versioned assets |
| /js/*.js | S3 Main | 1 year | Versioned assets |
| /registry/* | Registry | 30 minutes | Dynamic content, origin-proxied |
| /guides/* | Guides | 30 minutes | Dynamic content, origin-proxied |
| /learn* | Learn (pulumi/marketing-web) | 30 minutes | Origin-proxied; cache key includes Accept for the origin's markdown negotiation |
| /docs/* | S3 Main | 10 min | Content negotiation for Accept: text/markdown |
| /docs/reference/pkg/dotnet/* | S3 Main | 10 min | CloudFront Function lowercases URI (viewer-request); Lambda@Edge handles redirects (origin-request) |
| /ai | S3 Main | 1 week | 301 redirect to /product/neo/ (Lambda@Edge) |
| /ai/* | S3 Main | 1 week | 410 Gone (Lambda@Edge) |
| /uploads/* | Uploads | 1 hour | User uploads (legacy forwardedValues for CORS) |
| /*/rss.xml | S3 Main | 10 min | Syndication feeds (legacy forwardedValues for CORS) |
| /fonts/* | S3 Main | 1 year | Web fonts |
| /icons/* | S3 Main | 1 hour | Icons |
| /logos/brand/* | S3 Main | 30 minutes | Brand logos |
| /logos/* | S3 Main | 1 hour | Logos |
| /fingerprinted/* | S3 Main | 1 year (immutable) | Content-hashed assets |
| /js/components*.js | S3 Main | 0 (no cache) | Web-component loaders, names change per build |
| /metadata.json | S3 Main | 0 (no cache) | Build metadata |
Compression: Enabled (gzip, brotli)
Origin Shield: Enabled on the docs (S3 main) origin in us-west-2 to improve cache hit ratio and reduce origin load. Registry and guides origins have their own CloudFront distributions and should configure Origin Shield in their respective repos.
Price Class: All (global distribution)
Custom Error Responses:
- 404 → 404.html
Geo Restrictions: None
Purpose: Rate limiting to protect CloudFront from bot/scraper abuse
Toggle: enableWaf stack config (boolean, default false)
Rate limit: wafRateLimit stack config (integer, default 500). Maximum requests per 5-minute window per IP before WAF blocks the IP. Must be at least 100 (AWS minimum).
Region: us-east-1 (required for CloudFront-scoped WebACLs)
CloudWatch metrics:
cdn-waf- overall WebACL metricscdn-waf-rate-limit- rate-based rule metrics
Stack export: wafWebAclArn - ARN of the WAF WebACL (undefined when WAF is disabled)
1. Edge Redirects
Event Type: origin-request
Purpose: Handle cross-origin redirects at the edge
Use Cases:
- Redirect legacy URLs
- Handle cross-repository navigation
- Apply custom routing logic
Configuration:
const edgeRedirects = new aws.lambda.Function("edge-redirects", {
runtime: "nodejs22.x",
handler: "index.handler",
role: edgeRole.arn,
code: new pulumi.asset.AssetArchive({
"index.js": new pulumi.asset.StringAsset(redirectCode)
})
});2. AI Redirect and Gone
Event Type: origin-request
Purpose: Redirect /ai to /product/neo/ (301) and return 410 Gone for /ai/* subpaths
Applied To: /ai and /ai/* paths
Why: The /ai page has been replaced by /product/neo/; subpaths are permanently removed
dotnet-lowercase-uri
Event Type: viewer-request
Purpose: Normalize incoming URIs for .NET SDK docs by lowercasing the path, so that requests with mixed-case paths (e.g., /docs/reference/pkg/dotnet/Pulumi.Automation/) resolve to the lowercase S3 keys.
Applied To: /docs/reference/pkg/dotnet/* (ordered cache behavior)
Why: DocFX generates PascalCase filenames by default. The build post-processing step (scripts/run_docfx.sh) lowercases all output filenames and internal hrefs. The CloudFront Function ensures that any externally-linked URLs with the original casing still resolve correctly, without maintaining mixed-case files in S3.
Execution order: CloudFront Functions run at viewer-request before Lambda@Edge origin-request functions. The URI is lowercased before dotnetSDKRedirect in the Lambda@Edge redirect handler evaluates it, which is why that function's regex uses lowercase patterns.
Hosted Zone: From config (<www.pulumi.com> or <www.pulumi-test.io>)
A Record:
new aws.route53.Record("root-record", {
zoneId: hostedZoneId,
name: websiteDomain,
type: "A",
aliases: [{
name: distribution.domainName,
zoneId: distribution.hostedZoneId,
evaluateTargetHealth: false
}]
});AAAA Record: IPv6 alias (same as A record)
The atomic deployment strategy ensures zero-downtime deployments with instant rollback capability.
How It Works:
-
Build Phase
./scripts/build-site.sh
- Compile assets
- Generate HTML
- Optimize CSS
- Create search index
-
Create New Bucket
./scripts/sync-and-test-bucket.sh update
- Generate unique bucket name with commit SHA
- Create S3 bucket
- Configure website hosting
- Enable public access
-
Sync Content
aws s3 sync public/ s3://{bucket}/ --delete- Upload all files
- Set content types
- Apply cache headers
-
Validate Deployment
# Check file count aws s3 ls s3://{bucket}/ --recursive | grep index.html | wc -l # Must meet minimum threshold (verify specific value in sync-and-test-bucket.sh)
-
Run Tests
./scripts/run-browser-tests.sh
- Cypress smoke tests
- Verify critical pages
-
Update Infrastructure
./scripts/run-pulumi.sh
- Pulumi reads
origin-bucket-metadata.json - Updates CloudFront origin to new bucket
- Applies infrastructure changes
- Pulumi reads
-
Apply Redirects
./scripts/make-s3-redirects.sh
- Generate S3 redirect rules
- Apply to bucket
-
Clean Up Old Buckets (Automated)
- Scheduled cleanup workflow removes buckets older than retention period
- Keeps recent buckets for potential rollback
Benefits:
- Zero Downtime: CloudFront continues serving old bucket until new one is ready
- Instant Rollback: Revert CloudFront origin to previous bucket (< 1 minute)
- Validation: Test new deployment before switching traffic
- Immutable Deployments: Each deployment is a complete snapshot
- Debugging: Old buckets remain available for comparison
Rollback Procedures:
Choose the appropriate method based on the situation:
Method 1: Git Revert (Primary - Recommended)
Use this when the issue is caused by recent code/content/configuration changes (most common scenario).
Prerequisites: GitHub write access
Steps:
# Find the problematic commit
git log --oneline -10
# Revert it
git revert {commit-sha}
# Push to master
git push origin masterThe push automatically triggers build and deployment of the reverted code (~10-15 minutes).
When to use: Bad code changes, configuration errors, content issues Pros: Simple, automatic, preserves git history, minimal access needed Cons: Takes full build time (~10-15 min)
Method 2: Pin to Previous Bucket (For Infrastructure Issues)
Use this for faster rollback when the problem isn't in code (e.g., infrastructure issue, broken integration).
Prerequisites: Access to Pulumi Cloud (pulumi/docs organization)
Steps:
-
Find the previous bucket name:
- Check GitHub Actions → Previous successful run → Artifacts →
origin-bucket-metadata.json - Bucket format:
www-production-pulumi-docs-origin-{git-sha}
- Check GitHub Actions → Previous successful run → Artifacts →
-
Update Pulumi stack config:
- Open https://app.pulumi.com/pulumi/docs/www-production
- Go to Settings → Configuration
- Set
originBucketNameOverrideto the previous bucket name - Save
-
Trigger deployment:
- Go to GitHub → Actions → "Build and deploy" workflow
- Click "Run workflow" → Select master branch → Run
CloudFront switches origins within 1-2 minutes (no rebuild required).
When to use: Infrastructure issues, external service problems, need fast rollback Pros: Very fast (~1-2 min), no rebuild needed Cons: Requires Pulumi Cloud access, doesn't fix code issues
Important: After the issue is resolved, clear the override to resume normal deployments:
# In Pulumi Cloud console, set:
originBucketNameOverride: ""Method 3: Local Pulumi Execution (Advanced)
For team members with complete local development environment.
Prerequisites:
- Pulumi CLI installed
PULUMI_ACCESS_TOKENenvironment variable set- AWS CLI configured with SSO/OIDC
- Repository cloned locally
Steps:
# From repository root
cd infrastructure
# Select the production stack
pulumi stack select www-production
# List available buckets
aws s3 ls | grep pulumi-docs-origin | sort
# Pin to previous bucket
pulumi config set originBucketNameOverride {previous-bucket-name}
# Deploy (updates CloudFront only, no rebuild)
pulumi up
# Later: Clear override to resume normal deployments
pulumi config set originBucketNameOverride ""
pulumi upRollback Time: 1-2 minutes for CloudFront origin switch
The docs site uses three complementary redirect strategies.
When to Use: Moving or renaming Hugo content files
How It Works:
Add aliases to frontmatter:
---
title: New Page Title
aliases:
- /old/path/to/page/
- /another/old/path/
---Hugo generates meta-refresh HTML pages at old paths:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="refresh" content="0; url=/new/path/">
<meta name="robots" content="noindex">
</head>
</html>Verification:
Use scripts in scripts/alias-verification/:
# After moving files
cd scripts/alias-verification
npm install
node verify-aliases.jsPros:
- Automatic (Hugo handles it)
- Preserves SEO (meta-refresh + robots tag)
- No infrastructure changes needed
Cons:
- Not true 301 redirects (but search engines understand meta-refresh)
- Requires page to exist in Hugo content
When to Use: Redirecting non-Hugo content (generated docs, CLI references)
How It Works:
Add redirect rules to text files in scripts/redirects/:
# scripts/redirects/neo-redirects.txt
source-path|destination-url
docs/old/path/index.html|/docs/new/path/
Format: source-path|destination-url
Script: scripts/make-s3-redirects.sh
Process:
- Read redirect files from
scripts/redirects/*.txt - Convert to S3 redirect rules
- Apply to S3 bucket via website configuration
S3 Redirect Rule:
<RoutingRule>
<Condition>
<KeyPrefixEquals>docs/old/path/index.html</KeyPrefixEquals>
</Condition>
<Redirect>
<HostName>www.pulumi.com</HostName>
<ReplaceKeyWith>docs/new/path/</ReplaceKeyWith>
<HttpRedirectCode>301</HttpRedirectCode>
</Redirect>
</RoutingRule>Pros:
- True 301 redirects
- Works for any path
- No Hugo involvement
Cons:
- Must be manually added to redirect files
- Deployed per bucket (atomic deployment limitation)
When to Use: Redirecting between different origins (docs → registry, etc.)
How It Works:
Lambda function intercepts requests at CloudFront edge:
exports.handler = async (event) => {
const request = event.Records[0].cf.request;
const uri = request.uri;
// Redirect /ai or /ai/ to /product/neo/.
if (uri === '/ai' || uri === '/ai/') {
return {
status: '301',
statusDescription: 'Moved Permanently',
headers: {
location: [{ key: 'Location', value: '/product/neo/' }],
'cache-control': [{ key: 'Cache-Control', value: 'max-age=604800' }],
},
};
}
// All other /ai/* subpaths return 410 Gone.
return {
status: '410',
statusDescription: 'Gone',
headers: {
'cache-control': [{ key: 'Cache-Control', value: 'max-age=604800' }],
'content-type': [{ key: 'Content-Type', value: 'text/plain' }],
},
};
};Deployment: Managed in infrastructure/index.ts
Pros:
- Works across origins
- True 301 redirects
- Lowest latency (executes at edge)
Cons:
- Requires code deploy
- More complex to maintain
Is the file managed by Hugo?
├─ Yes → Use Hugo aliases
└─ No → Is it same origin?
├─ Yes → Use S3 redirects
└─ No → Use Lambda@Edge
Production CloudFront distribution applies security headers via response headers policy:
Headers Applied:
Strict-Transport-Security: max-age=31536000; includeSubDomainsX-Frame-Options: DENYContent-Security-Policy: frame-ancestors 'self' *.learnworlds.com academy.pulumi.comX-XSS-Protection: 1; mode=blockX-Content-Type-Options: nosniffReferrer-Policy: strict-origin-when-cross-origin
A CloudFront Function (markdown-negotiation) enables content negotiation for documentation pages:
- Trigger: Viewer-request stage on
/docs/*paths - Behavior: When a request includes
Accept: text/markdown, the function rewrites the URI from/docs/path/index.htmlto/docs/path/index.md - Cache: The rewritten URI becomes the cache key, so HTML and markdown responses are cached separately
- Response headers: A
DocsResponseHeadersPolicyaddsVary: Acceptto docs responses, ensuring caches distinguish between HTML and markdown requests - TTL: Docs paths use a 5-minute default TTL
Infrastructure: infrastructure/cloudfrontFunctions.ts
Production (<www.pulumi.com>)
File: Pulumi.www-production.yaml
config:
aws:region: us-west-2
www.pulumi.com:addSecurityHeaders: "true"
www.pulumi.com:doEdgeRedirects: "true"
www.pulumi.com:websiteDomain: www.pulumi.com
www.pulumi.com:websiteLogsBucketName: www-prod.pulumi.com-website-logs
www.pulumi.com:hostedZone: www.pulumi.com
www.pulumi.com:registryStack: pulumi/registry/production
www.pulumi.com:guidesStack: pulumi/guides/production
www.pulumi.com:answersStack: pulumi/answers/production
www.pulumi.com:cdnLogDeliverySourceName: CreatedByCloudFront-E3PRSXO1BZJEEY
www.pulumi.com:enableWaf: "true"
www.pulumi.com:wafRateLimit: "500"
www.pulumi.com:enableDataWarehouseAccess: "true"
www.pulumi.com:certificateArn: arn:aws:acm:us-east-1:388588623842:certificate/...Testing (<www.pulumi-test.io>)
File: Pulumi.www-testing.yaml
config:
aws:region: us-west-2
www.pulumi.com:addSecurityHeaders: "true"
www.pulumi.com:doEdgeRedirects: "true"
www.pulumi.com:websiteDomain: www.pulumi-test.io
www.pulumi.com:websiteLogsBucketName: pulumi-test-io-website-logs
www.pulumi.com:hostedZone: www.pulumi-test.io
www.pulumi.com:registryStack: pulumi/registry/testing
www.pulumi.com:guidesStack: pulumi/guides/testing
www.pulumi.com:answersStack: pulumi/answers/testing
www.pulumi.com:certificateArn: arn:aws:acm:us-east-1:571684982431:certificate/...The repository employs multiple testing strategies to ensure quality and reliability.
Tool: Custom markdown linter
Configuration: Cascading configuration using .markdownlint-base.json at the root and optional .markdownlint.json files in subdirectories.
Rules Enforced:
- Heading levels increment by one
- No trailing spaces
- Proper list formatting
- Code block language specification
- No bare URLs (must use markdown links)
Usage:
make lintScript: scripts/lint/lint-markdown.js
Tool: markdownlint-cli2
The markdown output files (generated by the markdown output format) are linted separately from source markdown.
Configuration: .markdownlint-cli2-markdown-output.jsonc
Pipeline:
scripts/join-markdown-lines.js— Joins soft-wrapped paragraph lines into single lines while preserving code blocks, frontmatter, and block-level elementsmarkdownlint-cli2 --fix— Auto-fix common issuesmarkdownlint-cli2— Validate (fail on remaining issues)
Usage:
make lint-markdownTool: Prettier
Scope:
- Markdown files
- JavaScript/TypeScript
- JSON files
- YAML files
Usage:
# Check formatting
make lint
# Auto-fix
make formatConfiguration: .prettierrc.json
Ignore rules: .prettierignore, unioned with .gitignore. make lint,
make format, and make ensure invoke prettier through scripts/prettier.sh,
which concatenates the two lists into a temporary root-level ignore file and
passes it as --ignore-path. Prettier does not read .gitignore on its own,
and --ignore-path takes a single file on v2.x, so without this every runtime
artifact needs an entry in both lists -- and a missed one turns an untracked
scratch file into a make lint failure. A new generated or scratch file only
needs a .gitignore entry.
Version: Prettier v2.8.8
Why v2.x Instead of v3.x:
We intentionally use Prettier v2.x instead of the newer v3.x due to significant performance regressions:
- Prettier v2.x performance: ~4 seconds for full repository lint
- Prettier v3.x performance: ~28 seconds for full repository lint (6x slower)
- Root cause: Prettier v3.x performs 45% more filesystem operations for config resolution, checking 33 .editorconfig files across node_modules directories
Impact on Workflows:
- Local commits: Fast (1-2 seconds) thanks to
lint-stagedchecking only changed files - CI full lint: Fast (~16 seconds total) thanks to v2.x performance
- Git hooks: Pre-commit hooks run
lint-stagedinstead of fullmake lintfor speed
When to Upgrade:
We will consider upgrading to Prettier v3.x when:
- The Prettier team addresses the config resolution performance issues
- Full repository linting with v3.x approaches v2.x performance (<10 seconds)
- Benefits of v3.x features outweigh the performance cost
See Prettier's CLI Performance Deep Dive for details on the performance characteristics.
Remove trailing spaces from files:
sed -i '' 's/[[:space:]]*$//' file1.md file2.mdLocation: cypress/integration/
Tests:
- Smoke tests on deployed sites
- Critical path verification (homepage, docs, registry)
- Form submissions
- Search functionality
- Navigation menus
Execution:
# Local
npx cypress open
# CI
./scripts/run-browser-tests.shCI Integration:
Runs automatically in:
pull-request.yml(PR builds)build-and-deploy.yml(production deployments)
Artifacts:
Video recordings archived in GitHub Actions artifacts on failure.
Typical Duration: 3-5 minutes
After Pulumi updates complete, automated health checks validate the deployed site using curl-based tests.
Workflow: .github/workflows/post-deployment-health-check.yml
Implementation: Inline bash script using curl (no external dependencies or repository checkout required)
What it checks:
- Core pages (homepage, docs, registry)
- SDK documentation endpoints (Node.js, Python, .NET, Java)
- High-traffic documentation pages
- Lambda@Edge redirect functionality
When it runs:
- Automatically after
build-and-deploy.ymlortesting-build-and-deploy.ymlcompletes successfully - Can be manually triggered via GitHub Actions UI
- Can be scheduled (add
scheduletrigger to workflow)
On failure:
- Dedicated Slack notification sent to #docs-ops (production) or #docs-ops-test (testing)
- Notification includes deployment info, commit SHA, and link to logs
- Health check workflow marked as failed in GitHub Actions
- Deployment workflow remains marked as successful (separation of concerns)
# Test individual endpoint
curl -s -o /dev/null -w "%{http_code}\n" -L https://www.pulumi.com/docs
# Test redirect
curl -s -o /dev/null -w "%{http_code}|%{redirect_url}\n" https://www.pulumi.com/docs/intro/cloud-providers/aws/Edit .github/workflows/post-deployment-health-check.yml and add calls to:
check_endpointfunction for page availability checks (expects 200 status)check_redirectfunction for Lambda@Edge redirect tests (expects 301 with location match)
Purpose: Validate that all code examples are functional
Programs Tested: ~425 programs in /static/programs/
Languages:
- TypeScript
- Python
- Go
- C# (.NET)
- Java
- YAML
Cloud Providers:
- AWS
- Google Cloud
- Azure
- Kubernetes
- Digital Ocean
Test Script: scripts/programs/test.sh
Process:
-
For each program directory:
- Install dependencies
- Run
pulumi preview(verify no errors) - Check output for expected resources
-
Multi-cloud authentication:
- AWS via OIDC
- GCP via workload identity
- Azure via service principal
Usage:
# Test all programs
make test
# Test specific program
ONLY_TEST="aws-s3-bucket-typescript" ./scripts/programs/test.shCI Execution:
- scheduled-test.yml: Daily at 8 AM UTC (full test suite)
- pull-request.yml: On PRs (quick validation with limited scope)
Typical Duration (based on historical runs): 2-2.5 hours (scheduled runs with full test suite), 3-5 minutes (PR runs with limited scope)
Note: The repository contains ~425 program directories.
Purpose: Prevent broken links in documentation
Script: scripts/link-checker/check-links.sh
Process:
- Crawl production site (<www.pulumi.com>)
- Check all links (internal and external)
- Report:
- 404 Not Found
- Timeout
- Invalid SSL
Types of Links Checked:
- Internal documentation links
- External reference links
- Images and assets
- Anchors (# fragments)
Usage:
make check_linksCI Execution: Daily at 3 PM UTC via check-links.yml. In CI the results
are enriched with real-404 server-log hits from the reader-signals export
(scripts/link-checker/merge-404-signal.py) before triage, so the highest
reader-impact breakage is fixed first.
Output: Report posted to Slack
Purpose: Ensure search index integrity
Script: scripts/search/check-urls.sh
Process:
- Query Algolia search index
- Extract all indexed URLs
- Verify each URL is accessible (HTTP 200)
- Report broken URLs
Why It Matters: Prevents search results from linking to 404 pages, which hurts user experience and SEO.
Usage:
make check_search_urlsCI Execution: Daily at 3 PM UTC via check-search-urls.yml
Tool: Lighthouse CI
Pages Monitored:
- Homepage (<www.pulumi.com>)
- Product page (/product/)
- Pricing (/pricing/)
- Get Started (/docs/get-started/)
- Documentation concepts (/docs/intro/concepts/resources/)
- Registry (/registry/)
- Registry package (/registry/packages/aws/api-docs/s3/bucket/)
Metrics:
- Performance: Page load speed, time to interactive
- Accessibility: WCAG compliance, ARIA labels
- Best Practices: HTTPS, console errors, security
- SEO: Meta tags, structured data, indexability
Thresholds:
Configurable in Lighthouse CI configuration. Typical thresholds:
- Performance: > 90
- Accessibility: > 95
- Best Practices: > 95
- SEO: > 95
Usage:
# Local
npm install -g @lhci/cli
lhci autorunCI Execution: Daily at 3 PM UTC via check-lighthouse.yml
Output: Lighthouse reports with scores and recommendations
The Pulumi docs infrastructure operates across multiple environments for different purposes.
Domain: <www.pulumi.com>
AWS Account: 388588623842
Region: us-west-2
Pulumi Stack: www-production
CloudFront Distribution: E3PRSXO1BZJEEY
Deployment Triggers:
- Push to
masterbranch - Scheduled: Daily at 6 AM Eastern (7 AM during DST), noon Pacific (1 PM during DST)
- Manual via workflow_dispatch
S3 Bucket Pattern:
www-production-pulumi-docs-origin-{git-sha}
Search Index: Production Algolia index (appId: OCCYMHQD)
Logs: CloudWatch Logs, S3 logs bucket
Access:
- Developers: Via Pulumi organization
- CI/CD: Via OIDC role
arn:aws:iam::388588623842:role/ContinuousDelivery
Domain: <www.pulumi-test.io>
AWS Account: 571684982431
Region: us-west-2
Pulumi Stack: www-testing
Purpose:
- Validate infrastructure changes before production
- Parallel testing environment
- PR preview deployments
Deployment Triggers:
- Push to
masterbranch - Manual via workflow_dispatch
S3 Bucket Pattern:
www-testing-pulumi-docs-origin-{identifier}
Differences from Production:
- Separate AWS account
- Separate CloudFront distribution
- Separate search indices
- Less restrictive security policies (for testing)
- Slack alerts go to
docs-opschannel (same as production)
Purpose: Per-PR preview sites for reviewing changes
AWS Account: 571684982431 (Testing)
URL Pattern:
http://www-testing-pulumi-docs-origin-pr-{PR_NUMBER}-{SHA}.s3-website.us-west-2.amazonaws.com
Lifecycle:
- Created: On first PR commit
- Updated: On subsequent commits
- Deleted: When PR is closed
Characteristics:
- Direct S3 website hosting (no CloudFront)
- Ephemeral (deleted after PR closes)
- No custom domain
- Limited search indexing
Bucket Naming:
www-testing-pulumi-docs-origin-pr-123-abc1234
Cleanup: Automated via pr-closed.yml workflow
Purpose: Personal development and testing stacks
Who Can Use: Pulumi organization members
Naming: dev-{username}
Usage:
# Create dev stack
pulumi stack init dev-myname
# Deploy
pulumi up
# Destroy
pulumi destroyBenefits:
- Test infrastructure changes without affecting production or testing
- Personal sandbox environment
- Full CloudFront distribution (not just S3)
Costs: Developer is responsible for cleaning up
Critical environment variables used across all environments:
| Variable | Purpose | Example |
|---|---|---|
ASSET_BUNDLE_ID |
Asset versioning | abc1234 or pr-123-abc1234 |
CSS_BUNDLE_ID |
CSS versioning | Same as ASSET_BUNDLE_ID |
REL_CSS_BUNDLE |
CSS path for Hugo | /css/styles.abc1234.css |
REL_JS_BUNDLE |
JS path for Hugo | /js/bundle.min.abc1234.js |
HUGO_BASEURL |
Site base URL | https://www.pulumi.com/ |
DEPLOYMENT_ENVIRONMENT |
Environment name | production or testing |
NODE_OPTIONS |
Node.js memory | --max_old_space_size=8192 |
| Variable | Purpose | Source |
|---|---|---|
AWS_REGION |
AWS region | Pulumi config |
AWS_ACCESS_KEY_ID |
AWS credentials | OIDC (temporary) |
AWS_SECRET_ACCESS_KEY |
AWS credentials | OIDC (temporary) |
AWS_SESSION_TOKEN |
AWS credentials | OIDC (temporary) |
CDN_PULUMI_URN |
CloudFront URN | Pulumi stack output |
| Variable | Purpose | Source |
|---|---|---|
PULUMI_ACCESS_TOKEN |
Pulumi API access | Pulumi ESC |
PULUMI_STACK_NAME |
Current stack | Workflow config |
PULUMI_CONFIG_PASSPHRASE |
Stack encryption | Not used (ESC) |
| Variable | Purpose | Source |
|---|---|---|
ALGOLIA_APP_ID |
Search app ID | Pulumi ESC |
ALGOLIA_APP_ADMIN_KEY |
Search admin key | Pulumi ESC |
SLACK_WEBHOOK_URL |
Notifications | Pulumi ESC |
GITHUB_TOKEN |
GitHub API | GitHub Actions |
| Variable | Purpose | Values |
|---|---|---|
NOBUILD |
Skip repo rebuilds | 1 or unset |
ONLY_TEST |
Test single program | Program name |
What is Pulumi ESC?
Pulumi ESC (Environments, Secrets, and Configuration) is a centralized secrets and config management service.
Why Use It?
- No static credentials in GitHub
- OIDC authentication for AWS
- Dynamic credential generation
- Audit logging
- Centralized secret rotation
GitHub Actions Integration:
- uses: pulumi/esc-action@v1
with:
organization: pulumi
environment: github-secrets/pulumi-docs
env:
PULUMI_ACCESS_TOKEN: ${{ secrets.PULUMI_ACCESS_TOKEN }}What It Provides:
All secrets and config for:
- AWS credentials (via OIDC)
- Pulumi tokens
- Algolia keys
- Slack webhooks
- GCP credentials
- Azure credentials
No Hardcoded Secrets: All sensitive values retrieved dynamically at runtime.
Common issues and their solutions.
Symptom: Build fails with Hugo errors
Solution:
# Check Hugo version
hugo version
# Should be: hugo v0.157.0
# Update if different
# macOS:
brew upgrade hugo
# Verify in workflows that Hugo version matches
grep "hugo-version" .github/workflows/*.ymlNote: Hugo version must match exactly across all workflows. Version mismatches cause template compatibility issues.
Symptom: Build fails with "JavaScript heap out of memory"
Solution:
# Increase Node.js memory
export NODE_OPTIONS="--max_old_space_size=8192"
# Or in package.json scripts:
"build": "NODE_OPTIONS='--max_old_space_size=8192' webpack"Symptom: make ensure fails with peer dependency errors
Solution:
TypeDoc peer dependency warnings are expected and safe to ignore:
npm WARN ERESOLVE overriding peer dependency
npm WARN While resolving: typedoc-plugin-script-inject@2.0.0
Real errors look different (missing packages, network timeouts). If you see those:
# Clear cache and retry
make clean
yarn cache clean
make ensureSymptom: Webpack build fails
Solution:
# Check theme dependencies
cd theme
yarn install
# Rebuild assets
yarn run build
# Check for TypeScript errors
yarn run tsc --noEmitSymptom: Build fails with "undefined environment variable"
Solution:
Check that required variables are set:
# For local builds
export ASSET_BUNDLE_ID=$(git rev-parse --short HEAD)
export CSS_BUNDLE_ID=$ASSET_BUNDLE_ID
# For CI/CD, check Pulumi ESC configuration
pulumi config -s www-productionSymptom: "Bucket already exists" or permission errors
Cause: Bucket name collision or insufficient permissions
Solution:
# Check if bucket exists
aws s3 ls | grep pulumi-docs-origin
# If exists from failed deployment, delete it
aws s3 rb s3://{bucket-name} --force
# Check AWS credentials
aws sts get-caller-identity
# Should show role: ContinuousDeliverySymptom: Changes not visible on production site
Cause: CloudFront cache serving old content
Solution:
# Create cache invalidation
aws cloudfront create-invalidation \
--distribution-id E3PRSXO1BZJEEY \
--paths "/*"
# Check invalidation status
aws cloudfront get-invalidation \
--distribution-id E3PRSXO1BZJEEY \
--id {invalidation-id}Note: Invalidations take 5-15 minutes to propagate globally.
Symptom: Edge functions not executing after deployment
Cause: Lambda@Edge replication to edge locations takes time
Timeline:
- Code update: Immediate
- Replication to edges: 5-30 minutes
- Full propagation: Up to 1 hour
Verification:
# Check function version
aws lambda get-function \
--function-name edge-redirects \
--region us-east-1
# Lambda@Edge must be in us-east-1Solution: Wait for propagation or test from different edge location.
Symptom: "Another update is currently in progress"
Cause: Concurrent Pulumi operations or orphaned locks
Solution:
# Check for in-progress operations
pulumi stack -s www-production
# Cancel orphaned updates (if safe)
pulumi cancel -s www-production
# If persistent, check Pulumi service console
# https://app.pulumi.com/pulumi/docs/www-productionSymptom: Search returns old results
Cause: Algolia indexing failed or timed out
Solution:
# Manual index update
make ci_update_search_index
# Check Algolia dashboard
# https://www.algolia.com/apps/OCCYMHQD/
# Verify index settings
node scripts/search/check-settings.jsSymptom: make test fails for specific programs
Debug:
# Run single test with verbose output
ONLY_TEST="aws-s3-bucket-typescript" \
PULUMI_VERBOSE_LOGGING=true \
./scripts/programs/test.sh
# Check for common issues:
# - Missing cloud credentials
# - Outdated dependencies
# - API changes
# Update dependencies
cd static/programs/aws-s3-bucket-typescript
npm updateSymptom: Browser tests fail in CI but pass locally
Common Causes:
- Timing issues (page not loaded)
- Environment differences (URLs, auth)
- Flaky tests (random failures)
Debug:
# View video artifacts in GitHub Actions
# Artifacts > browser-test-videos.zip
# Run locally with same baseURL
CYPRESS_BASE_URL=http://bucket.s3-website.amazonaws.com \
npx cypress runFix:
// Add explicit waits
cy.get('.element').should('be.visible');
cy.wait('@apiCall');
// Increase timeout
cy.get('.element', { timeout: 10000 });Symptom: make check_links reports broken links
Triage:
-
Internal links: Usually due to moved/deleted pages
- Check if file was moved → add alias
- Check if intentionally deleted → update linking pages
-
External links: May be temporary outages
- Verify in browser
- Check if domain changed
- Consider link rot (archive.org)
-
Anchors: Fragment not found
- Verify heading exists
- Check for typos in anchor
- Hugo auto-generates slugs (spaces → hyphens, lowercase)
Fix:
# Find pages linking to broken URL
grep -r "broken-url" content/
# Update or remove linksSymptom: Performance score drops below threshold
Common Causes:
- Large images not optimized
- JavaScript bundle size increased
- Render-blocking resources
- Third-party scripts
Debug:
# Run Lighthouse locally
npx @lhci/cli@latest autorun --url=https://www.pulumi.com
# Analyze bundle size
cd theme
yarn run webpack-bundle-analyzer
# Check image sizes
find static/images -type f -size +500kFix:
- Optimize images (compress, WebP format)
- Code split large bundles
- Lazy load images
- Defer non-critical JavaScript
# Verify site is accessible
curl -I https://www.pulumi.com
# Check specific page
curl -I https://www.pulumi.com/docs/
# Check S3 bucket
aws s3 ls s3://www-production-pulumi-docs-origin-abc1234/
# Test S3 website directly
curl -I http://www-production-pulumi-docs-origin-abc1234.s3-website.us-west-2.amazonaws.com# Get distribution details
aws cloudfront get-distribution --id E3PRSXO1BZJEEY
# List cache behaviors
aws cloudfront get-distribution-config --id E3PRSXO1BZJEEY \
| jq '.DistributionConfig.CacheBehaviors'
# Check origin configuration
aws cloudfront get-distribution-config --id E3PRSXO1BZJEEY \
| jq '.DistributionConfig.Origins'# View stack outputs
pulumi stack output -s www-production
# View configuration
pulumi config -s www-production
# View recent updates
pulumi stack history -s www-production
# View resources
pulumi stack -s www-productionGitHub Actions Logs:
- Navigate to Actions tab
- Select workflow run
- View job logs
CloudWatch Logs:
# Lambda@Edge logs are in region where function executed
# Check multiple regions
aws logs tail /aws/lambda/us-east-1.edge-redirects --followS3 Access Logs:
# Download logs
aws s3 sync s3://www-prod.pulumi.com-website-logs/ ./logs/
# Analyze with AWS Athena (if configured)When reviewing infrastructure changes (infrastructure/, package.json, webpack config, Lambda@Edge, CloudFront), identify potential risks that require human attention. This section provides guidance on common issues to flag during review.
Lambda@Edge Bundling Issues:
Lambda@Edge failures are often caused by bundling problems that aren't caught until runtime:
- ESM/CommonJS incompatibility: ESM-only packages (e.g.,
url-pattern>=7.0.0) break if webpack is misconfigured - Webpack config changes: Changes to
output.moduleorexperiments.outputModulecan break bundling - Dynamic imports:
import()statements may not work in Lambda@Edge runtime - Bundle size: Lambda@Edge has strict limits (1MB compressed, 50MB uncompressed)
What to flag in reviews:
- Dependency updates that affect webpack, babel, or bundlers (especially major versions)
- Changes to webpack configuration files
- New dependencies in
package.jsonused by Lambda@Edge code - Changes to
infrastructure/index.ts(Lambda@Edge function code)
CloudFront Distribution Changes:
- Redirect logic: Changes to redirect handling may break existing URLs
- Cache behavior: Modified cache settings require invalidation
- Lambda associations: Changes to CloudFront-Lambda event types must be coordinated
Deployment Risks:
- High risk (affects all users immediately): Lambda@Edge, CloudFront, WAF, DNS changes
- Medium risk (affects next deployment): Build system, dependency updates
- Low risk (limited scope): Documentation, scripts
Dependency Updates:
Large batches of dependency updates (especially Dependabot PRs with 20+ updates) increase risk:
- Flag webpack/bundler updates for testing against Lambda@Edge bundling
- Flag major version bumps for changelog review
- Suggest splitting build tool updates into separate PRs
For human reviewers to verify:
- Changes tested on pulumi-test.io staging environment
- Lambda@Edge execution tested manually (not just deployment)
- Critical pages return expected status codes (not 503/500)
- CloudWatch logs checked for Lambda@Edge errors (logs appear in edge regions)
- Lambda@Edge limits: https://docs.aws.amazon.com/AmazonCloudFront/latest/DeveloperGuide/lambda-requirements-limits.html
- Lambda@Edge code:
infrastructure/index.ts - CloudWatch logs: Edge regions (not us-east-1)
Regular maintenance tasks to keep the infrastructure healthy and cost-effective.
Workflows:
bucket-cleanup.yml(production)bucket-cleanup-testing.yml(testing)
Schedule: Daily at 3:00 PM UTC
Process:
- List all origin buckets
- Identify buckets older than retention period (typically 7 days)
- Delete old buckets and contents
- Clean up metadata in AWS Parameter Store
- Report cleanup results
Retention Policy:
Retains 10 buckets beyond the currently deployed bucket (count-based, not time-based).
Configured in scripts/list-recent-buckets.sh:
buckets_to_retain=10Why It Matters: Old buckets accumulate quickly (1 per deployment), costing money and cluttering AWS console.
List recent buckets:
./scripts/list-recent-buckets.sh
# Output:
# www-production-pulumi-docs-origin-abc1234 (2 days old)
# www-production-pulumi-docs-origin-def5678 (5 days old)
# www-production-pulumi-docs-origin-ghi9012 (10 days old) ← Cleanup candidateDelete specific bucket:
# Delete bucket and contents
aws s3 rb s3://bucket-name --force
# Verify deletion
aws s3 ls | grep bucket-nameBulk cleanup:
# Delete all buckets older than 30 days
./scripts/ci-bucket-cleanup.sh --days 30Locations:
- Root
package.json theme/package.jsontheme/stencil/package.jsoninfrastructure/package.json
Update Process:
# Check for outdated packages
yarn outdated
# Update all dependencies
yarn upgrade
# Update specific package
yarn upgrade package-name
# Run tests
make test
# Commit changes
git add package.json yarn.lock
git commit -m "Update Node.js dependencies"Automation: Dependabot creates PRs for dependency updates weekly.
Warning: Hugo updates require changes in multiple locations.
Files to Update:
.github/workflows/build-and-deploy.yml.github/workflows/testing-build-and-deploy.yml.github/workflows/pull-request.yml.github/workflows/pulumi-cli.yml.github/workflows/scheduled-test.ymlscripts/ensure.sh- Dev container configuration (if applicable)
Process:
# Update Hugo version in all workflow files
find .github/workflows -name "*.yml" -exec sed -i 's/hugo-version: 0.157.0/hugo-version: 0.155.0/g' {} +
# Update ensure.sh
sed -i 's/0.157.0/0.155.0/g' scripts/ensure.sh
# Test locally
make clean
make ensure
make build
# Test in PR before mergingTypeDoc:
# Check current version
npm list typedoc
# Update
yarn add --dev typedoc@latest typedoc-plugin-script-inject@latest
# Regenerate docs locally to verify
NOBUILD=true PKGS=pulumi ./scripts/run_typedoc.shSphinx:
# Update in Python requirements
# (typically handled in scripts/generate_python_docs.sh)
# Test generation
./scripts/generate_python_docs.shDependabot automatically updates GitHub Actions versions. Review and merge Dependabot PRs regularly.
Current Action Versions (as of January 2026):
| Action | Version | Purpose |
|---|---|---|
actions/checkout |
v4 | Repository checkout |
actions/setup-node |
v6 | Node.js environment setup |
actions/setup-go |
v5 | Go environment setup |
actions/setup-python |
v5 | Python environment setup |
actions/setup-dotnet |
v4 | .NET environment setup |
actions/setup-java |
v3 | Java environment setup |
actions/upload-artifact |
v4 | Artifact upload |
actions/create-github-app-token |
v2 | GitHub App token generation |
aws-actions/configure-aws-credentials |
v4 | AWS credential configuration |
google-github-actions/auth |
v2 | Google Cloud authentication |
peaceiris/actions-hugo |
v2 | Hugo installation |
pulumi/actions |
v4 | Pulumi CLI installation |
pulumi/esc-action |
v1 | Pulumi ESC integration |
pulumi/action-install-pulumi-cli |
v1.0.1 | Legacy action used in the CLI release workflow; pulumi/actions should be used everywhere else |
jaxxstorm/action-install-gh-release |
v2.1.0 | Install tools from GitHub releases |
treosh/lighthouse-ci-action |
v12 | Lighthouse CI integration |
hmarr/auto-approve-action |
v4 | Automated PR approval |
repo-sync/pull-request |
v2 | Pull request creation |
helm/kind-action |
v1 | Kubernetes KinD cluster setup |
Recent Major Version Updates:
- setup-node v4 → v6: Updated Node.js setup action with improved caching and performance
- jaxxstorm/action-install-gh-release v1 → v2: Enhanced GitHub release installation with better error handling
- create-github-app-token v1 → v2: Updated GitHub App token generation with security improvements
Example Update:
# Before
- uses: actions/setup-node@v4
# After (Dependabot PR)
- uses: actions/setup-node@v6Google generally caches robots.txt for up to 24 hours, after which its crawlers pick up any changes automatically (it may occasionally cache longer when refreshing isn't possible). If you need the cache refreshed sooner (for example, after a significant crawling rule change), you can request an immediate refresh through Google Search Console.
Note: Access to the Pulumi Google Search Console property is required. If you don't have access, contact a member of the docs team who does.
Steps:
- Open Google Search Console and select the pulumi.com property.
- In the left navigation, go to Indexing > robots.txt.
- In the robots.txt report, click Request a recrawl.
- Confirm the request. Google will refresh its cached copy within a few hours instead of waiting for the cache to expire.
For reference, see Google's documentation on submitting an updated robots.txt.
This section provides comprehensive guidance for triaging and managing Dependabot pull requests in this repository.
Schedule: Monthly updates (first Monday at 09:00 UTC)
Ecosystems:
- npm (root, theme, stencil, infrastructure)
- GitHub Actions
- pip (Python dependencies)
Grouping Strategy: Ultra-aggressive single catch-all group per ecosystem
- Root:
all-dependenciesgroup captures all npm packages - Theme:
all-dependenciesgroup captures all theme packages - Stencil:
all-dependenciesgroup captures all stencil packages - Infrastructure:
all-dependenciesgroup captures all infrastructure packages - GitHub Actions:
all-actionsgroup captures all action updates
Expected Volume: 5 grouped PRs per month + security patches as needed
PR Limits: 1 PR per ecosystem (prevents flooding)
Major Version Updates: Blocked for non-security updates via wildcard ignore rules
Security Updates: Arrive immediately regardless of schedule (Dependabot auto-override)
All Dependabot PRs automatically receive:
Dependabot-applied labels:
dependencies- Standard label applied by Dependabot
Auto-applied labels (via label-dependabot.yml workflow):
deps-security-patch- Security update; evaluate and merge promptlydeps-lambda-edge-risk- Webpack/bundler/AWS SDK updates (see Infrastructure Change Review)deps-bulk-update- 10+ dependencies in single PR
The workflow does not classify PRs into risk tiers. Dependency updates are
grouped per ecosystem and arrive at a low, predictable volume, so the policy is
simply to evaluate each PR and merge it once CI is green (see below). The two
flags above surface the only signals that change handling: security patches get
priority, and deps-lambda-edge-risk PRs need the bundle-size check.
On the first Monday of each month, Dependabot generates roughly 5 grouped PRs (one per ecosystem), plus security patches as they arise. The policy is to evaluate each PR and merge it as it comes in — there is no risk tiering and no quarterly deferral. Grouping already keeps volume low, so batching buys nothing.
For each PR:
- Build and spot-check. Run
make build(ormake serve-allwhen the PR touches browser-facing packages such as search, markdown rendering, or web components) and confirm the site builds and loads. Spot-check search, console errors, and markdown rendering. - Let CI gate it. The PR's build/lint/Cypress checks (
pull-request.yml) are the merge gate. Merge once they're green. - Prioritize security patches. PRs labeled
deps-security-patcharrive off-schedule — evaluate and merge them promptly rather than waiting for the monthly batch. - Give Lambda@Edge updates the bundle check. For PRs labeled
deps-lambda-edge-risk(webpack/bundler/AWS SDK), cross-reference the Infrastructure Change Review section, check the Lambda@Edge function size against the 1MB compressed limit, and verify the CloudFront deployment succeeds in the testing environment before merging.
Automated Claude review does not run on Dependabot PRs (the review pipeline is built for prose, not dependency bumps). To get a Claude pass on a specific PR, run /pr-review <number>.
Expected Monthly Time: 5-10 minutes for triage + extra time only for Lambda@Edge or bulk PRs that warrant deeper testing.
Override Rule: Security patches bypass all other processes
Arrival: Immediately when vulnerability discovered (ignores monthly schedule)
Labels: Auto-labeled with deps-security-patch
Workflow:
- Dependabot opens PR immediately (any time of month)
- Auto-labeling workflow adds
deps-security-patch - Build and spot-check (
make build, ormake serve-allfor browser-facing packages) - Merge within 24 hours once CI is green
- Deploy to production immediately
Example: CVE in marked
- PR arrives immediately
- Labels:
deps-security-patch(anddeps-lambda-edge-riskif a bundler/AWS SDK update) - Build and spot-check search/markdown rendering
- Merge and deploy within 24 hours
Label: deps-bulk-update
Risk: Higher chance of conflicts or breaking changes
Process:
- Review PR carefully—don't rely solely on automated labels
- Check for major version updates within the bulk (may be hidden)
- Test more thoroughly than single-dependency updates
- Consider splitting into smaller batches if failures occur
Testing:
- Full test suite:
make test && make lint - Local build:
make serve-all - Visual regression testing on key pages
- Extended soak testing (leave
make serve-allrunning for 30 minutes)
Lambda@Edge Deployment Risks:
- See Infrastructure Change Review section
- Webpack, bundlers, and AWS SDK updates affect Lambda@Edge function size
- 1MB compressed size limit—test after bundler updates
Infrastructure Changes:
- Pulumi infrastructure updates (
@pulumi/*,@aws-sdk/*) - See Infrastructure Change Review for deployment process
Prettier v3.x:
- Ignored in root and theme
dependabot.yml - Reason: Performance regression (5-10x slower than v2.x)
- Revisit: When performance regression is fixed upstream
Tailwindcss Major Versions:
- Ignored in root and theme
dependabot.yml - Currently on v4.x (migrated from v2)
- Reason: Breaking changes require manual migration
- Revisit: During planned design system updates
pulumi/action-install-pulumi-cli:
- Ignored in
dependabot.ymlGitHub Actions section - Reason: v2+ has circular dependency on
versions.jsonwhich the CLI release workflow creates - Current version: v1.0.1 (downloads directly from GitHub releases)
- Usage:
.github/workflows/pulumi-cli.ymlonly;pulumi/actionsshould be used elsewhere - Revisit: When action is fixed to support direct GitHub release downloads without
versions.json
Major Versions (Wildcard):
- Ignored across all ecosystems via wildcard rule
- Reason: Breaking changes require manual review and testing
- Exception: Security patches override this rule
Settings File: scripts/search/settings.js
Configuration:
- Searchable attributes
- Ranking rules
- Custom ranking
- Facets
- Synonyms
Update Settings:
# Apply settings
node scripts/search/apply-settings.js
# Verify settings
node scripts/search/check-settings.jsManual Reindexing:
# Update all indices
make ci_update_search_index
# Update specific index
ALGOLIA_INDEX=pulumi node scripts/search/update-index.jsAutomated Updates:
- Hourly: Via
update-search-index.yml - On Deploy: Via
ci-push.shandci-pull-request.sh
Priority order for search results:
- Exact match
- Page title match
- Heading match
- Content match
- Custom ranking:
- Documentation > Blog > Registry
- Newer content ranked higher
Modify Ranking:
Edit scripts/search/settings.js:
ranking: [
'typo',
'geo',
'words',
'filters',
'proximity',
'attribute',
'exact',
'custom'
],
customRanking: [
'desc(date)',
'desc(weight)'
]Check Current Size:
# Build site
make build
# Check CSS sizes
ls -lh public/css/
# Target: < 200KB per bundleOptimization Techniques:
-
Scope Tailwind's content scan
Tailwind v4 tree-shakes by default — it only emits CSS for classes it detects in scanned files. If the bundle is larger than expected, narrow the scan with explicit
@sourcedirectives intheme/src/scss/main.scss:/* Restrict scanning to only the directories that use Tailwind classes */ @source "../../layouts/**/*.html"; @source "../../content/**/*.md";
Avoid broad globs like
../../**that pull innode_modulesor generated files — these inflate the detected class list and slow builds. -
Code Splitting
- Separate critical CSS
- Load non-critical CSS async
Analyze Bundle:
cd theme
npx webpack-bundle-analyzer dist/stats.jsonOptimization Techniques:
-
Code Splitting
// Dynamic imports const module = await import('./heavy-module');
-
Tree Shaking
- Use ES modules
- Avoid default exports
-
Lazy Loading
// Load on interaction button.addEventListener('click', async () => { const module = await import('./feature'); });
Check Large Images:
find static -type f \( -name "*.png" -o -name "*.jpg" \) -size +500k
# Optimize with ImageOptim, TinyPNG, or similarBest Practices:
- Use WebP format with fallback
- Provide multiple sizes (srcset)
- Lazy load images below fold
- Use SVG for icons and logos
Cache Headers:
# Long cache for fingerprinted / immutable assets (1 year)
/css/bundle.*.css: 1 year
/js/bundle.*.js: 1 year
/js/search.*.js: 1 year
/js/chunk-*.js: 1 year
/js/consent-manager.*.js: 1 year
/js/algolia.*.js: 1 year
/js/homepage.*.js: 1 year
/js/marketing.*.js: 1 year
/fonts/*: 1 year
/fingerprinted/*: 1 year
# Short cache for HTML (invalidated on deploy)
Default (HTML): 10 minutes
/docs/*: 10 minutes
/registry/*: 30 minutes
/guides/*: 30 minutes
# No cache for build-specific files
/js/components*.js: 0
/metadata.json: 0Post-deploy invalidation:
After each deploy, CloudFront cache is automatically invalidated for HTML content paths
(see scripts/run-pulumi.sh). Fingerprinted assets are excluded since their URLs change
with content. This allows aggressive TTLs without stale content after deploys.
Optimization:
-
Increase TTL for static assets
- Fingerprinted assets can cache forever
- Use content-hash URLs for cache busting
-
Cache Patterns
Every cache behavior attaches a
CachePolicyviacachePolicyId. LegacyforwardedValues/minTtl/defaultTtl/maxTtlon the behavior is only used for the two CORS-forwarding paths (/uploads/*,/*/rss.xml) since cache policies can't forward arbitrary CORS request headers to S3 without inflating the cache key. CachePolicy is also required to serve Brotli — see Serving compressed files.Use the
cacheKeyPolicy(name, ttl)helper ininfrastructure/index.tsto create new cache policies; it enables Brotli + gzip by default.⚠️ Always use the helper. CloudFront's automatic compression silently fails —compress: truebecomes a no-op, responses ship uncompressed with no error or warning — if aCachePolicyis created withoutenableAcceptEncodingGzip/enableAcceptEncodingBrotli. The helper sets both. Hand-rolledaws.cloudfront.CachePolicyresources will compile, deploy, and pass review while serving 5–10× more bytes than they should. This is the bug that affected/registry/*,/guides/*, and/fingerprinted/logos/pkg/*before this refactor — it took several hours of investigation to find because every diagnostic signal pointed elsewhere. Verify withcurl -sD - -o /dev/null -H 'Accept-Encoding: gzip' <url>(a GET —curl -sIHEAD requests don't always reflect compression state for cache-policy paths).// infrastructure/index.ts const imagesCacheKeyPolicy = cacheKeyPolicy("images-cache", oneDay); orderedCacheBehaviors: [{ ...baseCacheBehavior, pathPattern: "/images/*", cachePolicyId: imagesCacheKeyPolicy.id, }]
-
Compression
- Brotli and gzip are enabled on every cache policy via
enableAcceptEncodingBrotli/enableAcceptEncodingGzip. CloudFront picks Brotli when the viewer sendsAccept-Encoding: br, and falls back to gzip otherwise. Brotli is roughly 15-24% smaller than gzip on text assets. - Uncompressed origin responses: CloudFront compresses at the edge for any
behavior with
compress: trueplus a cache policy that has the encoding flags set.
- Brotli and gzip are enabled on every cache policy via
Static assets (images, icons) used on the homepage and product pages can be fingerprinted for long-term caching. Hugo's fingerprint pipe appends a content hash to the filename, allowing a 1-year CloudFront TTL with Cache-Control: immutable headers.
How it works:
- Place source assets in
assets/fingerprinted/(e.g.,assets/fingerprinted/images/product/neo-tasks.png). - Use the
fingerprinted-img.htmlpartial in templates:{{ partial "fingerprinted-img.html" (dict "src" "images/product/neo-tasks.png" "alt" "Alt text") }} - Hugo hashes the file, converts non-SVG images to WebP, and outputs to
/fingerprinted/<hash>.webp. - CloudFront serves these with a 1-year TTL and immutable cache headers via the
/fingerprinted/*cache behavior.
Partial parameters: src (required), alt, class, style.
Important: meta_image frontmatter must point to a stable path in static/, not a fingerprinted asset, since social media crawlers need a predictable URL.
File: .github/dependabot.yml
version: 2
updates:
- package-ecosystem: npm
directory: "/"
schedule:
interval: weekly
open-pull-requests-limit: 10
- package-ecosystem: github-actions
directory: "/"
schedule:
interval: weeklyProcess:
- Dependabot creates PR with dependency update
- CI runs tests automatically
- Review changes
- Merge if tests pass
Pulumi ESC Secrets:
All secrets managed via Pulumi ESC with automatic rotation capabilities.
To Rotate:
- Update secret in Pulumi ESC console
- No code changes needed
- Next deployment uses new secret
Secrets to Rotate Regularly:
- Pulumi tokens
- AWS IAM roles (refresh OIDC trust)
- Algolia keys
- Slack webhooks
Check Role Permissions:
# Production role
aws iam get-role --role-name ContinuousDelivery --profile production
# Testing role
aws iam get-role --role-name ContinuousDelivery --profile testingPrinciple of Least Privilege:
Regularly review and remove unnecessary permissions:
{
"Effect": "Allow",
"Action": [
"s3:CreateBucket",
"s3:DeleteBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::www-*-pulumi-docs-origin-*"
}GitHub OIDC Configuration:
Trust relationship between GitHub and AWS allows temporary credentials:
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::388588623842:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:pulumi/docs:ref:refs/heads/master"
}
}
}]
}Benefits:
- No long-lived credentials
- Automatic expiration (2 hours)
- Audit trail in CloudTrail
- Scoped to specific workflows
Verify OIDC Config:
aws iam get-role --role-name ContinuousDelivery \
| jq '.Role.AssumeRolePolicyDocument'Complete reference of all build and deployment scripts.
| Script | Location | Purpose | Usage |
|---|---|---|---|
| build-site.sh | scripts/ | Main build orchestrator | ./scripts/build-site.sh [preview] |
| ensure.sh | scripts/ | Install dependencies | ./scripts/ensure.sh |
| serve.sh | scripts/ | Local dev server | ./scripts/serve.sh |
| clean.sh | scripts/ | Remove build artifacts | ./scripts/clean.sh |
| ci-push.sh | scripts/ | Production deployment | ./scripts/ci-push.sh |
| ci-pull-request.sh | scripts/ | PR deployment | ./scripts/ci-pull-request.sh |
| sync-and-test-bucket.sh | scripts/ | S3 sync and validation | ./scripts/sync-and-test-bucket.sh update |
| run-pulumi.sh | scripts/ | Pulumi deployment | ./scripts/run-pulumi.sh |
| make-s3-redirects.sh | scripts/ | Apply S3 redirects | ./scripts/make-s3-redirects.sh |
| generate-search-index.sh | scripts/ | Update search index | ./scripts/generate-search-index.sh |
| run_typedoc.sh | scripts/ | Generate Node.js docs | ./scripts/run_typedoc.sh |
| generate_python_docs.sh | scripts/ | Generate Python docs | ./scripts/generate_python_docs.sh |
| run-browser-tests.sh | scripts/ | Run Cypress tests | ./scripts/run-browser-tests.sh |
| check-links.sh | scripts/link-checker/ | Verify links | ./scripts/link-checker/check-links.sh |
| check-urls.sh | scripts/search/ | Validate search index | ./scripts/search/check-urls.sh |
| minify-css.js | scripts/ | Optimize CSS | node scripts/minify-css.js |
| generate-docs-content.js | scripts/content/ | Generate search data | node scripts/content/generate-docs-content.js |
| list-recent-buckets.sh | scripts/ | List S3 buckets | ./scripts/list-recent-buckets.sh |
| ci-bucket-cleanup.sh | scripts/ | Delete old buckets | ./scripts/ci-bucket-cleanup.sh --days 30 |
| common.sh | scripts/ | Shared utilities | Sourced by other scripts |
| Variable | Purpose | Example | Set By |
|---|---|---|---|
| ASSET_BUNDLE_ID | Asset versioning | abc1234 or pr-123-abc1234 |
build-site.sh |
| CSS_BUNDLE_ID | CSS versioning | Same as ASSET_BUNDLE_ID | build-site.sh |
| REL_CSS_BUNDLE | CSS path (Hugo) | /css/styles.abc1234.css |
build-site.sh |
| REL_JS_BUNDLE | JS path (Hugo) | /js/bundle.min.abc1234.js |
build-site.sh |
| HUGO_BASEURL | Site base URL | https://www.pulumi.com/ |
Workflow |
| DEPLOYMENT_ENVIRONMENT | Environment | production or testing |
Workflow |
| NODE_OPTIONS | Node.js memory | --max_old_space_size=8192 |
Workflow |
| AWS_REGION | AWS region | us-west-2 |
Pulumi config |
| CDN_PULUMI_URN | CloudFront URN | urn:pulumi:www-production::... |
Pulumi stack |
| PULUMI_ACCESS_TOKEN | Pulumi API | pul-... |
Pulumi ESC |
| PULUMI_STACK_NAME | Stack name | www-production |
Workflow |
| ALGOLIA_APP_ID | Search app | OCCYMHQD |
Pulumi ESC |
| ALGOLIA_APP_ADMIN_KEY | Search admin key | (secret) | Pulumi ESC |
| SLACK_WEBHOOK_URL | Notifications | (secret) | Pulumi ESC |
| GITHUB_TOKEN | GitHub API | (auto) | GitHub Actions |
| NOBUILD | Skip rebuilds | 1 |
User |
| ONLY_TEST | Test single program | aws-s3-typescript |
User |
| GOGC | Go GC tuning | 3 |
Workflow |
| Resource Type | Naming Pattern | Example |
|---|---|---|
| S3 Origin Bucket | www-{env}-pulumi-docs-origin-{id} |
www-production-pulumi-docs-origin-abc1234 |
| S3 Preview Bucket | www-testing-pulumi-docs-origin-pr-{num}-{sha} |
www-testing-pulumi-docs-origin-pr-123-abc1234 |
| S3 Logs Bucket | {domain}-website-logs |
www-prod.pulumi.com-website-logs |
| CloudFront Distribution | Manual (persistent) | E3PRSXO1BZJEEY |
| Lambda@Edge Function | edge-{purpose} |
edge-redirects |
| WAF WebACL | cdn-waf |
cdn-waf |
| IAM Role | ContinuousDelivery |
ContinuousDelivery |
| Pulumi Stack | www-{environment} |
www-production |
| Service | URL/Port | Purpose |
|---|---|---|
| Local Dev Server | http://localhost:1313 | Hugo development server |
| Static Server | http://localhost:8080 | Serve built public/ directory |
| Production | https://www.pulumi.com | Production site |
| Testing | https://www.pulumi-test.io | Testing environment |
| PR Preview | http://{bucket}.s3-website.{region}.amazonaws.com | PR preview sites |
| Pulumi Console | https://app.pulumi.com | Pulumi service |
| Algolia Dashboard | https://www.algolia.com/apps/OCCYMHQD | Search management |
| GitHub Actions | https://github.com/pulumi/docs/actions | CI/CD workflows |
| Document | Location | Purpose |
|---|---|---|
| README.md | / | Getting started guide |
| CONTRIBUTING.md | / | Contribution guidelines |
| BLOGGING.md | / | Blog post creation |
| STYLE-GUIDE.md | / | Content style guide |
| AGENTS.md | / | AI agent guidelines |
| CODE-EXAMPLES.md | / | Example program guide |
| SEO.md | / | Search optimization |
| SCHEMA.md | / | Data schemas |
| infrastructure/README.md | infrastructure/ | Infrastructure details |
| Pulumi Docs | https://www.pulumi.com/docs/ | Pulumi documentation |
| Hugo Docs | https://gohugo.io/documentation/ | Hugo reference |
| AWS CloudFront | https://docs.aws.amazon.com/cloudfront/ | CloudFront documentation |
| GitHub Actions | https://docs.github.com/actions | Workflow documentation |
| Algolia Docs | https://www.algolia.com/doc/ | Search documentation |
| Dependency | Version | Purpose |
|---|---|---|
| Node.js | 24.x | Runtime for build tools |
| Hugo | 0.157.0 | Static site generator |
| Yarn | 1.22.x | Package manager |
| Go | 1.26+ | Doc generation |
| Python | 3.13+ | Doc generation |
| Pulumi CLI | Latest | Infrastructure deployment |
| TypeDoc | 0.28.15 | Node.js doc generation |
| Sphinx | Latest | Python doc generation |
| Webpack | 5.x | Asset bundling |
| Tailwind CSS | 4.x | CSS framework (CSS-first config via @import "tailwindcss") |
| @tailwindcss/postcss | 4.x | PostCSS plugin for Tailwind v4 |
| PostCSS | 8.x | CSS processing |
| Cypress | Latest | Browser testing |
| AWS CLI | 2.x | AWS operations |
| GitHub CLI (gh) | Latest | GitHub operations |
The atomic deployment strategy makes rollbacks easy. Choose the method based on the situation:
Most Common: Git Revert (10-15 min)
git revert {problematic-commit-sha}
git push origin master
# Auto-triggers full rebuild and deploymentFaster: Pin to Previous Bucket (1-2 min)
For infrastructure issues or when git revert isn't suitable:
- Find previous bucket (check GitHub Actions artifacts:
origin-bucket-metadata.json) - Update config in Pulumi Cloud console:
- https://app.pulumi.com/pulumi/docs/www-production
- Settings → Configuration → Set
originBucketNameOverride
- Trigger "Build and deploy" workflow in GitHub Actions
CloudFront switches origins within 1-2 minutes (no rebuild).
See Rollback Procedures for detailed instructions on all methods.
PR preview environments persist until the PR is closed. They are automatically cleaned up by the pr-closed.yml workflow.
Yes! Use the testing environment or create a personal dev stack:
# Testing environment (shared)
# Push to master, manually trigger testing-build-and-deploy.yml
# Personal dev stack
cd infrastructure
pulumi stack init dev-myname
pulumi up- CloudFront continues serving the previous deployment
- No downtime occurs
- Slack alert sent to
docs-opschannel - Failed build artifacts retained for debugging
It depends on the scenario:
- Moved Hugo content: Add alias to frontmatter
- Generated content: Add to
scripts/redirects/*.txt - Cross-origin: Update Lambda@Edge function in
infrastructure/index.ts
See Redirect Management for details.
Each deployment creates a new bucket for atomic deployments. Old buckets are automatically cleaned up after 7 days by the bucket-cleanup.yml workflow.
Update in all workflow files and scripts/ensure.sh. See Hugo Version Updates.
Pulumi ESC (Environments, Secrets, and Config) manages all secrets and credentials. It provides:
- No static credentials in GitHub
- OIDC-based AWS authentication
- Centralized secret management
- Audit logging
Yes:
make clean
make ensure
# Optionally regenerate any SDK / CLI reference docs you want to inspect locally
# (see README's "Generating SDK and CLI documentation" section). Skip for an
# ordinary site build.
make build
make serve-staticThe site will be available at http://localhost:8080.
For Git Revert (Method 1 - Most Common):
- Write access to GitHub repository (to push reverts to master)
- That's it! No additional credentials needed
For Bucket Pinning (Method 2 - Fast Rollback):
- Access to Pulumi Cloud organization (
pulumi/docsstack) - Permission to trigger GitHub Actions workflows
For Local Execution (Method 3 - Advanced):
- All of the above, plus:
- Pulumi CLI installed locally
PULUMI_ACCESS_TOKENenvironment variable- AWS CLI configured (SSO/OIDC)
- Local development environment setup
Don't have access? Contact your team admin or use Method 1 (git revert) which requires minimal permissions.
Issue: "Cannot access Pulumi stack"
- Verify you're logged into Pulumi Cloud (https://app.pulumi.com)
- Check you have access to the
pulumiorganization - Verify stack name:
pulumi/docs/www-production - Contact team admin for access if needed
Issue: "AWS credentials not configured" (Local execution)
- Configure AWS CLI with SSO:
aws sso login --profile production - Or use Pulumi ESC: Credentials are automatically provided in GitHub Actions
- Check your AWS profile is set:
export AWS_PROFILE=production
Issue: "Bucket not found"
- Verify bucket name format:
www-production-pulumi-docs-origin-{git-sha-short} - Check bucket wasn't cleaned up (retention is 10 buckets beyond current)
- Older buckets may have been deleted by automated cleanup workflow
- Find bucket name in GitHub Actions artifacts:
origin-bucket-metadata.json
Issue: "Git revert creates conflicts"
-
If automatic revert fails, manually revert changes:
git revert {commit-sha} --no-commit # Resolve conflicts manually git add . git commit git push origin master
Issue: "Rollback didn't fix the problem"
- Verify you reverted/pinned to a known-good deployment
- Check GitHub Actions history for last successful deployment
- Issue might be in external service (check Algolia, CloudFront, Lambda@Edge)
- Consider rolling back further if problem persists
- Navigate to GitHub Actions tab
- Select the failed workflow run
- Expand failed job steps
- Download artifacts (browser-test-videos, bucket-metadata)
- Check Slack notifications for summary
For deployments, also check:
- AWS CloudWatch Logs
- S3 bucket contents
- CloudFront distribution status
- Pulumi stack history