From d4a26219208ae9a08ba7982d729bcdfb2bf357ba Mon Sep 17 00:00:00 2001 From: Pete MacKinnon Date: Mon, 17 Aug 2026 10:33:59 -0400 Subject: [PATCH 1/7] docs: add Fern documentation scaffolding Add fern/ directory with NVIDIA global theme, docs.yml config, and docs/index.yml navigation covering getting-started, architecture, user-guide, operations, observability, and security sections. contributing/ and designs/ excluded from published nav. Four GitHub Actions workflows added: fern check CI, preview build, preview comment, and publish on docs/v* tags. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Pete MacKinnon --- .github/workflows/fern-docs-ci.yml | 57 ++++++++ .github/workflows/fern-docs-preview-build.yml | 53 +++++++ .../workflows/fern-docs-preview-comment.yml | 133 ++++++++++++++++++ .github/workflows/publish-fern-docs.yml | 81 +++++++++++ docs/index.yml | 57 ++++++++ fern/.gitignore | 2 + fern/docs.yml | 32 +++++ fern/fern.config.json | 4 + 8 files changed, 419 insertions(+) create mode 100644 .github/workflows/fern-docs-ci.yml create mode 100644 .github/workflows/fern-docs-preview-build.yml create mode 100644 .github/workflows/fern-docs-preview-comment.yml create mode 100644 .github/workflows/publish-fern-docs.yml create mode 100644 docs/index.yml create mode 100644 fern/.gitignore create mode 100644 fern/docs.yml create mode 100644 fern/fern.config.json diff --git a/.github/workflows/fern-docs-ci.yml b/.github/workflows/fern-docs-ci.yml new file mode 100644 index 000000000..67f481648 --- /dev/null +++ b/.github/workflows/fern-docs-ci.yml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Fern docs (check) + +on: + pull_request: + paths: + - 'docs/**' + - 'fern/**' + - '.github/workflows/fern-docs-ci.yml' + +permissions: + contents: read + +jobs: + check: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Fern CLI + run: | + VERSION=$(jq -r .version fern/fern.config.json) + if [[ "$VERSION" != "latest" ]] && ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::fern.config.json .version must be 'latest' or a semver string" + exit 1 + fi + npm install -g "fern-api@${VERSION}" + + - name: Check MDX safety + run: | + BAD=$(grep -rn ']*[^/]>\|' docs/ fern/ --include="*.md" --include="*.mdx" || true) + if [ -n "$BAD" ]; then + echo "::error::Non-self-closing tags found (MDX requires ):" + echo "$BAD" + exit 1 + fi + + - name: Fern check + working-directory: ./fern + run: fern check + + - name: Check MDX validity + working-directory: ./fern + run: fern docs md check + + - name: Check broken links + working-directory: ./fern + run: fern docs broken-links diff --git a/.github/workflows/fern-docs-preview-build.yml b/.github/workflows/fern-docs-preview-build.yml new file mode 100644 index 000000000..97606c3e2 --- /dev/null +++ b/.github/workflows/fern-docs-preview-build.yml @@ -0,0 +1,53 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Workflow 1 of 2 for Fern doc previews. +# +# Collects the fern/ and docs/ sources and PR metadata from the (possibly +# untrusted) PR branch and uploads them as an artifact. No secrets are used +# here, so this is safe to run on fork PRs via the regular pull_request trigger. +# +# The companion workflow (fern-docs-preview-comment.yml) picks up the artifact, +# builds the preview with DOCS_FERN_TOKEN, and posts the PR comment. + +name: "Preview Fern Docs: Build" + +on: + pull_request: + paths: + - 'docs/**' + - 'fern/**' + - '.github/workflows/fern-docs-preview-build.yml' + +permissions: + contents: read + +jobs: + collect: + runs-on: ubuntu-latest + steps: + - name: Checkout PR + uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Save PR metadata + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + HEAD_REF: ${{ github.head_ref }} + BASE_REF: ${{ github.base_ref }} + run: | + mkdir -p preview-metadata + echo "$PR_NUMBER" > preview-metadata/pr_number + echo "$HEAD_REF" > preview-metadata/head_ref + git diff --name-only "origin/${BASE_REF}...HEAD" -- '*.md' > preview-metadata/changed_md_files 2>/dev/null || true + + - name: Upload fern sources and metadata + uses: actions/upload-artifact@v4 + with: + name: fern-preview + path: | + fern/ + docs/ + preview-metadata/ + retention-days: 1 diff --git a/.github/workflows/fern-docs-preview-comment.yml b/.github/workflows/fern-docs-preview-comment.yml new file mode 100644 index 000000000..b0ffcb8ce --- /dev/null +++ b/.github/workflows/fern-docs-preview-comment.yml @@ -0,0 +1,133 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Workflow 2 of 2 for Fern doc previews. +# +# Triggered by workflow_run after "Preview Fern Docs: Build" completes. +# Downloads the fern/ artifact, builds a preview with DOCS_FERN_TOKEN, and +# posts a stable :herb: comment on the PR. This workflow never checks out the +# PR branch directly, keeping secrets isolated from untrusted code. +# +# Required configuration: +# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) + +name: "Preview Fern Docs: Comment" + +on: + workflow_run: + workflows: ["Preview Fern Docs: Build"] + types: [completed] + +permissions: + pull-requests: write + actions: read + +jobs: + preview: + runs-on: ubuntu-latest + if: ${{ github.event.workflow_run.conclusion == 'success' }} + steps: + - name: Download fern sources and metadata + uses: actions/download-artifact@v4 + with: + name: fern-preview + run-id: ${{ github.event.workflow_run.id }} + github-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Read PR metadata + id: metadata + run: | + echo "pr_number=$(cat preview-metadata/pr_number)" >> "$GITHUB_OUTPUT" + echo "head_ref=$(cat preview-metadata/head_ref)" >> "$GITHUB_OUTPUT" + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Fern CLI + run: | + VERSION=$(jq -r .version fern/fern.config.json) + if [[ "$VERSION" != "latest" ]] && ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then + echo "::error::fern.config.json .version must be 'latest' or a semver string" + exit 1 + fi + npm install -g "fern-api@${VERSION}" + + - name: Generate preview URL + id: generate-docs + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + HEAD_REF: ${{ steps.metadata.outputs.head_ref }} + working-directory: ./fern + run: | + OUTPUT=$(fern generate --docs --preview --id "$HEAD_REF" 2>&1) + echo "$OUTPUT" + URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()') + if [ -z "$URL" ]; then + echo "::error::Failed to generate preview URL. See fern output above." + exit 1 + fi + echo "preview_url=$URL" >> "$GITHUB_OUTPUT" + + - name: Build page links for changed files + id: page-links + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }} + run: | + CHANGED_FILES="" + if [ -f preview-metadata/changed_md_files ]; then + CHANGED_FILES=$(cat preview-metadata/changed_md_files) + fi + + if [ -z "$CHANGED_FILES" ] || [ -z "$PREVIEW_URL" ]; then + echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0 + fi + + BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+') + FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//' \ + | python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=',/'))") + RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || { + echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0 + } + + PAGE_LINKS=$(echo "$RESPONSE" | jq -r --arg url "$BASE_URL" \ + '.mappings[] | select(.slug != null) | "- [\(.slug)](\($url)/\(.slug))"') + + if [ -n "$PAGE_LINKS" ]; then + { echo "page_links<> "$GITHUB_OUTPUT" + else + echo "page_links=" >> "$GITHUB_OUTPUT" + fi + + - name: Post or update PR comment + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + PR_NUMBER: ${{ steps.metadata.outputs.pr_number }} + PREVIEW_URL: ${{ steps.generate-docs.outputs.preview_url }} + PAGE_LINKS: ${{ steps.page-links.outputs.page_links }} + run: | + BODY=":herb: **Preview your docs:** <${PREVIEW_URL}>" + if [ -n "${PAGE_LINKS}" ]; then + BODY="${BODY} + + Here are the markdown pages you've updated: + ${PAGE_LINKS}" + fi + + MARKER="" + BODY="${BODY} + + ${MARKER}" + + COMMENT_ID=$(gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + --jq ".[] | select(.body | contains(\"${MARKER}\")) | .id" | tr -d '\r' | head -1) + + if [ -n "$COMMENT_ID" ]; then + gh api "repos/${{ github.repository }}/issues/comments/${COMMENT_ID}" \ + -X PATCH -f body="$BODY" + else + gh api "repos/${{ github.repository }}/issues/${PR_NUMBER}/comments" \ + -f body="$BODY" + fi diff --git a/.github/workflows/publish-fern-docs.yml b/.github/workflows/publish-fern-docs.yml new file mode 100644 index 000000000..35aacc89e --- /dev/null +++ b/.github/workflows/publish-fern-docs.yml @@ -0,0 +1,81 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Publishes the Fern documentation site when a docs tag is pushed or manually triggered. +# +# To publish: git tag docs/v1.0.0 && git push origin docs/v1.0.0 +# Or use the "Run workflow" button in the Actions tab. +# +# Required configuration: +# - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) + +name: Publish Fern Docs + +on: + push: + tags: + - 'docs/v*' + workflow_dispatch: {} + +permissions: + contents: read + +concurrency: + group: fern-publish + cancel-in-progress: true + +jobs: + publish: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v7 + with: + fetch-tags: true + + - name: Checkout frozen version content + run: | + set -eo pipefail + for version_file in fern/versions/v*.yml; do + [ -e "$version_file" ] || continue + version=$(basename "$version_file" .yml) + if git rev-parse "$version" >/dev/null 2>&1; then + mkdir -p "fern/versions/${version}-content" + git archive "$version" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" + find "fern/versions/${version}-content" -name '*.md' -exec sed -i 's/]*[^/]\)>//g' {} + + echo "Extracted docs from $version" + else + echo "::warning::Tag $version not found — skipping content checkout" + fi + done + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: Install Fern CLI + run: | + VERSION=$(jq -r .version fern/fern.config.json) + npm install -g "fern-api@${VERSION}" + + - name: Stamp version in docs.yml + run: | + VERSION=$(curl -sf https://api.github.com/repos/${{ github.repository }}/releases/latest | jq -r .tag_name) + if [ -n "$VERSION" ] && [ "$VERSION" != "null" ]; then + sed -i 's/display-name: NodeWright/display-name: "NodeWright · '"${VERSION}"'"/' fern/docs.yml + fi + echo "--- docs.yml products after stamp ---" + grep "display-name:" fern/docs.yml + + - name: Publish Docs + env: + FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} + working-directory: ./fern + run: | + set -o pipefail + fern generate --docs 2>&1 | tee /tmp/fern-output.log + URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true) + if [ -n "$URL" ]; then + echo "### Published: [$URL]($URL)" >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/docs/index.yml b/docs/index.yml new file mode 100644 index 000000000..77ec4db54 --- /dev/null +++ b/docs/index.yml @@ -0,0 +1,57 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +navigation: + - section: Getting Started + contents: + - page: Overview + path: getting-started/overview.md + - page: Installation + path: getting-started/installation.md + - page: Migration from Skyhook + path: getting-started/migration.md + + - section: Architecture + contents: + - page: Operator Status Definitions + path: architecture/operator-status.md + - page: Interrupt Flow + path: architecture/interrupt-flow.md + - page: Strict Ordering + path: architecture/ordering.md + + - section: User Guide + contents: + - page: CLI Reference + path: user-guide/cli.md + - page: Deployment Policy + path: user-guide/deployment-policy.md + - page: Providing Secrets + path: user-guide/providing-secrets.md + - page: Runtime Required + path: user-guide/runtime-required.md + - page: Taints + path: user-guide/taints.md + - page: Uninstall + path: user-guide/uninstall.md + + - section: Operations + contents: + - page: Kubernetes Support + path: operations/kubernetes-support.md + - page: Resource Management + path: operations/resource-management.md + - page: Resources at Scale + path: operations/resources-at-scale.md + - page: Versioning + path: operations/versioning.md + + - section: Observability + contents: + - page: Metrics + path: observability/metrics.md + + - section: Security + contents: + - page: Kyverno Policies + path: security/kyverno/README.md diff --git a/fern/.gitignore b/fern/.gitignore new file mode 100644 index 000000000..ff9bdf6fd --- /dev/null +++ b/fern/.gitignore @@ -0,0 +1,2 @@ +.fern/ +node_modules/ diff --git a/fern/docs.yml b/fern/docs.yml new file mode 100644 index 000000000..095b45b25 --- /dev/null +++ b/fern/docs.yml @@ -0,0 +1,32 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +instances: + - url: nvidia-nodewright.docs.buildwithfern.com/nodewright + custom-domain: docs.nvidia.com/nodewright + multi-source: true + +redirects: + - source: /nodewright/index.html + destination: /nodewright + +title: NVIDIA NodeWright + +global-theme: nvidia + +logo: + href: /nodewright + right-text: NodeWright + +navbar-links: + - type: github + value: https://github.com/NVIDIA/nodewright + +experimental: + mdx-components: + - ./components + +products: + - display-name: NodeWright + path: ../docs/index.yml + slug: / diff --git a/fern/fern.config.json b/fern/fern.config.json new file mode 100644 index 000000000..2e340abf2 --- /dev/null +++ b/fern/fern.config.json @@ -0,0 +1,4 @@ +{ + "organization": "nvidia", + "version": "latest" +} From c740db746353125af612ba265706886f1d3f4bd3 Mon Sep 17 00:00:00 2001 From: Pete MacKinnon Date: Mon, 17 Aug 2026 10:45:08 -0400 Subject: [PATCH 2/7] docs: replace thin getting-started pages with real content Extract overview and installation content from root README into docs/getting-started/ so pages render in Fern local preview. The README keeps its content as-is for GitHub visitors. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Pete MacKinnon --- docs/getting-started/installation.md | 55 ++++++++++++++++++++++++++-- docs/getting-started/overview.md | 55 ++++++++++++++++++++++------ 2 files changed, 96 insertions(+), 14 deletions(-) diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 419daf16d..41abdb8cc 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,19 +2,68 @@ NodeWright is installed via Helm as an OCI artifact from GitHub Container Registry. -For prerequisites, install commands, and configuration options, see the [project README](../../README.md#installation-via-helm). +## Prerequisites -## Quick install +- Kubernetes cluster (tested on v1.30+) +- Helm 3.x installed +- Container registry access credentials (if using private registries) + +## Install NodeWright ```bash +# The chart is distributed as an OCI artifact on GitHub Container Registry. +# Helm 3.8+ supports OCI natively — no `helm repo add` needed. helm install nodewright oci://ghcr.io/nvidia/nodewright/charts/nodewright \ --version v0.17.1 \ --namespace nodewright \ --create-namespace ``` +> **Where things live:** chart at `oci://ghcr.io/nvidia/nodewright/charts/nodewright`, operator image at `ghcr.io/nvidia/nodewright/operator`, agent image at `ghcr.io/nvidia/nodewright/agent`. + +> **Migrating from `helm repo add skyhook https://helm.ngc.nvidia.com/...`?** Run `helm repo remove skyhook` and use the OCI install above. + +## Configure Image Pull Secrets (if needed) + +If you're using private container registries, create the necessary secrets: + +```bash +kubectl create secret generic node-init-secret \ + --from-file=.dockerconfigjson=${HOME}/.docker/config.json \ + --type=kubernetes.io/dockerconfigjson \ + --namespace nodewright +``` + +NodeWright currently uses a single shared image pull secret for all packages, and agent/operator containers. If you need access to multiple registries, combine the credentials into one `dockerconfigjson` secret with multiple registry auths. + +## Verify Installation + +```bash +# Check that the operator is running +kubectl get pods -n nodewright + +# Wait for the deployment to be available +kubectl wait --for=condition=Available deployment -l control-plane=controller-manager -n nodewright --timeout=300s + +# Wait for the operator pod to be ready +kubectl wait --for=condition=Ready pod -l control-plane=controller-manager -n nodewright --timeout=300s + +# Verify the CRDs are installed +kubectl get crd | grep nodewright +``` + +## Uninstalling + +By default, the Helm chart includes a pre-delete hook that automatically cleans up all NodeWright and DeploymentPolicy resources before uninstalling: + +```bash +helm uninstall nodewright --namespace nodewright +``` + +For more details on explicit package uninstall, see [Uninstall](../user-guide/uninstall.md). + ## Related - [Kubernetes support matrix](../operations/kubernetes-support.md) - [Versioning](../operations/versioning.md) -- [Uninstall](../user-guide/uninstall.md) +- [Migration from Skyhook](migration.md) diff --git a/docs/getting-started/overview.md b/docs/getting-started/overview.md index 3be032021..7da83f64a 100644 --- a/docs/getting-started/overview.md +++ b/docs/getting-started/overview.md @@ -1,19 +1,52 @@ # NodeWright Overview -NodeWright is a Kubernetes-aware package manager for cluster administrators to safely modify and maintain underlying hosts declaratively at scale. +**NodeWright** is a Kubernetes-aware package manager for cluster administrators to safely modify and maintain underlying hosts declaratively at scale. -For a full introduction — what NodeWright is, why it exists, where it fits, and its key features — see the [project README](../../README.md#what-is-nodewright). +## Why NodeWright? -## Components +Managing and updating Kubernetes clusters is challenging. While Kubernetes advocates treating compute as disposable, certain scenarios make this difficult: -1. **NodeWright Operator** — manages installing, updating, and removing packages -2. **NodeWright Custom Resource** — declarative definitions of changes to apply -3. **Packages** — the actual modifications you want to implement +- **Updating hosts without re-imaging:** Limited excess hardware/capacity for rolling replacements, or long node replacement times (can be hours in some cloud providers). +- **OS image management:** Maintain a common base image with workload-specific overlays instead of multiple OS images. +- **Workload sensitivity:** Some workloads can't be moved, are difficult to move, or take a long time to migrate. -Pre-built packages are available at [NVIDIA/skyhook-packages](https://github.com/NVIDIA/skyhook-packages). +## What is NodeWright? -## Next steps +NodeWright functions like a package manager but for your entire Kubernetes cluster, with three main components: -- [Installation](installation.md) — install NodeWright via Helm -- [Migration from Skyhook](migration.md) — rename transition guide -- [CLI reference](../user-guide/cli.md) — `kubectl nodewright` commands +1. **NodeWright Operator** — Manages installing, updating, and removing packages. +2. **NodeWright Custom Resource** — Declarative definitions of changes to apply. +3. **Packages** — The actual modifications you want to implement. + +NodeWright works in any Kubernetes environment (self-managed, on-prem, cloud) and shines when you need: + +- Kubernetes-aware scheduling that protects important workloads +- Rolling or simultaneous updates across your cluster +- Declarative configuration management for host-level changes + +## Benefits + +- **Native Kubernetes integration** — Packages are standard Kubernetes resources compatible with GitOps tools like ArgoCD, Helm, and Flux. +- **Autoscaling support** — Ensure newly created nodes are properly configured before schedulable. +- **First-class upgrades** — Deploys changes with minimal disruption, waiting for running workloads to complete when needed. + +## Key Features + +- **Interruption Budget:** percent of nodes or count +- **Node Selectors:** selectors for which nodes to apply to (node labels) +- **Pod Non Interrupt Labels:** labels for pods to **never** interrupt +- **Package Interrupt:** service (containerd, cron, anything systemd), or reboot +- **Additional Tolerations:** tolerations added to the packages +- [**Runtime Required**](../user-guide/runtime-required.md): requires node to come into the cluster with a taint, and will do work prior to removing custom taint +- [**Resource Management**](../operations/resource-management.md): CPU/memory resources using LimitRange, per-package overrides, and validation rules +- [**Explicit Uninstall**](../user-guide/uninstall.md): controlled uninstall of packages from nodes with webhook guards, finalizer-driven cleanup, and cancel support + +## Pre-built Packages + +There are pre-built generalist packages available at [NVIDIA/skyhook-packages](https://github.com/NVIDIA/skyhook-packages). + +## Next Steps + +- [Installation](installation.md) — Install NodeWright via Helm +- [Migration from Skyhook](migration.md) — Transition guide from Skyhook to NodeWright +- [CLI Reference](../user-guide/cli.md) — `kubectl nodewright` commands From de5b601e958fa80d243ae0083873c492da6de703 Mon Sep 17 00:00:00 2001 From: Pete MacKinnon Date: Mon, 17 Aug 2026 17:42:55 -0400 Subject: [PATCH 3/7] docs: align Fern CI workflows with canonical skill templates Replace hand-written workflows with the github-markdown-to-fern skill templates, adapted for NodeWright's action-ref convention (@v7/@v4). Adds full version registration/pruning/auto-PR on release, MDX sanitization of frozen content, stricter validation, and concurrency groups. Fixes BASE_URL stripping instance path in preview links, removes nonexistent mdx-components config, and fixes MD028 lint. Co-Authored-By: Claude Opus 4.6 Signed-off-by: Pete MacKinnon --- .github/workflows/fern-docs-ci.yml | 91 ++++++- .github/workflows/fern-docs-preview-build.yml | 93 ++++++- .../workflows/fern-docs-preview-comment.yml | 31 ++- .github/workflows/publish-fern-docs.yml | 236 ++++++++++++++++-- docs/getting-started/installation.md | 1 - fern/docs.yml | 4 - 6 files changed, 404 insertions(+), 52 deletions(-) diff --git a/.github/workflows/fern-docs-ci.yml b/.github/workflows/fern-docs-ci.yml index 67f481648..b22e6ad4b 100644 --- a/.github/workflows/fern-docs-ci.yml +++ b/.github/workflows/fern-docs-ci.yml @@ -1,7 +1,21 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -name: Fern docs (check) +# Validates Fern docs configuration on pull requests that touch docs or fern/. + +name: Fern Docs CI on: pull_request: @@ -9,31 +23,87 @@ on: - 'docs/**' - 'fern/**' - '.github/workflows/fern-docs-ci.yml' + workflow_dispatch: {} + ## --- OPTION: NVIDIA copy-PR-bot trigger --- + ## Replace the pull_request block above with: + # push: + # branches: + # - "pull-request/[0-9]+" + # paths: + # - 'docs/**' + # - 'fern/**' + # - '.github/workflows/fern-docs-ci.yml' + ## and add the changed-files gating job below (uncomment the full block). permissions: contents: read jobs: - check: + ## --- OPTION: copy-PR-bot changed-files gate --- + ## Uncomment this job and add "needs: changed-files" + + ## "if: needs.changed-files.outputs.docs == 'true'" to fern-check when + ## using push-to-pull-request/* triggers without paths: filters. + # + # changed-files: + # runs-on: ubuntu-latest + # outputs: + # docs: ${{ steps.changes.outputs.docs }} + # steps: + # - name: Checkout code + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # with: + # fetch-depth: 0 + # + # - name: Check for docs changes + # id: changes + # run: | + # if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then + # echo "docs=true" >> $GITHUB_OUTPUT + # elif [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then + # echo "docs=true" >> $GITHUB_OUTPUT + # elif git diff --name-only "${{ github.event.before }}" HEAD 2>/dev/null | grep -qE '^(docs/|fern/)'; then + # echo "docs=true" >> $GITHUB_OUTPUT + # else + # echo "docs=false" >> $GITHUB_OUTPUT + # fi + + fern-check: + name: Fern Check runs-on: ubuntu-latest + # runs-on: linux-amd64-cpu8 # NVIDIA self-hosted runner timeout-minutes: 10 steps: - name: Checkout repository uses: actions/checkout@v7 + ## --- OPTION: filename convention check (warning) --- + # - name: Warn on non-lowercase or underscore markdown filenames + # run: | + # find docs/ -name "*.md" | while read f; do + # base=$(basename "$f") + # if echo "$base" | grep -qE '[A-Z]|_'; then + # suggested=$(echo "$base" | tr '[:upper:]' '[:lower:]' | tr '_' '-') + # echo "::warning file=$f::Filename '$base' should be lowercase with hyphens (e.g. '$suggested')" + # fi + # done + + ## --- OPTION: filename convention check (hard fail) --- + # - name: Check markdown filename conventions + # run: | + # BAD=$(find docs/ -name '*.md' | grep -E '[A-Z_]' || true) + # if [ -n "$BAD" ]; then + # echo "::error::Markdown filenames must be lowercase with hyphens only:" + # echo "$BAD" + # exit 1 + # fi + - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '20' - name: Install Fern CLI - run: | - VERSION=$(jq -r .version fern/fern.config.json) - if [[ "$VERSION" != "latest" ]] && ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+ ]]; then - echo "::error::fern.config.json .version must be 'latest' or a semver string" - exit 1 - fi - npm install -g "fern-api@${VERSION}" + run: npm install -g fern-api@$(jq -r .version fern/fern.config.json) - name: Check MDX safety run: | @@ -45,13 +115,10 @@ jobs: fi - name: Fern check - working-directory: ./fern run: fern check - name: Check MDX validity - working-directory: ./fern run: fern docs md check - name: Check broken links - working-directory: ./fern run: fern docs broken-links diff --git a/.github/workflows/fern-docs-preview-build.yml b/.github/workflows/fern-docs-preview-build.yml index 97606c3e2..660eca53f 100644 --- a/.github/workflows/fern-docs-preview-build.yml +++ b/.github/workflows/fern-docs-preview-build.yml @@ -1,11 +1,26 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # Workflow 1 of 2 for Fern doc previews. # -# Collects the fern/ and docs/ sources and PR metadata from the (possibly -# untrusted) PR branch and uploads them as an artifact. No secrets are used -# here, so this is safe to run on fork PRs via the regular pull_request trigger. +# Collects the fern/ and docs/ sources plus PR metadata from the PR branch and +# uploads them as an artifact. Both directories are needed because +# fern/docs.yml references ../docs/index.yml; omitting docs/ causes +# `fern generate --docs` to fail in the companion workflow. No secrets are +# used here, so this is safe to run on fork PRs via the regular pull_request +# trigger. # # The companion workflow (fern-docs-preview-comment.yml) picks up the artifact, # builds the preview with DOCS_FERN_TOKEN, and posts the PR comment. @@ -18,18 +33,61 @@ on: - 'docs/**' - 'fern/**' - '.github/workflows/fern-docs-preview-build.yml' + ## --- OPTION: NVIDIA copy-PR-bot trigger --- + ## Replace the pull_request block above with: + # push: + # branches: + # - "pull-request/[0-9]+" + # paths: + # - 'docs/**' + # - 'fern/**' + # - '.github/workflows/fern-docs-preview-build.yml' + ## and switch the "Save PR metadata" step to the copy-PR-bot variant below. permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: + ## --- OPTION: copy-PR-bot changed-files gate --- + ## Uncomment this job and add "needs: changed-files" + + ## "if: needs.changed-files.outputs.docs == 'true'" to collect when using + ## push-to-pull-request/* triggers without paths: filters. + # + # changed-files: + # runs-on: ubuntu-latest + # outputs: + # docs: ${{ steps.changes.outputs.docs }} + # steps: + # - name: Checkout code + # uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + # with: + # fetch-depth: 0 + # + # - name: Check for docs changes + # id: changes + # run: | + # if [ "${{ github.event.before }}" = "0000000000000000000000000000000000000000" ]; then + # echo "docs=true" >> $GITHUB_OUTPUT + # elif git diff --name-only "${{ github.event.before }}" HEAD 2>/dev/null | grep -qE '^(docs/|fern/)'; then + # echo "docs=true" >> $GITHUB_OUTPUT + # else + # echo "docs=false" >> $GITHUB_OUTPUT + # fi + collect: runs-on: ubuntu-latest + # runs-on: linux-amd64-cpu8 # NVIDIA self-hosted runner + timeout-minutes: 10 steps: - name: Checkout PR uses: actions/checkout@v7 with: fetch-depth: 0 + persist-credentials: false - name: Save PR metadata env: @@ -41,6 +99,35 @@ jobs: echo "$PR_NUMBER" > preview-metadata/pr_number echo "$HEAD_REF" > preview-metadata/head_ref git diff --name-only "origin/${BASE_REF}...HEAD" -- '*.md' > preview-metadata/changed_md_files 2>/dev/null || true + ## --- OPTION: copy-PR-bot metadata extraction --- + ## Replace the step above with this when using push-to-pull-request/*: + # - name: Save PR metadata + # env: + # BRANCH_NAME: ${{ github.ref_name }} + # run: | + # mkdir -p preview-metadata + # echo "${BRANCH_NAME#pull-request/}" > preview-metadata/pr_number + # echo "$BRANCH_NAME" > preview-metadata/head_ref + # git diff --name-only "origin/main...HEAD" -- '*.md' > preview-metadata/changed_md_files 2>/dev/null || true + + - name: Checkout frozen version content + run: | + set -eo pipefail + for version_file in fern/versions/v*.yml; do + [ -f "$version_file" ] || continue + version=$(basename "$version_file" .yml) + if git show-ref --verify --quiet "refs/tags/${version}"; then + mkdir -p "fern/versions/${version}-content" + git archive "refs/tags/${version}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" + find "fern/versions/${version}-content" -name '*.md' -print0 | xargs -0 -r sed -i \ + -e 's/{/\\{/g' \ + -e 's/}/\\}/g' \ + -e 's/&1) - echo "$OUTPUT" - URL=$(echo "$OUTPUT" | grep -oP 'Published docs to \K.*(?= \()') + set -o pipefail + fern generate --docs --preview --id "$HEAD_REF" 2>&1 | tee /tmp/fern-output.log + URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true) if [ -z "$URL" ]; then echo "::error::Failed to generate preview URL. See fern output above." exit 1 @@ -85,7 +104,7 @@ jobs: echo "page_links=" >> "$GITHUB_OUTPUT"; exit 0 fi - BASE_URL=$(echo "$PREVIEW_URL" | grep -oP 'https?://[^/]+') + BASE_URL="${PREVIEW_URL%/}" FILES_PARAM=$(echo "$CHANGED_FILES" | tr '\n' ',' | sed 's/,$//' \ | python3 -c "import sys, urllib.parse; print(urllib.parse.quote(sys.stdin.read().strip(), safe=',/'))") RESPONSE=$(curl -sf -H "FERN_TOKEN: $FERN_TOKEN" "${PREVIEW_URL}/api/fern-docs/get-slug-for-file?files=${FILES_PARAM}" 2>/dev/null) || { diff --git a/.github/workflows/publish-fern-docs.yml b/.github/workflows/publish-fern-docs.yml index 35aacc89e..f3964cdb3 100644 --- a/.github/workflows/publish-fern-docs.yml +++ b/.github/workflows/publish-fern-docs.yml @@ -1,10 +1,35 @@ # SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. # SPDX-License-Identifier: Apache-2.0 +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. -# Publishes the Fern documentation site when a docs tag is pushed or manually triggered. +# Publishes the Fern documentation site on GitHub Release or manual dispatch. +# +# All versions serve frozen content extracted from their git tag — there is no +# "live docs" entry. The newest version is stamped "Latest · vX.Y.Z" at publish +# time so readers know which is current; the stamp is transient and not persisted. # -# To publish: git tag docs/v1.0.0 && git push origin docs/v1.0.0 -# Or use the "Run workflow" button in the Actions tab. +# Manual dispatch accepts an optional `tag` input: +# - empty: resolves to the latest GitHub release tag +# - explicit (e.g. v1.2.0): publishes that tag +# +# On a vX.Y.Z release (publish or dispatch), the workflow also: +# 1. Generates fern/versions/vX.Y.Z.yml from the tag's docs/index.yml +# 2. Adds a version entry to fern/docs.yml, sorted by semver descending +# 3. Prunes older versions to keep only the MAX_VERSIONS most recent +# 4. Opens a PR to persist registry changes back to main (after publish) +# +# Pre-releases trigger publish but skip version registration. # # Required configuration: # - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) @@ -12,63 +37,200 @@ name: Publish Fern Docs on: - push: - tags: - - 'docs/v*' - workflow_dispatch: {} + release: + types: [published] + workflow_dispatch: + inputs: + tag: + description: "Tag to publish (e.g. v1.2.0). Leave empty to use the latest GitHub release tag." + required: false + type: string + ## --- OPTION: tag-push trigger --- + ## Use this instead of `release` for repos that push tags without creating + ## GitHub Releases. The resolve step handles both triggers, but the release + ## trigger is preferred because the Release object always exists at workflow + ## start — no race between tag push and Release creation. + # push: + # tags: + # - "v[0-9]*.[0-9]*.[0-9]*" permissions: - contents: read + contents: write + pull-requests: write concurrency: group: fern-publish cancel-in-progress: true +env: + YQ_VERSION: v4.53.2 + MAX_VERSIONS: 3 + jobs: publish: runs-on: ubuntu-latest + # runs-on: linux-amd64-cpu8 # NVIDIA self-hosted runner + timeout-minutes: 20 steps: - name: Checkout repository uses: actions/checkout@v7 with: + ref: main fetch-tags: true + persist-credentials: false + + - name: Install yq + run: | + if command -v yq &>/dev/null; then + echo "yq already installed: $(yq --version)" + else + OS=$(uname -s | tr '[:upper:]' '[:lower:]') + ARCH=$(uname -m) + case "$ARCH" in + x86_64) ARCH="amd64" ;; + aarch64) ARCH="arm64" ;; + esac + mkdir -p "$HOME/.local/bin" + curl -sSfL "https://github.com/mikefarah/yq/releases/download/${YQ_VERSION}/yq_${OS}_${ARCH}" -o "$HOME/.local/bin/yq" + chmod +x "$HOME/.local/bin/yq" + echo "$HOME/.local/bin" >> "$GITHUB_PATH" + fi + + - name: Resolve target tag + id: resolve + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + INPUT_TAG: ${{ inputs.tag }} + run: | + set -eo pipefail + if [ "${GITHUB_EVENT_NAME}" = "release" ]; then + TAG="${{ github.event.release.tag_name }}" + IS_PRERELEASE="${{ github.event.release.prerelease }}" + elif [ "${GITHUB_EVENT_NAME}" = "push" ]; then + TAG="${GITHUB_REF#refs/tags/}" + elif [ -n "${INPUT_TAG}" ]; then + TAG="${INPUT_TAG}" + else + TAG=$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName -q .tagName) + if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then + echo "::error::Could not determine latest release tag" + exit 1 + fi + fi + if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then + echo "::error::Tag '${TAG}' is not a valid semver tag (expected vMAJOR.MINOR.PATCH[-PRERELEASE])" + exit 1 + fi + if ! git show-ref --verify --quiet "refs/tags/${TAG}"; then + echo "::error::Tag '${TAG}' does not exist in this repository" + exit 1 + fi + # Pre-release detection: use the Release event flag when available, + # fall back to hyphen check for tag-push and dispatch triggers. + if [ "${IS_PRERELEASE}" = "true" ] || [[ "$TAG" == *-* ]]; then + IS_RELEASE=false + else + IS_RELEASE=true + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" + echo "Resolved target tag: $TAG (is_release=$IS_RELEASE)" + + - name: Register new version from tag + if: steps.resolve.outputs.is_release == 'true' + env: + TAG_VERSION: ${{ steps.resolve.outputs.tag }} + run: | + set -eo pipefail + + if [ -f "fern/versions/${TAG_VERSION}.yml" ]; then + echo "Version ${TAG_VERSION} already registered — skipping" + exit 0 + fi + + echo "Generating fern/versions/${TAG_VERSION}.yml from tag $TAG_VERSION" + git archive "refs/tags/${TAG_VERSION}" -- docs/index.yml | tar -xO docs/index.yml | \ + sed "s|path: |path: ${TAG_VERSION}-content/|g" > "fern/versions/${TAG_VERSION}.yml" + + export TAG_VERSION + EXPR='.products[0].versions += [{"display-name": env(TAG_VERSION),' + EXPR+=' "path": "versions/" + env(TAG_VERSION) + ".yml",' + EXPR+=' "slug": env(TAG_VERSION), "availability": "stable"}]' + yq -i "$EXPR" fern/docs.yml + + # Sort all version entries by semver descending + SORTED_SLUGS=$(yq '.products[0].versions[].slug' fern/docs.yml | sort -rV) + yq -i '.products[0].versions = []' fern/docs.yml + for slug in $SORTED_SLUGS; do + export slug + yq -i '.products[0].versions += [{"display-name": env(slug), "path": "versions/" + env(slug) + ".yml", "slug": env(slug), "availability": "stable"}]' fern/docs.yml + done + + echo "--- docs.yml versions after insert + sort ---" + yq '.products[0].versions[].display-name' fern/docs.yml + + - name: Prune old versions + if: steps.resolve.outputs.is_release == 'true' + run: | + set -eo pipefail + TOTAL=$(yq '.products[0].versions | length' fern/docs.yml) + + if [ "$TOTAL" -le "$MAX_VERSIONS" ]; then + echo "Only $TOTAL versions — nothing to prune (keeping $MAX_VERSIONS)" + exit 0 + fi + + PRUNED=$(yq ".products[0].versions[$MAX_VERSIONS:][].slug" fern/docs.yml) + yq -i ".products[0].versions |= .[:$MAX_VERSIONS]" fern/docs.yml + + for slug in $PRUNED; do + rm -f "fern/versions/${slug}.yml" + echo "Pruned $slug" + done + + echo "--- docs.yml versions after prune ---" + yq '.products[0].versions[].display-name' fern/docs.yml - name: Checkout frozen version content run: | set -eo pipefail for version_file in fern/versions/v*.yml; do - [ -e "$version_file" ] || continue + [ -f "$version_file" ] || continue version=$(basename "$version_file" .yml) - if git rev-parse "$version" >/dev/null 2>&1; then + if git show-ref --verify --quiet "refs/tags/${version}"; then mkdir -p "fern/versions/${version}-content" - git archive "$version" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" - find "fern/versions/${version}-content" -name '*.md' -exec sed -i 's/]*[^/]\)>//g' {} + + git archive "refs/tags/${version}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" + find "fern/versions/${version}-content" -name '*.md' -print0 | xargs -0 -r sed -i \ + -e 's/{/\\{/g' \ + -e 's/}/\\}/g' \ + -e 's/> "$GITHUB_STEP_SUMMARY" fi + + - name: Restore unstamped docs.yml for persistence + if: steps.resolve.outputs.is_release == 'true' + run: cp /tmp/docs.yml.preStamp fern/docs.yml + + - name: Open PR for version registry changes + if: steps.resolve.outputs.is_release == 'true' + uses: peter-evans/create-pull-request@v7 + with: + base: main + branch: chore/fern-register-${{ steps.resolve.outputs.tag }} + delete-branch: true + sign-commits: true + commit-message: "chore(fern): register ${{ steps.resolve.outputs.tag }}, keep latest ${{ env.MAX_VERSIONS }} versions [automated]" + title: "chore(fern): register ${{ steps.resolve.outputs.tag }}" + body: | + Automated registration of `${{ steps.resolve.outputs.tag }}` in the Fern docs versions registry. + + Generated by the `Publish Fern Docs` workflow. + add-paths: | + fern/versions/v*.yml + fern/docs.yml diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 41abdb8cc..7837cba96 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -20,7 +20,6 @@ helm install nodewright oci://ghcr.io/nvidia/nodewright/charts/nodewright \ ``` > **Where things live:** chart at `oci://ghcr.io/nvidia/nodewright/charts/nodewright`, operator image at `ghcr.io/nvidia/nodewright/operator`, agent image at `ghcr.io/nvidia/nodewright/agent`. - > **Migrating from `helm repo add skyhook https://helm.ngc.nvidia.com/...`?** Run `helm repo remove skyhook` and use the OCI install above. ## Configure Image Pull Secrets (if needed) diff --git a/fern/docs.yml b/fern/docs.yml index 095b45b25..a3fb86a43 100644 --- a/fern/docs.yml +++ b/fern/docs.yml @@ -22,10 +22,6 @@ navbar-links: - type: github value: https://github.com/NVIDIA/nodewright -experimental: - mdx-components: - - ./components - products: - display-name: NodeWright path: ../docs/index.yml From 025be9444b91ad2e62214dec13d6c28f7eaa5e95 Mon Sep 17 00:00:00 2001 From: Pete MacKinnon Date: Wed, 19 Aug 2026 10:28:32 -0400 Subject: [PATCH 4/7] fix(docs): restore errexit in publish workflow, fix pull-secret install order fern generate failures in the publish step were being swallowed since -e was dropped from set -eo pipefail. Also reorders the installation guide so the image pull secret (and its namespace) exist before helm install references it via --set imagePullSecret, and requires Helm 3.8+ for native OCI support. Signed-off-by: Pete MacKinnon --- .github/workflows/publish-fern-docs.yml | 2 +- docs/getting-started/installation.md | 35 ++++++++++++++----------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/.github/workflows/publish-fern-docs.yml b/.github/workflows/publish-fern-docs.yml index f3964cdb3..b9a7a0e1c 100644 --- a/.github/workflows/publish-fern-docs.yml +++ b/.github/workflows/publish-fern-docs.yml @@ -235,7 +235,7 @@ jobs: FERN_TOKEN: ${{ secrets.DOCS_FERN_TOKEN }} working-directory: ./fern run: | - set -o pipefail + set -eo pipefail fern generate --docs 2>&1 | tee /tmp/fern-output.log URL=$(grep -oP 'Published docs to \K.*(?= \()' /tmp/fern-output.log || true) if [ -n "$URL" ]; then diff --git a/docs/getting-started/installation.md b/docs/getting-started/installation.md index 354b8e3d8..b42f1f0fc 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -5,9 +5,24 @@ NodeWright is installed via Helm as an OCI artifact from GitHub Container Regist ## Prerequisites - Kubernetes cluster (tested on v1.30+) -- Helm 3.x installed +- Helm 3.8+ installed (required for native OCI support) - Container registry access credentials (if using private registries) +## Configure Image Pull Secrets (if needed) + +If you're using private container registries, create the namespace and secret before installing so the release can reference it: + +```bash +kubectl create namespace nodewright + +kubectl create secret generic node-init-secret \ + --from-file=.dockerconfigjson=${HOME}/.docker/config.json \ + --type=kubernetes.io/dockerconfigjson \ + --namespace nodewright +``` + +NodeWright currently uses a single shared image pull secret for all packages, and agent/operator containers. If you need access to multiple registries, combine the credentials into one `dockerconfigjson` secret with multiple registry auths. + ## Install NodeWright ```bash @@ -16,25 +31,15 @@ NodeWright is installed via Helm as an OCI artifact from GitHub Container Regist helm install nodewright oci://ghcr.io/nvidia/nodewright/charts/nodewright \ --version v0.18.0 \ --namespace nodewright \ - --create-namespace + --create-namespace \ + --set imagePullSecret=node-init-secret ``` +Omit `--set imagePullSecret=node-init-secret` if you're pulling from public registries only. + > **Where things live:** chart at `oci://ghcr.io/nvidia/nodewright/charts/nodewright`, operator image at `ghcr.io/nvidia/nodewright/operator`, agent image at `ghcr.io/nvidia/nodewright/agent`. > **Migrating from `helm repo add skyhook https://helm.ngc.nvidia.com/...`?** Run `helm repo remove skyhook` and use the OCI install above. -## Configure Image Pull Secrets (if needed) - -If you're using private container registries, create the necessary secrets: - -```bash -kubectl create secret generic node-init-secret \ - --from-file=.dockerconfigjson=${HOME}/.docker/config.json \ - --type=kubernetes.io/dockerconfigjson \ - --namespace nodewright -``` - -NodeWright currently uses a single shared image pull secret for all packages, and agent/operator containers. If you need access to multiple registries, combine the credentials into one `dockerconfigjson` secret with multiple registry auths. - ## Verify Installation ```bash From 84c4cb7426b8aa990912333a63add64a5f4ea6ea Mon Sep 17 00:00:00 2001 From: Pete MacKinnon Date: Wed, 19 Aug 2026 10:57:26 -0400 Subject: [PATCH 5/7] fix(ci): harden Fern docs workflows per remaining CodeRabbit findings - fern-docs-ci.yml: add concurrency group to cancel superseded PR checks - fern-docs-preview-build.yml: include .mdx in changed-page diff pathspec, and hard-fail (instead of warn) when a registered version's tag is missing - publish-fern-docs.yml: pass release tag/prerelease through env vars instead of interpolating github.event.release.* into shell, and mkdir -p fern/versions before the first version file write Signed-off-by: Pete MacKinnon --- .github/workflows/fern-docs-ci.yml | 4 ++++ .github/workflows/fern-docs-preview-build.yml | 7 ++++--- .github/workflows/publish-fern-docs.yml | 7 +++++-- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.github/workflows/fern-docs-ci.yml b/.github/workflows/fern-docs-ci.yml index b22e6ad4b..97cbdf3d4 100644 --- a/.github/workflows/fern-docs-ci.yml +++ b/.github/workflows/fern-docs-ci.yml @@ -38,6 +38,10 @@ on: permissions: contents: read +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + jobs: ## --- OPTION: copy-PR-bot changed-files gate --- ## Uncomment this job and add "needs: changed-files" + diff --git a/.github/workflows/fern-docs-preview-build.yml b/.github/workflows/fern-docs-preview-build.yml index 660eca53f..fb1ed46e6 100644 --- a/.github/workflows/fern-docs-preview-build.yml +++ b/.github/workflows/fern-docs-preview-build.yml @@ -98,7 +98,7 @@ jobs: mkdir -p preview-metadata echo "$PR_NUMBER" > preview-metadata/pr_number echo "$HEAD_REF" > preview-metadata/head_ref - git diff --name-only "origin/${BASE_REF}...HEAD" -- '*.md' > preview-metadata/changed_md_files 2>/dev/null || true + git diff --name-only "origin/${BASE_REF}...HEAD" -- '*.md' '*.mdx' > preview-metadata/changed_md_files 2>/dev/null || true ## --- OPTION: copy-PR-bot metadata extraction --- ## Replace the step above with this when using push-to-pull-request/*: # - name: Save PR metadata @@ -108,7 +108,7 @@ jobs: # mkdir -p preview-metadata # echo "${BRANCH_NAME#pull-request/}" > preview-metadata/pr_number # echo "$BRANCH_NAME" > preview-metadata/head_ref - # git diff --name-only "origin/main...HEAD" -- '*.md' > preview-metadata/changed_md_files 2>/dev/null || true + # git diff --name-only "origin/main...HEAD" -- '*.md' '*.mdx' > preview-metadata/changed_md_files 2>/dev/null || true - name: Checkout frozen version content run: | @@ -125,7 +125,8 @@ jobs: -e 's/ "fern/versions/${TAG_VERSION}.yml" From f3ce5cf81fdf1b641281d42e37368543106e3f66 Mon Sep 17 00:00:00 2001 From: Alex Yuskauskas Date: Wed, 19 Aug 2026 11:10:54 -0700 Subject: [PATCH 6/7] fix(ci): publish Fern docs from chart releases only The publish workflow triggered on every `release: published` event and then required the tag to match `^v[0-9]+\.[0-9]+\.[0-9]+$`. This repo has no such tags: operator, agent, chart and cli are released independently as `/vX.Y.Z`, so every release would have failed at tag resolution and the docs would never have published. Gate the job on `chart/v` and make the chart the docs version axis. The chart is the artifact users install and it ships on every rollout, so its version is the one meaningful label for a docs snapshot; operator, agent and cli releases are skipped rather than failed, since they are not docs-publishing events. Tag resolution now emits two outputs instead of one. `tag` is the full ref (`chart/v1.2.0`) and is what git is asked for; `version` is the stripped label (`v1.2.0`) and is what names version files, slugs, display names and the registry PR branch. Conflating them would have produced `fern/versions/chart/v1.2.0.yml`, which the `fern/versions/v*.yml` globs in the freeze step and in `add-paths` would not have matched. Also reject tags with no `docs/index.yml` in their tree. No chart tag cut so far carries one, so a manual dispatch would otherwise have died inside `git archive` with a bare "pathspec did not match any files"; the empty-input fallback now skips those tags rather than resolving to an unpublishable one. Signed-off-by: Alex Yuskauskas --- .github/workflows/publish-fern-docs.yml | 102 ++++++++++++++++++------ 1 file changed, 76 insertions(+), 26 deletions(-) diff --git a/.github/workflows/publish-fern-docs.yml b/.github/workflows/publish-fern-docs.yml index 26c9f4a83..aa6671b86 100644 --- a/.github/workflows/publish-fern-docs.yml +++ b/.github/workflows/publish-fern-docs.yml @@ -13,23 +13,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -# Publishes the Fern documentation site on GitHub Release or manual dispatch. +# Publishes the Fern documentation site on a chart release or manual dispatch. +# +# The docs version axis follows the Helm chart. This repo releases four +# components independently (operator, agent, chart, cli) and every one of them +# publishes a GitHub Release, so an unfiltered `release` trigger would fire up +# to four times per coordinated release and label the docs with whichever +# component won the race. The chart is what users install and it ships on every +# rollout, so its version is the one meaningful label for a docs snapshot. +# Releases of the other three components are skipped, not failed. +# +# Tags are `chart/vX.Y.Z`. git is asked for the full tag; the stripped `vX.Y.Z` +# names version files, slugs and display names. The resolve step emits both as +# separate outputs — do not use one where the other is meant. # # All versions serve frozen content extracted from their git tag — there is no -# "live docs" entry. The newest version is stamped "Latest · vX.Y.Z" at publish -# time so readers know which is current; the stamp is transient and not persisted. +# "live docs" entry, so a docs change merged to main is not published until the +# next chart release carries it. The newest version is stamped "Latest · vX.Y.Z" +# at publish time so readers know which is current; the stamp is transient and +# not persisted. # # Manual dispatch accepts an optional `tag` input: -# - empty: resolves to the latest GitHub release tag -# - explicit (e.g. v1.2.0): publishes that tag +# - empty: resolves to the newest chart/vX.Y.Z tag in the repository +# - explicit: `chart/v1.2.0` or the bare `v1.2.0` — both resolve the same # -# On a vX.Y.Z release (publish or dispatch), the workflow also: +# On a stable chart/vX.Y.Z release (publish or dispatch), the workflow also: # 1. Generates fern/versions/vX.Y.Z.yml from the tag's docs/index.yml # 2. Adds a version entry to fern/docs.yml, sorted by semver descending # 3. Prunes older versions to keep only the MAX_VERSIONS most recent # 4. Opens a PR to persist registry changes back to main (after publish) # -# Pre-releases trigger publish but skip version registration. +# Pre-releases (chart/vX.Y.Z-rc.N) publish but skip version registration. # # Required configuration: # - Organization secret: DOCS_FERN_TOKEN (from `fern token` for the nvidia Fern org) @@ -42,7 +56,7 @@ on: workflow_dispatch: inputs: tag: - description: "Tag to publish (e.g. v1.2.0). Leave empty to use the latest GitHub release tag." + description: "Chart tag to publish (e.g. chart/v1.2.0, or the bare v1.2.0). Leave empty to use the newest chart tag." required: false type: string ## --- OPTION: tag-push trigger --- @@ -52,7 +66,7 @@ on: ## start — no race between tag push and Release creation. # push: # tags: - # - "v[0-9]*.[0-9]*.[0-9]*" + # - "chart/v[0-9]*.[0-9]*.[0-9]*" permissions: contents: write @@ -68,6 +82,12 @@ env: jobs: publish: + # `on: release` has no tag filter, so the component gate lives here. The + # operator, agent and cli releases fire this workflow too; they are not + # docs-publishing events, so skip them rather than failing the run. + if: >- + github.event_name != 'release' || + startsWith(github.event.release.tag_name, 'chart/v') runs-on: ubuntu-latest # runs-on: linux-amd64-cpu8 # NVIDIA self-hosted runner timeout-minutes: 20 @@ -99,7 +119,6 @@ jobs: - name: Resolve target tag id: resolve env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} INPUT_TAG: ${{ inputs.tag }} RELEASE_TAG: ${{ github.event.release.tag_name }} RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} @@ -113,35 +132,63 @@ jobs: elif [ -n "${INPUT_TAG}" ]; then TAG="${INPUT_TAG}" else - TAG=$(gh release view --repo "${GITHUB_REPOSITORY}" --json tagName -q .tagName) - if [ -z "$TAG" ] || [ "$TAG" = "null" ]; then - echo "::error::Could not determine latest release tag" + # Scoped to chart tags on purpose: the newest release in this repo + # is often an operator or cli release, which is not publishable here. + # Chart tags cut before Fern navigation landed are skipped — see the + # docs/index.yml check below for why they cannot be published. + for CANDIDATE in $(git tag -l 'chart/v*' --sort=-v:refname \ + | grep -E '^chart/v[0-9]+\.[0-9]+\.[0-9]+$'); do + if git cat-file -e "refs/tags/${CANDIDATE}:docs/index.yml" 2>/dev/null; then + TAG="${CANDIDATE}" + break + fi + done + if [ -z "${TAG:-}" ]; then + echo "::error::No chart tag carries docs/index.yml yet — the first publishable release is the first chart tag cut after this workflow landed" exit 1 fi fi - if ! [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$ ]]; then - echo "::error::Tag '${TAG}' is not a valid semver tag (expected vMAJOR.MINOR.PATCH[-PRERELEASE])" + # A bare version is accepted on manual dispatch: the docs version axis + # follows the chart, so v1.2.0 and chart/v1.2.0 name the same release. + # Only a bare version is promoted; anything else keeps the name the + # caller gave it so the error below names what they actually typed. + case "$TAG" in + v*) TAG="chart/${TAG}" ;; + esac + if ! [[ "$TAG" =~ ^chart/v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "::error::Tag '${TAG}' is not a publishable chart tag (expected chart/vMAJOR.MINOR.PATCH[-rc.N])" exit 1 fi if ! git show-ref --verify --quiet "refs/tags/${TAG}"; then echo "::error::Tag '${TAG}' does not exist in this repository" exit 1 fi + # Chart tags predating the Fern scaffold have no docs/index.yml to freeze. + # Without this the run dies later inside `git archive` with a bare + # "pathspec did not match any files", which reads as a workflow bug. + if ! git cat-file -e "refs/tags/${TAG}:docs/index.yml" 2>/dev/null; then + echo "::error::Tag '${TAG}' has no docs/index.yml — it predates the Fern docs navigation and cannot be published" + exit 1 + fi + # The docs version axis drops the component prefix: chart/v1.2.0 -> v1.2.0. + VERSION="${TAG#chart/}" # Pre-release detection: use the Release event flag when available, # fall back to hyphen check for tag-push and dispatch triggers. - if [ "${IS_PRERELEASE}" = "true" ] || [[ "$TAG" == *-* ]]; then + if [ "${IS_PRERELEASE}" = "true" ] || [[ "$VERSION" == *-* ]]; then IS_RELEASE=false else IS_RELEASE=true fi echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "is_release=$IS_RELEASE" >> "$GITHUB_OUTPUT" - echo "Resolved target tag: $TAG (is_release=$IS_RELEASE)" + echo "Resolved target tag: $TAG (version=$VERSION, is_release=$IS_RELEASE)" - name: Register new version from tag if: steps.resolve.outputs.is_release == 'true' env: - TAG_VERSION: ${{ steps.resolve.outputs.tag }} + TAG: ${{ steps.resolve.outputs.tag }} + TAG_VERSION: ${{ steps.resolve.outputs.version }} run: | set -eo pipefail @@ -151,8 +198,8 @@ jobs: fi mkdir -p fern/versions - echo "Generating fern/versions/${TAG_VERSION}.yml from tag $TAG_VERSION" - git archive "refs/tags/${TAG_VERSION}" -- docs/index.yml | tar -xO docs/index.yml | \ + echo "Generating fern/versions/${TAG_VERSION}.yml from tag $TAG" + git archive "refs/tags/${TAG}" -- docs/index.yml | tar -xO docs/index.yml | \ sed "s|path: |path: ${TAG_VERSION}-content/|g" > "fern/versions/${TAG_VERSION}.yml" export TAG_VERSION @@ -200,16 +247,19 @@ jobs: for version_file in fern/versions/v*.yml; do [ -f "$version_file" ] || continue version=$(basename "$version_file" .yml) - if git show-ref --verify --quiet "refs/tags/${version}"; then + # Registry files are named by the stripped version; the tag they were + # cut from still carries the chart/ prefix. + tag="chart/${version}" + if git show-ref --verify --quiet "refs/tags/${tag}"; then mkdir -p "fern/versions/${version}-content" - git archive "refs/tags/${version}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" + git archive "refs/tags/${tag}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" find "fern/versions/${version}-content" -name '*.md' -print0 | xargs -0 -r sed -i \ -e 's/{/\\{/g' \ -e 's/}/\\}/g' \ -e 's/ Date: Wed, 19 Aug 2026 15:01:24 -0400 Subject: [PATCH 7/7] fix(ci): resolve frozen version content against chart/ tags in preview-build fern-docs-preview-build.yml's "Checkout frozen version content" step still looked up refs/tags/ directly, but publish-fern-docs.yml (f3ce5cf8) now tags docs releases as chart/vX.Y.Z while naming the registry files by the stripped vX.Y.Z. Once the first chart version is registered, every PR preview build would fail to resolve the tag and hard-error. Mirrors the same chart/-prefix fix already applied on the publish side. Signed-off-by: Pete MacKinnon --- .github/workflows/fern-docs-preview-build.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/fern-docs-preview-build.yml b/.github/workflows/fern-docs-preview-build.yml index fb1ed46e6..0c8192ef3 100644 --- a/.github/workflows/fern-docs-preview-build.yml +++ b/.github/workflows/fern-docs-preview-build.yml @@ -116,16 +116,19 @@ jobs: for version_file in fern/versions/v*.yml; do [ -f "$version_file" ] || continue version=$(basename "$version_file" .yml) - if git show-ref --verify --quiet "refs/tags/${version}"; then + # Registry files are named by the stripped version; the tag they were + # cut from still carries the chart/ prefix. + tag="chart/${version}" + if git show-ref --verify --quiet "refs/tags/${tag}"; then mkdir -p "fern/versions/${version}-content" - git archive "refs/tags/${version}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" + git archive "refs/tags/${tag}" -- docs/ | tar -x --strip-components=1 -C "fern/versions/${version}-content" find "fern/versions/${version}-content" -name '*.md' -print0 | xargs -0 -r sed -i \ -e 's/{/\\{/g' \ -e 's/}/\\}/g' \ -e 's/