Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion .github/workflows/release-dev.yml
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,6 @@ jobs:
source_ref: ${{ github.sha }}
release_version: ${{ needs.compute-versions.outputs.docs_version }}
display_name: Dev
availability: beta
publish: true
secrets:
FERN_TOKEN: ${{ secrets.FERN_TOKEN }}
Expand Down
3 changes: 2 additions & 1 deletion .github/workflows/release-tag.yml
Original file line number Diff line number Diff line change
Expand Up @@ -758,9 +758,10 @@ jobs:
uses: ./.github/workflows/sync-docs.yml
with:
operation: sync
channel: latest
channel: stable
source_ref: ${{ needs.compute-versions.outputs.source_sha }}
release_version: ${{ needs.compute-versions.outputs.semver }}
version_slug: v${{ needs.compute-versions.outputs.semver }}
display_name: Latest (v${{ needs.compute-versions.outputs.semver }})
publish: true
secrets:
Expand Down
5 changes: 5 additions & 0 deletions architecture/build.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ OpenShell builds these main artifacts:
| VM driver/runtime assets | `crates/openshell-driver-vm` |
| Published docs site | `docs/` rendered by Fern config in `fern/` |

Release Tag publishes the same tagged docs commit as an immutable `vX.Y.Z`
snapshot and the mutable `latest` alias. Release Dev updates `dev`. The Fern
selector pins `latest` and `dev`, then lists versioned snapshots newest first.
The current release workflows do not request availability badges.

Workload images are standard OCI images supplied by operators or users.

## Build Features
Expand Down
13 changes: 8 additions & 5 deletions fern/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,17 @@ The generated `docs-website` branch contains the complete production input for F

The automated site uses these version types:

| Version | Source | Update policy | Fern status |
|---|---|---|---|
| `latest` | The newest stable release. | Mutable. A maintenance release older than the current stable release cannot move it backward unless a maintainer explicitly allows a rollback. | No status before v0.1.0. |
| `dev` | The most recent successful Release Dev run from `main`. | Mutable. Automation rejects an older version or the same version from a different commit unless a maintainer explicitly allows a rollback. | Beta. |
| Version | Source | Update policy |
|---|---|---|
| `latest` | The newest stable release. | Mutable. A maintenance release older than the current stable release cannot move it backward unless a maintainer explicitly allows a rollback. |
| `dev` | The most recent successful Release Dev run from `main`. | Mutable. Automation rejects an older version or the same version from a different commit unless a maintainer explicitly allows a rollback. |
| `vX.Y.Z` | The tagged release commit. | Immutable. A later sync cannot replace its source commit. |

Release Dev waits for the development artifacts and Helm chart, then calls `.github/workflows/sync-docs.yml` once. The reusable workflow updates `dev`, validates the generated site, commits and pushes the branch when needed, and publishes the production site once.

Release Tag follows the same sequence for a non-prerelease tag after the release artifacts, SDK package, Helm chart, and wheel publication complete. It updates `latest` when the release is not older than the current version, then publishes the production site once.
Release Tag follows the same sequence for a non-prerelease tag after the release artifacts, SDK package, Helm chart, and wheel publication complete. One sync copies the tagged source into both the immutable `vX.Y.Z` snapshot and `latest` when the release is not older than the current version, then publishes the production site once. The version selector pins `latest` and `dev` first, followed by versioned snapshots in descending version order. Other historical entries follow in their existing order.

The sync workflow still accepts an optional Fern availability badge, but the current Release Dev and Release Tag jobs do not request one. The next `dev` sync replaces the existing `dev` Beta badge with an unbadged entry. Other entries retain their badge setting until that entry is synced or removed.

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.

Expand Down
1 change: 0 additions & 1 deletion fern/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,6 @@ versions:
- display-name: Dev
path: ../docs/index.yml
slug: dev
availability: beta
announcement:
message: '<span style="display: block; padding: 0.375rem 0; text-align: left;"><strong>New in OpenShell 0.1.0:</strong> a stable release cadence, new isolation primitives, an expanded extension surface, and new APIs. <a href="https://docs.nvidia.com/openshell/dev/upgrade/0-1-0" target="_blank" rel="noreferrer">Read the 0.1.0 upgrade guide</a>.</span>'

Expand Down
43 changes: 18 additions & 25 deletions tasks/scripts/sync_docs_website.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,8 +91,8 @@ def resolve_display_name(
return slug


def resolve_availability(channel: str, override: str) -> str | None:
availability = override or ("beta" if channel == "dev" else "")
def resolve_availability(override: str) -> str | None:
availability = override
if not availability:
return None
if availability not in VERSION_AVAILABILITIES:
Expand All @@ -110,12 +110,6 @@ def parse_release_version(value: str) -> Version:
raise ValueError(f"invalid release version: {value}") from exc


def default_stable_availability(release_version: str) -> str | None:
if parse_release_version(release_version) >= Version("0.1.0"):
return "stable"
return None


def ensure_existing(path: Path, label: str) -> None:
if not path.exists():
raise FileNotFoundError(f"{label} does not exist: {path}")
Expand Down Expand Up @@ -340,18 +334,20 @@ def ordered_entries(
) -> list[VersionEntry]:
by_slug = {entry.slug: entry for entry in existing}
by_slug[updated.slug] = updated
existing_order = [entry.slug for entry in existing if entry.slug != updated.slug]
pinned = [by_slug[slug] for slug in ("latest", "dev") if slug in by_slug]

order: list[str] = []
for slug in ("latest", "dev"):
if slug in by_slug:
order.append(slug)
for slug in existing_order:
if slug not in order and slug in by_slug:
order.append(slug)
if updated.slug not in order:
order.append(updated.slug)
return [by_slug[slug] for slug in order]
versioned: list[tuple[Version, VersionEntry]] = []
other: list[VersionEntry] = []
for entry in by_slug.values():
if entry.slug in {"latest", "dev"}:
continue
try:
versioned.append((parse_release_version(entry.slug), entry))
except ValueError:
other.append(entry)

versioned.sort(key=lambda item: item[0], reverse=True)
return pinned + [entry for _, entry in versioned] + other


def render_versions(entries: list[VersionEntry]) -> list[YamlMapping]:
Expand Down Expand Up @@ -492,7 +488,7 @@ def sync_docs(args: argparse.Namespace) -> None:
)
slug = resolve_slug(channel, version_slug)
display_name = resolve_display_name(channel, slug, source_ref, display_override)
availability = resolve_availability(channel, availability_override)
availability = resolve_availability(availability_override)
metadata_path = target_fern / SNAPSHOT_METADATA_FILE
snapshots = read_snapshot_metadata(metadata_path)
docs_yml = target_fern / "docs.yml"
Expand All @@ -503,9 +499,6 @@ def sync_docs(args: argparse.Namespace) -> None:
if slug != expected_slug:
raise ValueError(f"stable version slug must be {expected_slug}, got {slug}")
ensure_immutable_snapshot(snapshots, target_fern, slug, source_sha)
stable_availability = availability or default_stable_availability(
release_version
)
write_snapshot(
source_docs,
source_fern,
Expand All @@ -514,7 +507,7 @@ def sync_docs(args: argparse.Namespace) -> None:
slug=slug,
display_name=slug,
path=f"./versions/{slug}.yml",
availability=stable_availability,
availability=availability,
announcement=source_version_announcement(
source_fern / "docs.yml", slug
),
Expand Down Expand Up @@ -543,7 +536,7 @@ def sync_docs(args: argparse.Namespace) -> None:
slug="latest",
display_name=display_override or f"Latest ({slug})",
path="./versions/latest.yml",
availability=stable_availability,
availability=availability,
announcement=source_version_announcement(
source_fern / "docs.yml", "latest"
),
Expand Down
98 changes: 79 additions & 19 deletions tasks/scripts/sync_docs_website_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def test_release_workflows_sync_and_publish_docs_once() -> None:
)
assert dev_job["with"]["publish"] == "true"
assert dev_job["with"]["display_name"] == "Dev"
assert dev_job["with"]["availability"] == "beta"
assert "availability" not in dev_job["with"]

assert tag_job["needs"] == [
"compute-versions",
Expand All @@ -58,12 +58,15 @@ def test_release_workflows_sync_and_publish_docs_once() -> None:
"trigger-wheel-publish",
]
assert tag_job["uses"] == "./.github/workflows/sync-docs.yml"
assert tag_job["with"]["channel"] == "latest"
assert tag_job["with"]["channel"] == "stable"
assert (
tag_job["with"]["release_version"]
== "${{ needs.compute-versions.outputs.semver }}"
)
assert "version_slug" not in tag_job["with"]
assert (
tag_job["with"]["version_slug"]
== "v${{ needs.compute-versions.outputs.semver }}"
)
assert (
tag_job["with"]["display_name"]
== "Latest (v${{ needs.compute-versions.outputs.semver }})"
Expand Down Expand Up @@ -139,12 +142,11 @@ def test_resolve_display_name() -> None:


def test_resolve_availability() -> None:
assert sdw.resolve_availability("dev", "") == "beta"
assert sdw.resolve_availability("latest", "") is None
assert sdw.resolve_availability("version", "") is None
assert sdw.resolve_availability("version", "deprecated") == "deprecated"
assert sdw.resolve_availability("") is None
assert sdw.resolve_availability("beta") == "beta"
assert sdw.resolve_availability("deprecated") == "deprecated"
with pytest.raises(ValueError):
sdw.resolve_availability("dev", "alpha")
sdw.resolve_availability("alpha")


def test_parse_and_render_versions_preserves_version_settings() -> None:
Expand Down Expand Up @@ -266,12 +268,35 @@ def test_source_version_announcement_maps_single_source_version_to_channel(

def test_ordered_entries_pins_latest_then_dev() -> None:
existing = [
sdw.VersionEntry("v0.0.36", "v0.0.36", "./versions/v0.0.36.yml"),
sdw.VersionEntry("v0.0.116", "v0.0.116", "./versions/v0.0.116.yml"),
sdw.VersionEntry("dev", "dev", "./versions/dev.yml"),
sdw.VersionEntry("v1.4.0", "v1.4.0", "./versions/v1.4.0.yml"),
sdw.VersionEntry("v1.4.2", "v1.4.2", "./versions/v1.4.2.yml"),
sdw.VersionEntry("v1.4.1", "v1.4.1", "./versions/v1.4.1.yml"),
sdw.VersionEntry("legacy", "legacy", "./versions/legacy.yml"),
]
updated = sdw.VersionEntry("latest", "Latest", "./versions/latest.yml")
ordered = [entry.slug for entry in sdw.ordered_entries(existing, updated)]
assert ordered == ["latest", "dev", "v0.0.36"]
assert ordered == [
"latest",
"dev",
"v1.4.2",
"v1.4.1",
"v1.4.0",
"v0.0.116",
"legacy",
]

refreshed = sdw.VersionEntry("v1.4.1", "v1.4.1", "./versions/v1.4.1.yml")
refreshed_order = [entry.slug for entry in sdw.ordered_entries(existing, refreshed)]
assert refreshed_order == [
"dev",
"v1.4.2",
"v1.4.1",
"v1.4.0",
"v0.0.116",
"legacy",
]


def test_prefix_navigation_paths() -> None:
Expand Down Expand Up @@ -398,7 +423,7 @@ def test_sync_docs_scopes_version_announcements_to_updated_channel(
release_version="0.0.117.dev56",
version_slug="",
display_name="Dev",
availability="beta",
availability="",
)
)

Expand All @@ -421,7 +446,6 @@ def test_sync_docs_scopes_version_announcements_to_updated_channel(
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"availability": "beta",
"announcement": {"message": "OpenShell 0.1.0 is coming soon."},
},
]
Expand Down Expand Up @@ -463,7 +487,9 @@ def test_sync_docs_scopes_version_announcements_to_updated_channel(
assert versions[1]["announcement"] == {"message": "OpenShell 0.1.0 is coming soon."}


def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
def test_sync_docs_clears_updated_badge_and_preserves_other_badges(
tmp_path: Path,
) -> None:
source = tmp_path / "source"
website = tmp_path / "docs-website"
_make_source_tree(source)
Expand All @@ -473,12 +499,18 @@ def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
yaml.safe_dump(
{
"versions": [
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
"availability": "beta",
},
{
"display-name": "v0.0.36",
"path": "./versions/v0.0.36.yml",
"slug": "v0.0.36",
"availability": "deprecated",
}
},
]
}
),
Expand All @@ -496,7 +528,7 @@ def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
release_version="0.0.117.dev56",
version_slug="",
display_name="Dev (v0.0.117.dev56)",
availability="beta",
availability="",
)
)

Expand All @@ -506,7 +538,6 @@ def test_sync_docs_preserves_other_version_availability(tmp_path: Path) -> None:
"display-name": "Dev (v0.0.117.dev56)",
"path": "./versions/dev.yml",
"slug": "dev",
"availability": "beta",
},
{
"display-name": "v0.0.36",
Expand Down Expand Up @@ -589,6 +620,30 @@ def test_stable_sync_creates_immutable_version_and_promotes_latest(
website = tmp_path / "docs-website"
_make_source_tree(source)
_make_docs_website_tree(website)
(website / "fern" / "docs.yml").write_text(
yaml.safe_dump(
{
"versions": [
{
"display-name": "Latest (v0.0.116)",
"path": "./versions/latest.yml",
"slug": "latest",
},
{
"display-name": "Dev",
"path": "./versions/dev.yml",
"slug": "dev",
},
{
"display-name": "v0.0.116",
"path": "./versions/v0.0.116.yml",
"slug": "v0.0.116",
},
]
}
),
encoding="utf-8",
)

sdw.sync_docs(
Namespace(
Expand All @@ -609,10 +664,14 @@ def test_stable_sync_creates_immutable_version_and_promotes_latest(
assert (fern / "pages-v0.2.0" / "intro.mdx").is_file()
assert (fern / "pages-latest" / "intro.mdx").is_file()
versions = read_yaml(fern / "docs.yml")["versions"]
assert [entry["slug"] for entry in versions] == ["latest", "v0.2.0"]
assert [entry["slug"] for entry in versions] == [
"latest",
"dev",
"v0.2.0",
"v0.0.116",
]
assert versions[0]["display-name"] == "Latest (v0.2.0)"
assert versions[0]["availability"] == "stable"
assert versions[1]["availability"] == "stable"
assert all("availability" not in entry for entry in versions)
snapshots = read_yaml(fern / sdw.SNAPSHOT_METADATA_FILE)["snapshots"]
assert snapshots["latest"] == {
"source-ref": "v0.2.0",
Expand Down Expand Up @@ -892,6 +951,7 @@ def test_remove_docs_drops_snapshot(tmp_path: Path) -> None:
fern = website / "fern"
assert (fern / "pages-v0.0.36").is_dir()
assert (fern / "versions" / "v0.0.36.yml").is_file()
assert read_yaml(fern / "docs.yml")["versions"][0]["availability"] == "deprecated"

sdw.remove_docs(
Namespace(
Expand Down
Loading