diff --git a/.github/workflows/fern-docs-ci.yml b/.github/workflows/fern-docs-ci.yml new file mode 100644 index 000000000..97cbdf3d4 --- /dev/null +++ b/.github/workflows/fern-docs-ci.yml @@ -0,0 +1,128 @@ +# 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. + +# Validates Fern docs configuration on pull requests that touch docs or fern/. + +name: Fern Docs CI + +on: + pull_request: + paths: + - '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 + +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 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: npm install -g fern-api@$(jq -r .version fern/fern.config.json) + + - 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 + run: fern check + + - name: Check MDX validity + run: fern docs md check + + - name: Check broken links + 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..0c8192ef3 --- /dev/null +++ b/.github/workflows/fern-docs-preview-build.yml @@ -0,0 +1,144 @@ +# 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 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. + +name: "Preview Fern Docs: Build" + +on: + pull_request: + paths: + - '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: + 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' '*.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 + # 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' '*.mdx' > 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) + # 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/${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/> "$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: | + set -euo pipefail + VERSION=$(jq -r .version fern/fern.config.json) + if ! [[ "$VERSION" =~ ^([0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?|latest)$ ]]; then + echo "::error::fern.config.json .version '$VERSION' is not a valid semver or 'latest'" + 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: | + 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 + 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="${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) || { + 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..aa6671b86 --- /dev/null +++ b/.github/workflows/publish-fern-docs.yml @@ -0,0 +1,318 @@ +# 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 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, 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 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 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 (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) + +name: Publish Fern Docs + +on: + release: + types: [published] + workflow_dispatch: + inputs: + 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 --- + ## 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: + # - "chart/v[0-9]*.[0-9]*.[0-9]*" + +permissions: + contents: write + pull-requests: write + +concurrency: + group: fern-publish + cancel-in-progress: true + +env: + YQ_VERSION: v4.53.2 + MAX_VERSIONS: 3 + +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 + 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: + INPUT_TAG: ${{ inputs.tag }} + RELEASE_TAG: ${{ github.event.release.tag_name }} + RELEASE_PRERELEASE: ${{ github.event.release.prerelease }} + run: | + set -eo pipefail + if [ "${GITHUB_EVENT_NAME}" = "release" ]; then + TAG="${RELEASE_TAG}" + IS_PRERELEASE="${RELEASE_PRERELEASE}" + elif [ "${GITHUB_EVENT_NAME}" = "push" ]; then + TAG="${GITHUB_REF#refs/tags/}" + elif [ -n "${INPUT_TAG}" ]; then + TAG="${INPUT_TAG}" + else + # 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 + # 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" ] || [[ "$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 (version=$VERSION, is_release=$IS_RELEASE)" + + - name: Register new version from tag + if: steps.resolve.outputs.is_release == 'true' + env: + TAG: ${{ steps.resolve.outputs.tag }} + TAG_VERSION: ${{ steps.resolve.outputs.version }} + run: | + set -eo pipefail + + if [ -f "fern/versions/${TAG_VERSION}.yml" ]; then + echo "Version ${TAG_VERSION} already registered — skipping" + exit 0 + fi + + mkdir -p fern/versions + 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 + 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 + [ -f "$version_file" ] || continue + version=$(basename "$version_file" .yml) + # 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/${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/&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 + + - 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.version }} + delete-branch: true + sign-commits: true + commit-message: "chore(fern): register ${{ steps.resolve.outputs.version }}, keep latest ${{ env.MAX_VERSIONS }} versions [automated]" + title: "chore(fern): register ${{ steps.resolve.outputs.version }}" + 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 b7a9f0aa1..b42f1f0fc 100644 --- a/docs/getting-started/installation.md +++ b/docs/getting-started/installation.md @@ -2,19 +2,72 @@ 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.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 +# 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.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. + +## 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 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..a3fb86a43 --- /dev/null +++ b/fern/docs.yml @@ -0,0 +1,28 @@ +# 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 + +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" +}