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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/fern-docs-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-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

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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

## --- 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@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'

- name: Install Fern CLI
run: npm install -g fern-api@$(jq -r .version fern/fern.config.json)
Comment on lines +105 to +106

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- target workflow excerpts ---'
for f in .github/workflows/fern-docs-ci.yml .github/workflows/publish-fern-docs.yml; do
  echo "### $f"
  sed -n '85,120p' "$f" 2>/dev/null || sed -n '35,70p' "$f"
done
printf '%s\n' '--- all fern CLI installation and VERSION validation sites ---'
rg -n -C 5 'Install Fern CLI|npm install -g fern-api|VERSION=.*fern|valid semver|semver' .github/workflows
printf '%s\n' '--- referenced config files and version values ---'
for f in fern/fern.config.json; do
  if [ -f "$f" ]; then
    jq -c '{version: .version}' "$f"
  else
    echo "$f: missing"
  fi
done
printf '%s\n' '--- workflow file list ---'
git ls-files '.github/workflows/*'

Repository: NVIDIA/fleet-intelligence-agent

Length of output: 5161


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- workflow triggers, checkout, and permissions ---'
for f in .github/workflows/fern-docs-ci.yml .github/workflows/publish-fern-docs.yml .github/workflows/fern-docs-preview-comment.yml; do
  echo "### $f"
  sed -n '1,95p' "$f"
done
printf '%s\n' '--- all references to fern.config.json and workflow-controlled inputs ---'
rg -n -C 4 'fern\.config\.json|pull_request|workflow_dispatch|repository_dispatch|ref:|checkout|paths:|permissions:' .github/workflows
printf '%s\n' '--- package-manager availability and npm spec parser metadata (read-only) ---'
command -v npm || true
npm --version 2>/dev/null || true
node --version 2>/dev/null || true
npm view npm version 2>/dev/null || true

Repository: NVIDIA/fleet-intelligence-agent

Length of output: 29230


🏁 Script executed:

#!/bin/bash
set -e
node - <<'JS'
const candidates = [
  'fern-api@5.67.1',
  'fern-api@latest',
  'fern-api@^5.67.1',
  'fern-api@~5.67.1',
  'fern-api@npm:some-other-package@1.0.0',
  'fern-api@file:../local-package',
  'fern-api@https://example.invalid/pkg.tgz',
];
let parse;
try {
  parse = require('npm-package-arg');
} catch (e) {
  console.error(`npm-package-arg unavailable: ${e.message}`);
  process.exit(2);
}
for (const spec of candidates) {
  try {
    const p = parse(spec);
    console.log(JSON.stringify({spec, type: p.type, rawSpec: p.rawSpec, fetchSpec: p.fetchSpec, name: p.name}));
  } catch (e) {
    console.log(JSON.stringify({spec, error: e.message}));
  }
}
JS
printf '%s\n' '--- preview regex acceptance ---'
node - <<'JS'
const re = /^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$/;
for (const v of ['5.67.1', '5.67.1-beta.1', 'latest', '^5.67.1', '~5.67.1', 'npm:other@1.0.0', 'file:../x', '5.67.1+build.1', '5.67.1-a..b', '5.67.1-01']) {
  console.log(`${v}\t${re.test(v)}`);
}
JS

Repository: NVIDIA/fleet-intelligence-agent

Length of output: 279


🏁 Script executed:

#!/bin/bash
set -e
printf '%s\n' '--- locate npm package-spec parser bundled with npm ---'
npm_root="$(npm root -g)"
echo "global root: $npm_root"
find /usr/local /usr/lib "$npm_root" -type f -path '*/npm-package-arg/*' 2>/dev/null | head -20
printf '%s\n' '--- evaluate the validation pattern independently ---'
node - <<'JS'
const re = /^[0-9]+\.[0-9]+\.[0-9]+(-[A-Za-z0-9.]+)?$/;
for (const v of ['5.67.1', '5.67.1-beta.1', 'latest', '^5.67.1', '~5.67.1', 'npm:other@1.0.0', 'file:../x', '5.67.1+build.1', '5.67.1-a..b', '5.67.1-01']) {
  console.log(`${v}\t${re.test(v)}`);
}
JS

Repository: NVIDIA/fleet-intelligence-agent

Length of output: 1062


🌐 Web query:

npm package spec documentation aliases tags ranges exact version package@spec

💡 Result:

In the npm ecosystem, a package specifier (or package-spec) is a string used to identify a package in commands like npm install or within a package.json file [1][2][3]. These specifiers allow you to request specific packages using different methods, including by name, version, range, tag, or alias [1][2]. Package Name and Versions A basic package specifier typically follows the format [<@scope>/] [1][3]. To specify a version, you use the @ symbol followed by the desired version, tag, or range [1][2][3]. - Exact Version: To install a specific version, use package@version (e.g., npm@6.13.1) [1][2][3]. - Version Ranges: You can specify a version range to allow for flexible updates, following semantic versioning (SemVer) rules, such as ^1.2.3 or ~1.2.3 (e.g., npm@^4.0.0) [1][3][4]. - Tags (Dist-tags): Distribution tags are human-readable labels that point to specific versions [5]. You can install a package by tag using package@tag (e.g., npm@latest or npm@beta) [6][7][8]. If no tag or version is specified, npm defaults to the latest tag [6][8][9]. Tags must not be interpretable as valid SemVer ranges (e.g., avoid tags starting with a number or the letter v) [6][8][9]. Aliases Package aliases allow you to install a package under a custom name, which is useful for resolving naming conflicts or using multiple versions of the same package simultaneously [7]. The syntax is @npm: [1][2]. For example, running npm install my-alias@npm:original-package installs original-package into your project but renames it to my-alias within your node_modules folder [7]. General Syntax The overarching format for a named package specifier is [1][3]: - [<@scope>/] - [<@scope>/]@ - [<@scope>/]@ - [<@scope>/]@ Other types of specifiers, such as local folders (prefixed with./ or /), tarballs, and git URLs, also exist within the broader definition of a package-spec [1][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -e
node - <<'JS'
const parse = require('/usr/local/lib/node_modules/npm/node_modules/npm-package-arg');
const candidates = [
  'fern-api@5.67.1',
  'fern-api@latest',
  'fern-api@^5.67.1',
  'fern-api@~5.67.1',
  'fern-api@npm:some-other-package@1.0.0',
  'fern-api@file:../local-package',
  'fern-api@https://example.invalid/pkg.tgz',
];
for (const spec of candidates) {
  try {
    const p = parse(spec);
    console.log(JSON.stringify({spec, type: p.type, rawSpec: p.rawSpec, fetchSpec: p.fetchSpec, name: p.name, alias: p.subSpec && p.subSpec.name}));
  } catch (e) {
    console.log(JSON.stringify({spec, error: e.message}));
  }
}
JS

Repository: NVIDIA/fleet-intelligence-agent

Length of output: 1076


Validate .version before installing Fern CLI.

Both workflows pass the unvalidated value to npm, which accepts ranges, tags, aliases, and other package specs. Apply the preview workflow’s semver validation and install "fern-api@${VERSION}" in both locations.

🧰 Tools
🪛 zizmor (1.28.0)

[warning] 106-106: ad-hoc installation of packages (adhoc-packages): installs a package outside of a lockfile

(adhoc-packages)

📍 Affects 2 files
  • .github/workflows/fern-docs-ci.yml#L105-L106 (this comment)
  • .github/workflows/publish-fern-docs.yml#L54-L55
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/fern-docs-ci.yml around lines 105 - 106, Validate the
.version value with the existing preview workflow’s semver validation before
installation, assign it to VERSION, and install the exact package using the
VERSION variable. Apply this change at .github/workflows/fern-docs-ci.yml lines
105-106 and .github/workflows/publish-fern-docs.yml lines 54-55.

Source: Linters/SAST tools


- name: Check MDX safety
run: |
BAD=$(grep -rn '<img\b[^>]*[^/]>\|<img>' docs/ fern/ --include="*.md" --include="*.mdx" || true)
if [ -n "$BAD" ]; then
echo "::error::Non-self-closing <img> tags found (MDX requires <img ... />):"
echo "$BAD"
exit 1
fi

- name: Fern check
run: fern check

- name: Check links
uses: lycheeverse/lychee-action@8646ba30535128ac92d33dfc9133794bfdd9b411 # v2.8.0
with:
args: --offline --no-progress 'docs/**/*.md'
fail: true
140 changes: 140 additions & 0 deletions .github/workflows/fern-docs-preview-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
# SPDX-FileCopyrightText: Copyright (c) 2025-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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
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' > 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/</\&lt;/g'
echo "Extracted docs from $version"
else
echo "::warning::Tag $version not found — skipping content checkout"
fi
done

- name: Upload fern sources and metadata
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: fern-preview
path: |
fern/
docs/
preview-metadata/
retention-days: 1
Loading
Loading