Skip to content

Add versioned SDK & CLI docs#19800

Draft
CamSoper wants to merge 21 commits into
masterfrom
CamSoper/hackathon-versioned-docs
Draft

Add versioned SDK & CLI docs#19800
CamSoper wants to merge 21 commits into
masterfrom
CamSoper/hackathon-versioned-docs

Conversation

@CamSoper

Copy link
Copy Markdown
Contributor

Publishes immutable, per-release snapshots of each SDK and CLI docset to a permanent S3 bucket, served through the existing CloudFront distribution under /docs/versioned/{tool}/{version}/. The publish step is additive to the doc-generation workflows that already run on each release — historical versions never enter git, Hugo, or the per-deploy origin sync, so site build and deploy times are unaffected.

What's here

  • infrastructure/versioned-docs/ — permanent bucket (pulumi-docs-versioned-{env}, website hosting, versioning, public-read GetObject, protect) + a GitHub-OIDC publisher role scoped to repo:pulumi/docs.
  • Main stack — behind an optional versionedDocsStack config, adds one origin + a /docs/versioned/* behavior (ordered ahead of dotnet//docs/*) and a cache policy that honors each object's origin Cache-Control, plus a pass-through response-headers policy so immutable archives aren't clobbered to max-age=60.
  • scripts/versioned-docs/inject-version-switcher, publish-version, snapshot-cli-docs (vendors fingerprinted assets and trims the CLI left-nav to a self-contained command list with Docs Home / Latest Version), redact-version, assert-head-tag, plus a runbook README.
  • static/js/versioned-docs.{js,css} — evergreen version selector; fails silent where no manifest exists.
  • 10 release workflows wired with a safe gate — publishing only runs when the repo variable VERSIONED_DOCS_ENABLED is set (or on an explicit dispatch). Real releases are unaffected until then.
  • Backfill prereqs: update_repos.sh honors a requested tag; generate_python_docs.sh pins the requested version.

Status

  • Verified end-to-end on the testing stack (www.pulumi-test.io): bucket + behavior deployed, immutable cache headers confirmed, full publish → serve → redact cycle exercised, CLI snapshot validated on a real page (vendored styling, trimmed nav, single canonical).
  • Production is not touched. Rollout is gated and documented in scripts/versioned-docs/README.md (deploy the prod stack, wire the main prod stack, deploy the site, set VERSIONED_DOCS_ENABLED + VERSIONED_DOCS_PROD_DISTRIBUTION_ID).

Publish immutable, per-release snapshots of each SDK and CLI docset to a
permanent S3 bucket, served via CloudFront under /docs/versioned/{tool}/{version}/.
The publish step is additive to the existing doc-generation workflows; historical
versions never enter git, Hugo, or the per-deploy origin sync, so site build and
deploy times are unaffected.

- infrastructure/versioned-docs/: permanent bucket + GitHub-OIDC publisher role
- main stack: /docs/versioned/* origin + behavior behind a versionedDocsStack config,
  with a cache policy that honors origin Cache-Control (no max-age=60 clobber)
- scripts/versioned-docs/: inject, publish, snapshot-cli (vendors assets + trims the
  CLI left-nav to a self-contained command list), redact, head-tag assertion
- static/js/versioned-docs.{js,css}: evergreen version selector (fails silent)
- 10 release workflows wired with a safe gate: publishing stays off until the repo
  variable VERSIONED_DOCS_ENABLED is set, so real releases are unaffected
- backfill prereqs: update_repos.sh honors a requested tag; generate_python_docs.sh
  pins the requested version

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@pulumi-bot

pulumi-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

@pulumi-bot

pulumi-bot commented Jun 22, 2026

Copy link
Copy Markdown
Collaborator

Lighthouse Performance Report

Commit: 213c081 | Metric definitions

Page Device Score FCP LCP TBT CLS SI
Homepage Mobile 🔴 39 5.9s 14.0s 825ms 0.079 5.9s
Homepage Desktop 🟡 82 0.7s 1.8s 244ms 0.016 1.2s
Install Pulumi Mobile 🟡 56 5.3s 8.9s 179ms 0.029 7.6s
Install Pulumi Desktop 🟡 84 1.2s 1.8s 0ms 0.014 2.7s
AWS Get Started Mobile 🟡 62 5.0s 8.4s 58ms 0.074 5.0s
AWS Get Started Desktop 🟡 84 1.2s 1.8s 0ms 0.031 2.6s

…rkflows

The dotnet, java, typescript, policy-ts, and both esc-sdk (dotnet/ts)
workflows check the docs repo out into a `docs/` subdirectory because they
also clone the SDK source repo alongside it. Their generation steps already
run with `working-directory: docs`, but the new "Publish versioned snapshot"
step did not — so `./scripts/versioned-docs/publish-version.sh` resolved
against the runner root and failed with exit 127 (No such file or directory).
Because the step is continue-on-error, the job still went green while
publishing nothing.

Add `working-directory: docs` to the publish step in the six split-checkout
workflows. The four root-checkout workflows (cli, python, policy-python,
esc-python) already worked and are unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
DocFX/Javadoc SDKs (.NET, Java) emit type-named .html files and no root
index.html — their `/docs/reference/pkg/<sdk>/` landing page is generated by
Hugo (`_index.md`) and is not part of the prebuilt API output we snapshot. As
a result the bare version root (`/docs/versioned/dotnet/vX/`) 404'd, which also
broke the version-selector's root fallback.

When the prebuilt has no root index.html, synthesize a minimal landing that
links each top-level namespace entry (`<ns>/<ns>.html`, or `<ns>/` when that
dir has its own index). Done before tag injection so the landing also gets the
selector loader, noindex, and canonical. SDKs that already ship a root
index.html (TypeDoc/nodejs, Sphinx/python) are untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…st selector UI

UI rework (versioned-docs.js/.css), all evergreen — existing archives pick it up
with no republish:
- Archives + live SDK pages (their own generator themes, no site chrome) now render a
  fixed top bar with max z-index that measures its own height and offsets the known
  themes' fixed chrome (Sphinx RTD sidebar, DocFX navbar, TypeDoc toolbar), so it never
  clips. Fixes the Python (RTD) overlap and pre-empts the same on .NET (DocFX).
- Live CLI command pages (with site chrome + a mount) keep the quiet inline control.
- One shared dropdown control, context-appropriate framing (archive adds the
  view-latest notice; latest is just the quiet dropdown).

Live SDK selectors (no commit churn): scripts/versioned-docs/inject-live-sdk-selectors.sh
injects the latest-mode loader into the SDK reference docsets in public/ at site-build
time (wired into build-site.sh). Nested policy/ESC subtrees are tagged with their own
tool id first; the idempotent injector leaves them alone on the parent pass.

Backfill without noisy PRs: add a `publish_only` workflow input to all 10 SDK/CLI doc
workflows; when set, the latest-docs PR-creation job is skipped while the versioned
snapshot still publishes.

Fixes: trim-cli-nav.py labels the version-root self-link "Overview" instead of a second
"pulumi"; corrected the nodejs policy/ESC --src/--live-root paths (they were missing the
pulumi/ segment and would have failed on "src dir not found").

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The nodejs (/docs/reference/pkg/nodejs/pulumi/) and .NET ESC SDK
(/docs/reference/pkg/dotnet/esc-sdk/) liveRoots are bare container dirs with no
index.html — the site links to a deeper entry, so the bare path 404s, and so
does the version selector's "View latest" for those tools. At build time, drop
a small redirect landing at those roots pointing to the real entry, only when
one isn't already present.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nx doctree cruft

- Guard the run-id branch push in all 10 doc-gen workflows behind
  publish_only != true, so backfill (publish-only) runs no longer leave
  orphan remote branches behind.
- Direct Sphinx's .doctrees pickle cache out of the python output dir
  (it defaults to OUTDIR/.doctrees), keeping build state out of the
  committed prebuilt and the immutable snapshots.
- Belt-and-braces: exclude *.doctree/.doctrees/.buildinfo from the
  versioned-docs publish sync.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
CamSoper and others added 2 commits June 24, 2026 21:12
The fixed selector bar reserves space by measuring its own height into
--vdocs-bar-h, but that measurement ran before the async-loaded
/js/versioned-docs.css applied — so the bar was measured unstyled (too
short) and the reserved gap was too small, leaving the generator's own
top chrome tucked under the bar on every live SDK docset.

- Re-measure the bar via ResizeObserver (fires on the initial observe and
  whenever the box changes: CSS applies, web fonts load, wrap, resize),
  with load/onload/rAF fallbacks where ResizeObserver is unavailable.
- Offset the generator top chrome that wasn't covered: DocFX's Bootstrap-3
  .navbar-fixed-top (.NET, ESC .NET) and Javadoc's .fixedNav (Java). The
  old rule only matched DocFX's Bootstrap-4 .navbar.fixed-top.

Verified headless across TypeDoc (nodejs), DocFX (dotnet), and Javadoc
(java); the policy/ESC docsets share these generators.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
The selector banner only rendered on individual command pages (single.html).
The commands section index (/docs/iac/cli/commands/) is the canonical live
root the selector points at, so render the banner there too — gated on the
cli_command_page cascade, which only the commands section carries.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
/docs/iac/cli/ and /docs/iac/cli/commands/ both rendered the full
{{< pulumi-command >}} reference, so they were near-duplicates. Drop the
full command dump from the overview and link to the canonical command
reference instead; the overview keeps the intro, install, common-commands
quick links, environment variables, and exit codes.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
CLI archives are Hugo-rendered site pages, so they need the site CSS.
Previously snapshot-cli-docs.sh vendored a frozen per-version copy of the
fingerprinted bundle into each archive's _vassets/ — robust, but it meant
re-theming the whole back-catalog required republishing every version.

Now every CLI archive references one shared, permanent contract URL,
/css/versioned-docs-archive.css, re-derived from the docs CSS bundle on
every site build (build-site.sh). Update that one file and the entire CLI
back-catalog re-themes at once.

- build-site.sh: copy public/css/bundle.<id>.css -> versioned-docs-archive.css
  after the CSS purge.
- snapshot-cli-docs.sh: rewrite archive CSS refs to the shared bundle instead
  of vendoring; drop ALL site /js/ <script src> bundles (archives are static —
  the nav is trimmed to a static list, and the site bundle lazy-loads
  fingerprinted chunks the snapshot never vendored, which would 404 once the
  main site rotates). Removing the live latest-mode selector tag also lets the
  archive-mode tag inject cleanly. Add --out-dir for a local dry run (no publish).

Verified headless: a dry-run snapshot renders fully themed from the shared
bundle with a correct archive-mode (vX.Y.Z) selector and no heavy site JS.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
… semver

A back-corpus run (publish_only) of an OLD release went through
publish-version.sh with its defaults — mark-latest=true and date=now — so
it stole the "latest" flag and, dated today, sorted ahead of newer
versions. Surfaced by the first real CLI back-corpus run (v3.244.0).

- publish-version.sh: order the manifest newest-first by SEMVER (numeric
  component compare, so 3.10.0 > 3.9.0), not by publish date — a version
  archived today still sorts below newer ones released earlier.
- snapshot-cli-docs.sh: accept --no-mark-latest / --date and pass them to
  publish-version.sh.
- All 10 doc-gen workflows: on publish_only (backfill), pass --no-mark-latest
  so the archived old version doesn't claim "latest".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
CamSoper and others added 2 commits June 25, 2026 19:45
…ce-free

publish-version.sh updates the manifest with a read-modify-write, which
races under a parallel back-corpus backfill (last-write-wins drops
entries). rebuild-manifest.sh instead regenerates versions.json from the
bucket's archive prefixes in one shot: semver-sorted, with the live
--latest-version emitted as a liveRoot/latest entry. Run it once after a
parallel fan-out to get a consistent manifest.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
snapshot-cli-docs.sh deleted the per-command markdown content-negotiation
outputs, so archived CLI versions had no .md for LLMs/agents (only the live
docs did). Keep them, and rewrite their frontmatter `url:` and cross-command
links to the versioned prefix (the new replace_in_content covers .md as well
as .html). The HTML stays noindex/canonical-to-live; the .md are the
machine-readable format, now versioned too.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
The .NET / ESC .NET (DocFX) pages put position:fixed on the <header>
element itself (docfx.css), while the navbar inside is just
.navbar.navbar-inverse — so the old .navbar-fixed-top offset matched
nothing and the bar overlapped the navbar/logo on those archives.

Offset `header` instead. Safe across generators: it also matches TypeDoc's
<header class="tsd-page-toolbar"> (same offset value, already applied),
Sphinx has no <header>, and Javadoc's <header> is static so `top` is a
no-op there.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
The Reference > CLI > Pulumi CLI nesting was pointless once the ESC CLI
was retired, leaving the CLI container wrapping a single Pulumi CLI entry.
Remove the reference-cli container node and re-parent Pulumi CLI directly
under reference-home (weight 1, where CLI was).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
The trimmed CLI-archive left nav is a static command list with no search.
Add a quiet client-side substring filter above it, injected by the shared
loader (not baked into snapshots) so it works on already-published archives
and stays controllable from the main repo without re-publishing.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
CLI snapshots strip the site JS bundle, so the right-rail On this page ToC
(JS-populated by theme/src/ts/misc.ts) shipped empty, and the Edit this Page /
Request a Change links, the Copy Page / top-button web components, and the
feedback widget were dead or meaningless on a frozen snapshot.

trim-cli-nav.py now, in the same pass that trims the left nav: drops the
right-rail actions/feedback chrome, and statically rebuilds the ToC from each
page id'd h2/h3 (mirroring misc.ts) into the desktop + mobile lists. Stdlib
only; each pass no-ops if its hook is absent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
@CamSoper CamSoper force-pushed the CamSoper/hackathon-versioned-docs branch from e3e1950 to 23dc488 Compare June 25, 2026 23:12
The live "Copy Page" menu is the pulumi-llm-menu web component, compiled into
the site JS bundle that CLI snapshots strip — so the bare element can't work on
an archive. But every archived page publishes its markdown sibling (index.md),
so the menu's actions still apply.

versioned-docs.js now rebuilds the component's exact DOM/classes on CLI archive
pages (loader-injected like the nav filter, so it works on already-published
archives without re-snapshotting) and wires all five actions against the page's
own index.md: Copy URL, Copy as Markdown, View as Markdown, Open in ChatGPT,
Open in Claude. The shared archive theme bundle already carries the .llm-menu-*
styles; versioned-docs.css only restores the trigger/chevron sizing the live
component set inline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
…llow wire fns

The loader runs as an async script. When the DOM is already parsed (typical
behind a CDN), onReady ran its callback synchronously — mid-IIFE, before the
var ICON / helper assignments below it — so wireCliCopyMenu hit an undefined
ICON and threw. The outer try swallowed it AND aborted the manifest fetch, so
both the Copy Page menu and the version selector silently vanished on live
archives (worked locally only because the script there runs mid-parse and
defers to DOMContentLoaded). Defer to a macrotask instead: the whole IIFE has
executed and wire-fn errors no longer abort the selector render.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
CLI archives kept the inline pre-paint script that reads the shared
"pulumi-docs-theme" key and sets data-theme on <html>, and the archive theme
bundle carries the dark overrides + .docs-theme-toggle styles — so a snapshot
already renders in the user's chosen theme. But the toggle control lives in the
trimmed sidebar and its wiring (theme/src/ts/docs-theme.ts) is in the stripped
site bundle, so there was no way to change it from an archive.

versioned-docs.js now injects the same three-button control into the CLI
archive nav and wires it against the identical localStorage key + data-theme/
-theme-pref attributes, so a preference set anywhere on the docs site — main
site or archive — stays consistent and persists everywhere. Loader-injected, so
it applies to every already-published archive without re-snapshotting; CLI
archives only (SDK generator themes have no docs dark mode).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017QYJeFHZw6NLaUu2wLpuay
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants