From f433ce2cba79db11862d0e5a0d75b84b94b41a57 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 27 Oct 2026 21:39:12 -0700 Subject: [PATCH 1/5] ci(build): introduce opt-in development feature bundles Signed-off-by: Drew Newberry --- .github/workflows/branch-checks.yml | 4 ++-- .github/workflows/branch-e2e.yml | 1 + .github/workflows/build-binaries.yml | 8 ++++++++ .github/workflows/release-dev.yml | 1 + .github/workflows/rust-cache.yml | 2 +- CI.md | 4 ++-- CONTRIBUTING.md | 5 +++++ crates/openshell-gateway/Cargo.toml | 2 ++ crates/openshell-server/Cargo.toml | 2 ++ crates/openshell-supervisor/Cargo.toml | 2 ++ tasks/ci.toml | 2 +- tasks/rust.toml | 4 ++-- tasks/test.toml | 4 ++-- 13 files changed, 31 insertions(+), 10 deletions(-) diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index 218d3d2657..ae50d955a8 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -273,7 +273,7 @@ jobs: - name: Lint shell: nix develop -c bash -euo pipefail {0} run: | - cargo clippy --locked --workspace --all-targets -- -D warnings + cargo clippy --locked --workspace --all-targets --features dev -- -D warnings cargo clippy --locked --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings @@ -325,7 +325,7 @@ jobs: env: OPENSHELL_TELEMETRY_ENABLED: "false" run: | - cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support + cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,dev cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml diff --git a/.github/workflows/branch-e2e.yml b/.github/workflows/branch-e2e.yml index db4c96b936..649146f86a 100644 --- a/.github/workflows/branch-e2e.yml +++ b/.github/workflows/branch-e2e.yml @@ -108,6 +108,7 @@ jobs: uses: ./.github/workflows/build-binaries.yml with: cargo-version: ${{ needs.version.outputs.cargo }} + dev-build: true secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 1289968d14..0973e901ab 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -17,6 +17,11 @@ on: required: false type: string default: "" + dev-build: + description: Enable the gateway and supervisor development feature bundle + required: false + type: boolean + default: false secrets: CACHIX_AUTH_TOKEN: required: true @@ -85,6 +90,7 @@ jobs: env: PACKAGE: openshell-gateway BINARY: openshell-gateway + EXTRA_CARGO_FLAGS: ${{ inputs.dev-build && '--features dev' || '' }} strategy: matrix: include: @@ -103,6 +109,8 @@ jobs: env: PACKAGE: ${{ matrix.package }} BINARY: ${{ matrix.binary }} + # Only supervisor entries define the development feature bundle. + EXTRA_CARGO_FLAGS: ${{ matrix.package == 'openshell-supervisor' && inputs.dev-build && '--features dev' || '' }} strategy: matrix: include: diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index aec77972ee..200ec0449e 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -68,6 +68,7 @@ jobs: cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} supervisor-image-tag: dev checkout-ref: ${{ github.sha }} + dev-build: true secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/.github/workflows/rust-cache.yml b/.github/workflows/rust-cache.yml index d9ec30c631..be54cac182 100644 --- a/.github/workflows/rust-cache.yml +++ b/.github/workflows/rust-cache.yml @@ -62,6 +62,6 @@ jobs: shell: nix develop -c bash -euo pipefail {0} run: | cargo test --locked -p openshell-crypto --no-default-features --test independent_backend --no-run - cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support --no-run + cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,dev --no-run cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --no-run cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml --no-run diff --git a/CI.md b/CI.md index df66dc0ad3..94d5370bd6 100644 --- a/CI.md +++ b/CI.md @@ -655,9 +655,9 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma | File | Role | |---|---| -| `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. | +| `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. Rust lint and tests run once with the development configuration (`--features dev`). | | `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups do not run E2E. | -| `.github/workflows/build-binaries.yml`, `build-vm-driver.yml` | Shared binary matrices used by branch and release workflows. The VM driver remains separate because its build consumes the runtime binaries. | +| `.github/workflows/build-binaries.yml`, `build-vm-driver.yml` | Shared binary matrices used by branch and release workflows. The VM driver remains separate because its build consumes the runtime binaries. `dev-build: true` selects `--features dev` for the gateway and supervisor. Release Dev and branch E2E enable it; prerelease and stable builds use the default of `false`. | | `.github/workflows/build-images.yml` | Builds and pushes multi-platform images, then uploads the same OCI images as workflow artifacts. | | `.github/workflows/package-release-binaries.yml` | Packages raw build artifacts into release tarballs without rebuilding them. | | `.github/workflows/e2e-docker-test.yml`, `e2e-podman-test.yml`, `e2e-vm-test.yml`, `e2e-kubernetes-test.yml` | Reusable runtime lanes called directly by branch and release workflows. Callers select suites and declare only the artifacts each runtime consumes. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b1034dd23f..25b863a287 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -306,6 +306,11 @@ mise trust mise run gateway ``` +The normal Rust workspace build, check, lint, and test tasks enable the +`dev` feature bundle. CI uses the same development configuration. Use +`--features dev` to select it in direct Cargo builds; Cargo's `dev` profile +does not enable it automatically. Prerelease and stable builds leave it off. + ## Building the `openshell` CLI Inside this repository, `openshell` is a local shortcut script at `scripts/bin/openshell`. The script will diff --git a/crates/openshell-gateway/Cargo.toml b/crates/openshell-gateway/Cargo.toml index fa977a2c2a..75d3eae25a 100644 --- a/crates/openshell-gateway/Cargo.toml +++ b/crates/openshell-gateway/Cargo.toml @@ -41,6 +41,8 @@ openshell-driver-mxc = { path = "../openshell-driver-mxc", optional = true } [features] default = ["telemetry", "in-tree-compute-drivers"] +# Opt-in bundle for development-only features; stable builds leave it off. +dev = ["openshell-server/dev"] in-tree-compute-drivers = [ "compute-driver-docker", "compute-driver-kubernetes", diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 9160719bf9..1532131a55 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -120,6 +120,8 @@ notify = "8" [features] default = ["telemetry"] +# Opt-in bundle for development-only features; stable builds leave it off. +dev = [] ## Compile in anonymous telemetry emission (forwards to openshell-core/telemetry). ## On by default; build with `--no-default-features` for a telemetry-free gateway ## that contains no telemetry endpoint, HTTP client, or emission code. diff --git a/crates/openshell-supervisor/Cargo.toml b/crates/openshell-supervisor/Cargo.toml index e6e7fcd6ba..aa84df9df2 100644 --- a/crates/openshell-supervisor/Cargo.toml +++ b/crates/openshell-supervisor/Cargo.toml @@ -54,6 +54,8 @@ uuid = { workspace = true } [features] default = ["telemetry", "bundled-ca-roots"] +# Opt-in bundle for development-only features; stable builds leave it off. +dev = [] system-ca-roots = ["telemetry"] defaults-without-telemetry = ["bundled-ca-roots"] telemetry = ["openshell-core/telemetry"] diff --git a/tasks/ci.toml b/tasks/ci.toml index 7f896c570d..98b49b1d47 100644 --- a/tasks/ci.toml +++ b/tasks/ci.toml @@ -14,7 +14,7 @@ hide = true ["build:rust:workspace"] description = "Build all Rust crates" -run = "cargo build --workspace" +run = "cargo build --workspace --features dev" hide = true ["build:release"] diff --git a/tasks/rust.toml b/tasks/rust.toml index 682e478fca..cbe63364a2 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -11,7 +11,7 @@ run = "cargo run --release -p openshell-sandbox --features perf-harness --bin op description = "Check all Rust crates for errors" depends = ["rust:lockfiles:check"] run = [ - "cargo check --workspace", + "cargo check --workspace --features dev", "cargo check -p openshell-sandbox --all-targets --features perf-harness", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 check native" @@ -27,7 +27,7 @@ hide = true description = "Lint Rust code with Clippy (deny warnings)" depends = ["rust:lockfiles:check"] run = [ - "cargo clippy --workspace --all-targets -- -D warnings", + "cargo clippy --workspace --all-targets --features dev -- -D warnings", "cargo clippy -p openshell-sandbox --all-targets --features perf-harness -- -D warnings", "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings", diff --git a/tasks/test.toml b/tasks/test.toml index 30e8982599..0859ae2286 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -116,8 +116,8 @@ run = [ "cargo test -p openshell-crypto --no-default-features --test independent_backend", # Run the workspace once without openshell-server so we can run that crate # with test-only helpers enabled. - "cargo test --workspace --exclude openshell-server", - "cargo test -p openshell-server --features test-support", + "cargo test --workspace --exclude openshell-server --features dev", + "cargo test -p openshell-server --features test-support,dev", "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/testkit/Cargo.toml", ] From 0fb6390fda085cb3572ef05a1810b38ccb074855 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 27 Oct 2026 22:03:32 -0700 Subject: [PATCH 2/5] docs(dev): move development workflow guidance into base branch Signed-off-by: Drew Newberry --- .agents/skills/release-captain/SKILL.md | 3 ++ .agents/skills/update-docs/SKILL.md | 1 + fern/README.md | 2 + tasks/scripts/sync_docs_website.py | 58 ++++++++++++++++++++- tasks/scripts/sync_docs_website_test.py | 68 +++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 2 deletions(-) diff --git a/.agents/skills/release-captain/SKILL.md b/.agents/skills/release-captain/SKILL.md index 6ad534404f..e0236faafc 100644 --- a/.agents/skills/release-captain/SKILL.md +++ b/.agents/skills/release-captain/SKILL.md @@ -41,6 +41,9 @@ tag, or start a second `Pre-release` run for a tag; it has no concurrency guard. - `Release` (`release.yml`) is dispatched by hand with the stable version. It picks the highest `-pre.N` on `main` whose whole `Pre-release` run passed, creates the stable tag on that commit, and publishes without requalifying. +- Pre-release and stable builds leave the `dev` feature disabled. Release + Dev enables it through `dev-build: true`. Normal workspace builds, lint, + and tests enable `dev` without separate CI lanes for development features. ## Setup diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index a4ecd862b4..9c09b3d810 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -111,6 +111,7 @@ Write the smallest update that tells users exactly what changed and what they ne - **Keep each page URL equal to its file path** under `docs/`. Rename the file when you rename a page, add a `fern/docs.yml` redirect for the old URL, and set a relative `slug:` when the nav label does not produce the file name. `mise run docs` runs `docs:nav`, which fails otherwise. - **Use `keywords` as a comma-separated string**. - **Do not add a duplicate H1**. Fern renders the page title from frontmatter. +- **Keep development-only features on development-only pages**. A feature that stable and prerelease builds exclude belongs on a page with `development-only: true` in frontmatter, which only the `dev` docs version publishes. Do not link to that page or describe the feature from other pages. Refer to `fern/README.md`. - **Always write NVIDIA in all caps.** Wrong: Nvidia, nvidia. - **Always capitalize OpenShell correctly.** Wrong: openshell, Openshell, openShell. - **Do not number section titles.** Wrong: "Section 1: Deploy a Gateway" or "Step 3: Verify." Use plain descriptive titles. diff --git a/fern/README.md b/fern/README.md index 71c26fa1e2..0661ee2c39 100644 --- a/fern/README.md +++ b/fern/README.md @@ -55,6 +55,8 @@ The sync workflow still accepts an optional Fern availability badge, but the cur The sync and publish workflows share the `docs-website` concurrency group. This serializes writes and publication. Queued runs remain pending instead of replacing one another. +A page whose front matter sets `development-only: true` documents a feature that stable and prerelease builds exclude. Only the `dev` snapshot publishes it: every other sync deletes the page from its `pages-/` copy and drops the page's entries from that version's navigation. Fern ignores the front matter field, but it rejects unknown keys in `docs/index.yml`, so the marker lives in the page. `fern check` does not report links to the removed page, so pages published in other versions must not link to a development-only page or document its feature. + The `dev` snapshot also owns the shared Fern configuration, components, assets, and CSS on `docs-website`. The `latest` snapshot copies its documentation, navigation, and redirects but does not replace shared components, assets, or CSS. This keeps the site configuration aligned with `main` while preserving the released content. A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release cannot change the `dev` announcement. diff --git a/tasks/scripts/sync_docs_website.py b/tasks/scripts/sync_docs_website.py index 03abee3ce6..2c0786b0c8 100644 --- a/tasks/scripts/sync_docs_website.py +++ b/tasks/scripts/sync_docs_website.py @@ -28,6 +28,10 @@ DISPLAY_VERSION_RE = re.compile(r"\bv?(\d+\.\d+\.\d+(?:[.-]?[A-Za-z0-9]+)*)\b") VERSION_AVAILABILITIES = {"beta", "deprecated", "ga", "stable"} SNAPSHOT_METADATA_FILE = ".docs-snapshots.yml" +# Front matter key for pages published only in the `dev` snapshot. Fern ignores +# unknown front matter keys but rejects unknown keys in navigation entries. +DEVELOPMENT_ONLY_KEY = "development-only" +FRONT_MATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---", re.S) YamlMapping = dict[str, object] @@ -288,8 +292,53 @@ def prefix_navigation_paths(value: object, pages_dir: str) -> object: return value -def version_navigation(source_index: Path, pages_dir: str) -> YamlMapping: +def is_development_only(page: Path) -> bool: + match = FRONT_MATTER_RE.match(page.read_text(encoding="utf-8")) + if match is None: + return False + front_matter = yaml.safe_load(match.group(1)) + return ( + isinstance(front_matter, dict) + and front_matter.get(DEVELOPMENT_ONLY_KEY) is True + ) + + +def remove_development_only_pages(pages: Path) -> set[str]: + """Delete pages marked development-only and return their docs-relative paths.""" + removed: set[str] = set() + for page in sorted(pages.rglob("*.mdx")): + if is_development_only(page): + page.unlink() + removed.add(page.relative_to(pages).as_posix()) + return removed + + +def drop_navigation_paths(value: object, removed: set[str]) -> object: + if isinstance(value, dict): + for child in cast("YamlMapping", value).values(): + drop_navigation_paths(child, removed) + elif isinstance(value, list): + items = cast("list[object]", value) + items[:] = [ + item + for item in items + if not ( + isinstance(item, dict) + and str(cast("YamlMapping", item).get("path", "")).removeprefix("./") + in removed + ) + ] + for item in items: + drop_navigation_paths(item, removed) + return value + + +def version_navigation( + source_index: Path, pages_dir: str, removed: set[str] | None = None +) -> YamlMapping: data = read_yaml(source_index) + if removed: + drop_navigation_paths(data, removed) prefix_navigation_paths(data, pages_dir) return data @@ -477,6 +526,11 @@ def write_snapshot( ) -> None: pages_dir = f"pages-{entry.slug}" reset_directory(source_docs, target_fern / pages_dir) + # Only the dev snapshot documents development-only features, which stable + # and prerelease builds exclude. + removed: set[str] = set() + if entry.slug != "dev": + removed = remove_development_only_pages(target_fern / pages_dir) if refresh_shared: merge_directory(source_fern / "assets", target_fern / "assets", overwrite=True) merge_directory( @@ -495,7 +549,7 @@ def write_snapshot( versions_dir.mkdir(parents=True, exist_ok=True) write_yaml( versions_dir / f"{entry.slug}.yml", - version_navigation(source_docs / "index.yml", pages_dir), + version_navigation(source_docs / "index.yml", pages_dir, removed), ) update_docs_yml(target_fern / "docs.yml", entry, target_fern) diff --git a/tasks/scripts/sync_docs_website_test.py b/tasks/scripts/sync_docs_website_test.py index 61486702f4..5b8156c514 100644 --- a/tasks/scripts/sync_docs_website_test.py +++ b/tasks/scripts/sync_docs_website_test.py @@ -918,6 +918,74 @@ def test_only_dev_refreshes_shared_fern_files(tmp_path: Path) -> None: assert card.read_text(encoding="utf-8") == "export const Card = 'dev';\n" +def test_development_only_pages_publish_only_in_dev(tmp_path: Path) -> None: + source = tmp_path / "source" + website = tmp_path / "docs-website" + _make_source_tree(source) + _make_docs_website_tree(website) + docs = source / "docs" + (docs / "guide").mkdir() + (docs / "guide" / "preview.mdx").write_text( + "---\ntitle: Preview\ndevelopment-only: true\n---\nPreview\n", + encoding="utf-8", + ) + (docs / "guide" / "stable.mdx").write_text( + "---\ntitle: Stable\ndevelopment-only: false\n---\nStable\n", + encoding="utf-8", + ) + (docs / "index.yml").write_text( + yaml.safe_dump( + { + "navigation": [ + {"page": "Intro", "path": "intro.mdx"}, + { + "section": "Guide", + "contents": [ + {"page": "Stable", "path": "guide/stable.mdx"}, + {"page": "Preview", "path": "guide/preview.mdx"}, + ], + }, + ] + } + ), + encoding="utf-8", + ) + + def sync(channel: str, release_version: str, version_slug: str) -> None: + sdw.sync_docs( + Namespace( + source_root=source, + docs_website_root=website, + channel=channel, + source_ref="main" if channel == "dev" else f"v{release_version}", + source_sha="sha", + release_version=release_version, + version_slug=version_slug, + display_name="", + availability="", + allow_rollback=False, + ) + ) + + def guide_pages(slug: str) -> list[str]: + navigation = read_yaml(fern / "versions" / f"{slug}.yml")["navigation"] + return [page["path"] for page in navigation[1]["contents"]] + + sync("dev", "0.2.1.dev1", "") + sync("stable", "0.2.0", "v0.2.0") + + fern = website / "fern" + assert (fern / "pages-dev" / "guide" / "preview.mdx").is_file() + assert guide_pages("dev") == [ + "../pages-dev/guide/stable.mdx", + "../pages-dev/guide/preview.mdx", + ] + for slug in ("v0.2.0", "latest"): + assert not (fern / f"pages-{slug}" / "guide" / "preview.mdx").exists() + assert (fern / f"pages-{slug}" / "guide" / "stable.mdx").is_file() + assert guide_pages(slug) == [f"../pages-{slug}/guide/stable.mdx"] + + def test_remove_docs_drops_snapshot(tmp_path: Path) -> None: source = tmp_path / "source" website = tmp_path / "docs-website" From 71191c943bd6ab21a436fb8bfe2234301a69c8ce Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Tue, 27 Oct 2026 22:07:00 -0700 Subject: [PATCH 3/5] refactor(docs): remove development-only Python publishing filter Signed-off-by: Drew Newberry --- .agents/skills/update-docs/SKILL.md | 1 - fern/README.md | 2 - tasks/scripts/sync_docs_website.py | 58 +-------------------- tasks/scripts/sync_docs_website_test.py | 68 ------------------------- 4 files changed, 2 insertions(+), 127 deletions(-) diff --git a/.agents/skills/update-docs/SKILL.md b/.agents/skills/update-docs/SKILL.md index 9c09b3d810..a4ecd862b4 100644 --- a/.agents/skills/update-docs/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -111,7 +111,6 @@ Write the smallest update that tells users exactly what changed and what they ne - **Keep each page URL equal to its file path** under `docs/`. Rename the file when you rename a page, add a `fern/docs.yml` redirect for the old URL, and set a relative `slug:` when the nav label does not produce the file name. `mise run docs` runs `docs:nav`, which fails otherwise. - **Use `keywords` as a comma-separated string**. - **Do not add a duplicate H1**. Fern renders the page title from frontmatter. -- **Keep development-only features on development-only pages**. A feature that stable and prerelease builds exclude belongs on a page with `development-only: true` in frontmatter, which only the `dev` docs version publishes. Do not link to that page or describe the feature from other pages. Refer to `fern/README.md`. - **Always write NVIDIA in all caps.** Wrong: Nvidia, nvidia. - **Always capitalize OpenShell correctly.** Wrong: openshell, Openshell, openShell. - **Do not number section titles.** Wrong: "Section 1: Deploy a Gateway" or "Step 3: Verify." Use plain descriptive titles. diff --git a/fern/README.md b/fern/README.md index 0661ee2c39..71c26fa1e2 100644 --- a/fern/README.md +++ b/fern/README.md @@ -55,8 +55,6 @@ The sync workflow still accepts an optional Fern availability badge, but the cur The sync and publish workflows share the `docs-website` concurrency group. This serializes writes and publication. Queued runs remain pending instead of replacing one another. -A page whose front matter sets `development-only: true` documents a feature that stable and prerelease builds exclude. Only the `dev` snapshot publishes it: every other sync deletes the page from its `pages-/` copy and drops the page's entries from that version's navigation. Fern ignores the front matter field, but it rejects unknown keys in `docs/index.yml`, so the marker lives in the page. `fern check` does not report links to the removed page, so pages published in other versions must not link to a development-only page or document its feature. - The `dev` snapshot also owns the shared Fern configuration, components, assets, and CSS on `docs-website`. The `latest` snapshot copies its documentation, navigation, and redirects but does not replace shared components, assets, or CSS. This keeps the site configuration aligned with `main` while preserving the released content. A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release cannot change the `dev` announcement. diff --git a/tasks/scripts/sync_docs_website.py b/tasks/scripts/sync_docs_website.py index 2c0786b0c8..03abee3ce6 100644 --- a/tasks/scripts/sync_docs_website.py +++ b/tasks/scripts/sync_docs_website.py @@ -28,10 +28,6 @@ DISPLAY_VERSION_RE = re.compile(r"\bv?(\d+\.\d+\.\d+(?:[.-]?[A-Za-z0-9]+)*)\b") VERSION_AVAILABILITIES = {"beta", "deprecated", "ga", "stable"} SNAPSHOT_METADATA_FILE = ".docs-snapshots.yml" -# Front matter key for pages published only in the `dev` snapshot. Fern ignores -# unknown front matter keys but rejects unknown keys in navigation entries. -DEVELOPMENT_ONLY_KEY = "development-only" -FRONT_MATTER_RE = re.compile(r"\A---\r?\n(.*?)\r?\n---", re.S) YamlMapping = dict[str, object] @@ -292,53 +288,8 @@ def prefix_navigation_paths(value: object, pages_dir: str) -> object: return value -def is_development_only(page: Path) -> bool: - match = FRONT_MATTER_RE.match(page.read_text(encoding="utf-8")) - if match is None: - return False - front_matter = yaml.safe_load(match.group(1)) - return ( - isinstance(front_matter, dict) - and front_matter.get(DEVELOPMENT_ONLY_KEY) is True - ) - - -def remove_development_only_pages(pages: Path) -> set[str]: - """Delete pages marked development-only and return their docs-relative paths.""" - removed: set[str] = set() - for page in sorted(pages.rglob("*.mdx")): - if is_development_only(page): - page.unlink() - removed.add(page.relative_to(pages).as_posix()) - return removed - - -def drop_navigation_paths(value: object, removed: set[str]) -> object: - if isinstance(value, dict): - for child in cast("YamlMapping", value).values(): - drop_navigation_paths(child, removed) - elif isinstance(value, list): - items = cast("list[object]", value) - items[:] = [ - item - for item in items - if not ( - isinstance(item, dict) - and str(cast("YamlMapping", item).get("path", "")).removeprefix("./") - in removed - ) - ] - for item in items: - drop_navigation_paths(item, removed) - return value - - -def version_navigation( - source_index: Path, pages_dir: str, removed: set[str] | None = None -) -> YamlMapping: +def version_navigation(source_index: Path, pages_dir: str) -> YamlMapping: data = read_yaml(source_index) - if removed: - drop_navigation_paths(data, removed) prefix_navigation_paths(data, pages_dir) return data @@ -526,11 +477,6 @@ def write_snapshot( ) -> None: pages_dir = f"pages-{entry.slug}" reset_directory(source_docs, target_fern / pages_dir) - # Only the dev snapshot documents development-only features, which stable - # and prerelease builds exclude. - removed: set[str] = set() - if entry.slug != "dev": - removed = remove_development_only_pages(target_fern / pages_dir) if refresh_shared: merge_directory(source_fern / "assets", target_fern / "assets", overwrite=True) merge_directory( @@ -549,7 +495,7 @@ def write_snapshot( versions_dir.mkdir(parents=True, exist_ok=True) write_yaml( versions_dir / f"{entry.slug}.yml", - version_navigation(source_docs / "index.yml", pages_dir, removed), + version_navigation(source_docs / "index.yml", pages_dir), ) update_docs_yml(target_fern / "docs.yml", entry, target_fern) diff --git a/tasks/scripts/sync_docs_website_test.py b/tasks/scripts/sync_docs_website_test.py index 5b8156c514..61486702f4 100644 --- a/tasks/scripts/sync_docs_website_test.py +++ b/tasks/scripts/sync_docs_website_test.py @@ -918,74 +918,6 @@ def test_only_dev_refreshes_shared_fern_files(tmp_path: Path) -> None: assert card.read_text(encoding="utf-8") == "export const Card = 'dev';\n" -def test_development_only_pages_publish_only_in_dev(tmp_path: Path) -> None: - source = tmp_path / "source" - website = tmp_path / "docs-website" - _make_source_tree(source) - _make_docs_website_tree(website) - docs = source / "docs" - (docs / "guide").mkdir() - (docs / "guide" / "preview.mdx").write_text( - "---\ntitle: Preview\ndevelopment-only: true\n---\nPreview\n", - encoding="utf-8", - ) - (docs / "guide" / "stable.mdx").write_text( - "---\ntitle: Stable\ndevelopment-only: false\n---\nStable\n", - encoding="utf-8", - ) - (docs / "index.yml").write_text( - yaml.safe_dump( - { - "navigation": [ - {"page": "Intro", "path": "intro.mdx"}, - { - "section": "Guide", - "contents": [ - {"page": "Stable", "path": "guide/stable.mdx"}, - {"page": "Preview", "path": "guide/preview.mdx"}, - ], - }, - ] - } - ), - encoding="utf-8", - ) - - def sync(channel: str, release_version: str, version_slug: str) -> None: - sdw.sync_docs( - Namespace( - source_root=source, - docs_website_root=website, - channel=channel, - source_ref="main" if channel == "dev" else f"v{release_version}", - source_sha="sha", - release_version=release_version, - version_slug=version_slug, - display_name="", - availability="", - allow_rollback=False, - ) - ) - - def guide_pages(slug: str) -> list[str]: - navigation = read_yaml(fern / "versions" / f"{slug}.yml")["navigation"] - return [page["path"] for page in navigation[1]["contents"]] - - sync("dev", "0.2.1.dev1", "") - sync("stable", "0.2.0", "v0.2.0") - - fern = website / "fern" - assert (fern / "pages-dev" / "guide" / "preview.mdx").is_file() - assert guide_pages("dev") == [ - "../pages-dev/guide/stable.mdx", - "../pages-dev/guide/preview.mdx", - ] - for slug in ("v0.2.0", "latest"): - assert not (fern / f"pages-{slug}" / "guide" / "preview.mdx").exists() - assert (fern / f"pages-{slug}" / "guide" / "stable.mdx").is_file() - assert guide_pages(slug) == [f"../pages-{slug}/guide/stable.mdx"] - - def test_remove_docs_drops_snapshot(tmp_path: Path) -> None: source = tmp_path / "source" website = tmp_path / "docs-website" From 5dc7764fe72a963f33f9bedd033f6a0bf5f76537 Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 28 Oct 2026 15:43:11 -0700 Subject: [PATCH 4/5] refactor(build): rename dev feature bundle to experimental Signed-off-by: Drew Newberry --- .agents/skills/release-captain/SKILL.md | 4 ++-- .github/workflows/branch-checks.yml | 4 ++-- .github/workflows/build-binaries.yml | 8 ++++---- .github/workflows/rust-cache.yml | 2 +- CI.md | 4 ++-- CONTRIBUTING.md | 4 ++-- crates/openshell-gateway/Cargo.toml | 4 ++-- crates/openshell-server/Cargo.toml | 4 ++-- crates/openshell-supervisor/Cargo.toml | 4 ++-- rfc/0014-release-stability/README.md | 2 +- tasks/ci.toml | 2 +- tasks/rust.toml | 4 ++-- tasks/test.toml | 4 ++-- 13 files changed, 25 insertions(+), 25 deletions(-) diff --git a/.agents/skills/release-captain/SKILL.md b/.agents/skills/release-captain/SKILL.md index e0236faafc..ed73ef19cb 100644 --- a/.agents/skills/release-captain/SKILL.md +++ b/.agents/skills/release-captain/SKILL.md @@ -41,9 +41,9 @@ tag, or start a second `Pre-release` run for a tag; it has no concurrency guard. - `Release` (`release.yml`) is dispatched by hand with the stable version. It picks the highest `-pre.N` on `main` whose whole `Pre-release` run passed, creates the stable tag on that commit, and publishes without requalifying. -- Pre-release and stable builds leave the `dev` feature disabled. Release +- Pre-release and stable builds leave the `experimental` feature disabled. Release Dev enables it through `dev-build: true`. Normal workspace builds, lint, - and tests enable `dev` without separate CI lanes for development features. + and tests enable `experimental` without separate CI lanes for development features. ## Setup diff --git a/.github/workflows/branch-checks.yml b/.github/workflows/branch-checks.yml index ae50d955a8..60e4d04432 100644 --- a/.github/workflows/branch-checks.yml +++ b/.github/workflows/branch-checks.yml @@ -273,7 +273,7 @@ jobs: - name: Lint shell: nix develop -c bash -euo pipefail {0} run: | - cargo clippy --locked --workspace --all-targets --features dev -- -D warnings + cargo clippy --locked --workspace --all-targets --features experimental -- -D warnings cargo clippy --locked --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings cargo clippy --locked --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --all-targets -- -D warnings @@ -325,7 +325,7 @@ jobs: env: OPENSHELL_TELEMETRY_ENABLED: "false" run: | - cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,dev + cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,experimental cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml diff --git a/.github/workflows/build-binaries.yml b/.github/workflows/build-binaries.yml index 0973e901ab..cbecea1168 100644 --- a/.github/workflows/build-binaries.yml +++ b/.github/workflows/build-binaries.yml @@ -18,7 +18,7 @@ on: type: string default: "" dev-build: - description: Enable the gateway and supervisor development feature bundle + description: Enable the gateway and supervisor experimental feature bundle required: false type: boolean default: false @@ -90,7 +90,7 @@ jobs: env: PACKAGE: openshell-gateway BINARY: openshell-gateway - EXTRA_CARGO_FLAGS: ${{ inputs.dev-build && '--features dev' || '' }} + EXTRA_CARGO_FLAGS: ${{ inputs.dev-build && '--features experimental' || '' }} strategy: matrix: include: @@ -109,8 +109,8 @@ jobs: env: PACKAGE: ${{ matrix.package }} BINARY: ${{ matrix.binary }} - # Only supervisor entries define the development feature bundle. - EXTRA_CARGO_FLAGS: ${{ matrix.package == 'openshell-supervisor' && inputs.dev-build && '--features dev' || '' }} + # Only supervisor entries define the experimental feature bundle. + EXTRA_CARGO_FLAGS: ${{ matrix.package == 'openshell-supervisor' && inputs.dev-build && '--features experimental' || '' }} strategy: matrix: include: diff --git a/.github/workflows/rust-cache.yml b/.github/workflows/rust-cache.yml index be54cac182..1434b051ae 100644 --- a/.github/workflows/rust-cache.yml +++ b/.github/workflows/rust-cache.yml @@ -62,6 +62,6 @@ jobs: shell: nix develop -c bash -euo pipefail {0} run: | cargo test --locked -p openshell-crypto --no-default-features --test independent_backend --no-run - cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,dev --no-run + cargo nextest run --locked --profile ci --workspace --features openshell-server/test-support,experimental --no-run cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml --no-run cargo nextest run --locked --config-file .config/nextest.toml --profile ci --manifest-path tests/testkit/Cargo.toml --no-run diff --git a/CI.md b/CI.md index 94d5370bd6..d5ce66ae82 100644 --- a/CI.md +++ b/CI.md @@ -655,9 +655,9 @@ The bot's full administrator documentation is internal to NVIDIA. The only comma | File | Role | |---|---| -| `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. Rust lint and tests run once with the development configuration (`--features dev`). | +| `.github/workflows/branch-checks.yml` | Required non-E2E checks. Triggers on `push: pull-request/[0-9]+` for PR mirrors and `merge_group` for queued merges. Rust lint and tests run once with the development configuration (`--features experimental`). | | `.github/workflows/branch-e2e.yml` | Standard, GPU, Kubernetes HA, and Kubernetes credential-driver E2E. PR mirror pushes use `test:e2e`, `test:e2e-gpu`, and `test:e2e-kubernetes` labels; merge groups do not run E2E. | -| `.github/workflows/build-binaries.yml`, `build-vm-driver.yml` | Shared binary matrices used by branch and release workflows. The VM driver remains separate because its build consumes the runtime binaries. `dev-build: true` selects `--features dev` for the gateway and supervisor. Release Dev and branch E2E enable it; prerelease and stable builds use the default of `false`. | +| `.github/workflows/build-binaries.yml`, `build-vm-driver.yml` | Shared binary matrices used by branch and release workflows. The VM driver remains separate because its build consumes the runtime binaries. `dev-build: true` selects `--features experimental` for the gateway and supervisor. Release Dev and branch E2E enable it; prerelease and stable builds use the default of `false`. | | `.github/workflows/build-images.yml` | Builds and pushes multi-platform images, then uploads the same OCI images as workflow artifacts. | | `.github/workflows/package-release-binaries.yml` | Packages raw build artifacts into release tarballs without rebuilding them. | | `.github/workflows/e2e-docker-test.yml`, `e2e-podman-test.yml`, `e2e-vm-test.yml`, `e2e-kubernetes-test.yml` | Reusable runtime lanes called directly by branch and release workflows. Callers select suites and declare only the artifacts each runtime consumes. | diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 25b863a287..8cee75c1b9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -307,8 +307,8 @@ mise run gateway ``` The normal Rust workspace build, check, lint, and test tasks enable the -`dev` feature bundle. CI uses the same development configuration. Use -`--features dev` to select it in direct Cargo builds; Cargo's `dev` profile +`experimental` feature bundle. CI uses the same development configuration. Use +`--features experimental` to select it in direct Cargo builds; Cargo's `dev` profile does not enable it automatically. Prerelease and stable builds leave it off. ## Building the `openshell` CLI diff --git a/crates/openshell-gateway/Cargo.toml b/crates/openshell-gateway/Cargo.toml index 75d3eae25a..c7925038ed 100644 --- a/crates/openshell-gateway/Cargo.toml +++ b/crates/openshell-gateway/Cargo.toml @@ -41,8 +41,8 @@ openshell-driver-mxc = { path = "../openshell-driver-mxc", optional = true } [features] default = ["telemetry", "in-tree-compute-drivers"] -# Opt-in bundle for development-only features; stable builds leave it off. -dev = ["openshell-server/dev"] +# Opt-in bundle for experimental features; stable builds leave it off. +experimental = ["openshell-server/experimental"] in-tree-compute-drivers = [ "compute-driver-docker", "compute-driver-kubernetes", diff --git a/crates/openshell-server/Cargo.toml b/crates/openshell-server/Cargo.toml index 1532131a55..a6e91420df 100644 --- a/crates/openshell-server/Cargo.toml +++ b/crates/openshell-server/Cargo.toml @@ -120,8 +120,8 @@ notify = "8" [features] default = ["telemetry"] -# Opt-in bundle for development-only features; stable builds leave it off. -dev = [] +# Opt-in bundle for experimental features; stable builds leave it off. +experimental = [] ## Compile in anonymous telemetry emission (forwards to openshell-core/telemetry). ## On by default; build with `--no-default-features` for a telemetry-free gateway ## that contains no telemetry endpoint, HTTP client, or emission code. diff --git a/crates/openshell-supervisor/Cargo.toml b/crates/openshell-supervisor/Cargo.toml index aa84df9df2..9b5af080ec 100644 --- a/crates/openshell-supervisor/Cargo.toml +++ b/crates/openshell-supervisor/Cargo.toml @@ -54,8 +54,8 @@ uuid = { workspace = true } [features] default = ["telemetry", "bundled-ca-roots"] -# Opt-in bundle for development-only features; stable builds leave it off. -dev = [] +# Opt-in bundle for experimental features; stable builds leave it off. +experimental = [] system-ca-roots = ["telemetry"] defaults-without-telemetry = ["bundled-ca-roots"] telemetry = ["openshell-core/telemetry"] diff --git a/rfc/0014-release-stability/README.md b/rfc/0014-release-stability/README.md index 3df68ba5fc..2548b5e23c 100644 --- a/rfc/0014-release-stability/README.md +++ b/rfc/0014-release-stability/README.md @@ -115,7 +115,7 @@ without a compatibility guarantee. Graduation adds a Stable `v1` package instead of renaming the Experimental package in place. Unreleased features use named compile-time flags such as `unstable-`, -collected under a `dev` compilation flag. Development releases enable them; stable +collected under an `experimental` compilation flag. Development releases enable them; stable releases and pre-releases exclude them from service binaries, the CLI, configuration, and documentation. For now, SDK distributions may include their generated types and client methods when the SDK naming convention communicates diff --git a/tasks/ci.toml b/tasks/ci.toml index 98b49b1d47..0e738035e6 100644 --- a/tasks/ci.toml +++ b/tasks/ci.toml @@ -14,7 +14,7 @@ hide = true ["build:rust:workspace"] description = "Build all Rust crates" -run = "cargo build --workspace --features dev" +run = "cargo build --workspace --features experimental" hide = true ["build:release"] diff --git a/tasks/rust.toml b/tasks/rust.toml index cbe63364a2..c3eb899238 100644 --- a/tasks/rust.toml +++ b/tasks/rust.toml @@ -11,7 +11,7 @@ run = "cargo run --release -p openshell-sandbox --features perf-harness --bin op description = "Check all Rust crates for errors" depends = ["rust:lockfiles:check"] run = [ - "cargo check --workspace --features dev", + "cargo check --workspace --features experimental", "cargo check -p openshell-sandbox --all-targets --features perf-harness", ] run_windows = "powershell -NoProfile -ExecutionPolicy Bypass -File tasks/scripts/windows-msvc.ps1 check native" @@ -27,7 +27,7 @@ hide = true description = "Lint Rust code with Clippy (deny warnings)" depends = ["rust:lockfiles:check"] run = [ - "cargo clippy --workspace --all-targets --features dev -- -D warnings", + "cargo clippy --workspace --all-targets --features experimental -- -D warnings", "cargo clippy -p openshell-sandbox --all-targets --features perf-harness -- -D warnings", "cargo clippy --manifest-path e2e/rust/Cargo.toml --all-targets -- -D warnings", "cargo clippy --manifest-path examples/governance-interceptor/Cargo.toml --all-targets -- -D warnings", diff --git a/tasks/test.toml b/tasks/test.toml index 0859ae2286..2f6646ceb9 100644 --- a/tasks/test.toml +++ b/tasks/test.toml @@ -116,8 +116,8 @@ run = [ "cargo test -p openshell-crypto --no-default-features --test independent_backend", # Run the workspace once without openshell-server so we can run that crate # with test-only helpers enabled. - "cargo test --workspace --exclude openshell-server --features dev", - "cargo test -p openshell-server --features test-support,dev", + "cargo test --workspace --exclude openshell-server --features experimental", + "cargo test -p openshell-server --features test-support,experimental", "cargo nextest run --config-file .config/nextest.toml --manifest-path examples/supervisor-middleware-content-guard/Cargo.toml", "cargo nextest run --config-file .config/nextest.toml --manifest-path tests/testkit/Cargo.toml", ] From 1c0e5390fbffb62fb80b8ded8ff57ff099c9517f Mon Sep 17 00:00:00 2001 From: Drew Newberry Date: Wed, 28 Oct 2026 16:38:35 -0700 Subject: [PATCH 5/5] docs(rfc): preserve release stability proposal Signed-off-by: Drew Newberry --- rfc/0014-release-stability/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rfc/0014-release-stability/README.md b/rfc/0014-release-stability/README.md index 2548b5e23c..3df68ba5fc 100644 --- a/rfc/0014-release-stability/README.md +++ b/rfc/0014-release-stability/README.md @@ -115,7 +115,7 @@ without a compatibility guarantee. Graduation adds a Stable `v1` package instead of renaming the Experimental package in place. Unreleased features use named compile-time flags such as `unstable-`, -collected under an `experimental` compilation flag. Development releases enable them; stable +collected under a `dev` compilation flag. Development releases enable them; stable releases and pre-releases exclude them from service binaries, the CLI, configuration, and documentation. For now, SDK distributions may include their generated types and client methods when the SDK naming convention communicates