Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
9ef2128
Add CLI/SDK docs verification script and CI workflow
Gobind-Vast Apr 13, 2026
37ea90b
Merge branch 'master' into feature/verify-cli-sdk-docs
Gobind-Vast Apr 13, 2026
f1d1ce3
Fix CLI two-level command parsing and add verification docs
Gobind-Vast Apr 23, 2026
b80614c
Added automated docs generation script and pipeline
Gobind-Vast May 11, 2026
66a6bbd
Added CLI/SDK generation based on API scopes
Gobind-Vast May 12, 2026
68bc080
Merge branch 'master' into SO-80--auto-generate-cli-sdk-docs
Gobind-Vast May 13, 2026
b6ae577
docs(workflow): open preview PR ready-for-review (Mintlify skips drafts)
Gobind-Vast May 14, 2026
2277b6c
auto-update docs.json nav with new/removed pages
Gobind-Vast May 14, 2026
3ab2f91
Merge branch 'master' into SO-80--auto-generate-cli-sdk-docs
Gobind-Vast May 20, 2026
7696cc8
docs(generator): escape angle brackets in prose to prevent MDX 404s
Gobind-Vast May 20, 2026
3b73938
docs(workflow): add post-deploy 404 sweep to catch silent MDX failures
Gobind-Vast May 22, 2026
8c4af1e
docs(workflow): wipe stale CLI/SDK MDX before regenerating
Gobind-Vast May 22, 2026
32af1ce
docs(nav): classify accept-price-increase and serverless endpt/wrkgrp…
Gobind-Vast May 22, 2026
cb45427
docs(workflow): trigger on patch_docs_nav.py changes too
Gobind-Vast May 22, 2026
11de4a8
docs(generator): exclude network-volume commands from public docs
Gobind-Vast May 22, 2026
0a9cd36
Merge branch 'master' into SO-80--auto-generate-cli-sdk-docs
Gobind-Vast Jun 1, 2026
b97bd01
docs(generator): discover metrics/benchmarks/price_increase; classify…
Gobind-Vast Jun 1, 2026
208b743
Merge branch 'master' into SO-80--auto-generate-cli-sdk-docs
Gobind-Vast Jun 4, 2026
1e5aa2b
Merge branch 'master' into SO-80--auto-generate-cli-sdk-docs
Gobind-Vast Jun 23, 2026
8b98d1f
docs(nav): place unclassified pages in default group instead of orpha…
Gobind-Vast Jun 23, 2026
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
356 changes: 356 additions & 0 deletions .github/workflows/auto-generate-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,356 @@
# Auto-generate CLI/SDK reference documentation and publish to vast-ai/docs.
#
# Mirrors the OpenAPI auto-generation pipeline that lives in the vast (backend)
# repo's bitbucket-pipelines.yml — same pattern, same docs repo target:
#
# * On a PR to vast-cli, generate the docs into a preview branch on
# vast-ai/docs and open a PR there for Mintlify preview.
# * On merge to master, push the regenerated docs as a PR to vast-ai/docs
# so the on-merge auto-deploy ships them.
# * Manual dispatch lets us regenerate on demand.
#
# Required repo secrets (Settings → Secrets and variables → Actions):
# DOCS_DEPLOY_KEY ed25519 SSH private key with write access to
# vast-ai/docs. Same key the vast Bitbucket pipeline
# already uses; add the matching deploy key to the
# vast-cli repo too if it isn't already paired.
# DOCS_GITHUB_TOKEN classic PAT with `repo` scope for creating PRs in
# vast-ai/docs via the GitHub API.

name: Auto-generate CLI/SDK Docs

on:
pull_request:
paths:
- "vastai/**"
- "scripts/generate_cli_sdk_docs.py"
- "scripts/patch_docs_nav.py"
- "pyproject.toml"
- ".github/workflows/auto-generate-docs.yml"
push:
branches: [master, main]
paths:
- "vastai/**"
- "scripts/generate_cli_sdk_docs.py"
- "scripts/patch_docs_nav.py"
- "pyproject.toml"
- ".github/workflows/auto-generate-docs.yml"
workflow_dispatch: {}

concurrency:
group: auto-generate-docs-${{ github.ref }}
cancel-in-progress: true

jobs:
generate:
runs-on: ubuntu-latest
env:
DOCS_REPO: vast-ai/docs
DOCS_DEFAULT_BRANCH: main
CLI_SUBDIR: cli/reference
SDK_SUBDIR: sdk/python/reference
steps:
- name: Checkout vast-cli
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install vast-cli (editable)
run: pip install -e .

- name: Configure SSH for docs repo
run: |
mkdir -p ~/.ssh
echo "${{ secrets.DOCS_DEPLOY_KEY }}" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
ssh-keyscan github.com >> ~/.ssh/known_hosts

- name: Clone docs repo
run: |
git clone "git@github.com:${DOCS_REPO}.git" docs-repo
cd docs-repo
git config user.email "vast-cli-docs-bot@vast.ai"
git config user.name "vast-cli docs bot"

- name: Wipe stale CLI/SDK MDX before regeneration
# Without this, files persist after their source command/method is
# removed (e.g. cluster/overlay commands disabled in vast-cli/main.py
# left 11 orphan MDX files on the preview branch as of 2026-05-20).
# Orphans are harmless visually (not in nav, not rendered) but they
# accumulate and confuse audits. Preserve hand-authored entries: the
# SDK overview at sdk/python/reference/vastai.mdx is the only one.
run: |
cd docs-repo
find "${CLI_SUBDIR}" -maxdepth 1 -name '*.mdx' -delete 2>/dev/null || true
find "${SDK_SUBDIR}" -maxdepth 1 -name '*.mdx' ! -name 'vastai.mdx' -delete 2>/dev/null || true

- name: Generate docs into clone
run: |
python scripts/generate_cli_sdk_docs.py \
--output-dir docs-repo \
--cli-subdir "${CLI_SUBDIR}" \
--sdk-subdir "${SDK_SUBDIR}" \
--manifest manifest.json

- name: Patch docs.json navigation
# Mintlify only renders pages registered in docs.json's nav. This
# step adds new generated pages to the right semantic groups and
# removes nav entries for deleted commands. Without it, every
# generated PR ships orphan pages and Mintlify skips the preview.
run: |
python scripts/patch_docs_nav.py \
--docs-json docs-repo/docs.json \
--manifest manifest.json

- name: Show diff summary
id: diff
run: |
cd docs-repo
git add -A "${CLI_SUBDIR}" "${SDK_SUBDIR}" docs.json
if git diff --cached --quiet; then
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "No CLI/SDK doc changes detected."
else
echo "has_changes=true" >> "$GITHUB_OUTPUT"
git diff --cached --stat | tee ../diff-stat.txt
fi

- name: Upload manifest
if: always()
uses: actions/upload-artifact@v4
with:
name: cli-sdk-docs-manifest
path: |
manifest.json
diff-stat.txt

# ----------------------------------------------------------------
# PR (preview): push to a preview branch and open a PR in docs repo.
# ----------------------------------------------------------------
- name: Push preview branch
if: |
github.event_name == 'pull_request' &&
steps.diff.outputs.has_changes == 'true'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_HTML_URL: ${{ github.event.pull_request.html_url }}
run: |
cd docs-repo
PREVIEW_BRANCH="auto/cli-sdk-preview-pr-${PR_NUMBER}"
git checkout -b "${PREVIEW_BRANCH}"
git commit -m "preview: CLI/SDK docs for vast-cli PR #${PR_NUMBER}

Source PR: ${PR_HTML_URL}
Title: ${PR_TITLE}
Auto-generated by .github/workflows/auto-generate-docs.yml"
git push -f origin "${PREVIEW_BRANCH}"
echo "PREVIEW_BRANCH=${PREVIEW_BRANCH}" >> "$GITHUB_ENV"

- name: Open or update preview PR in docs repo
if: |
github.event_name == 'pull_request' &&
steps.diff.outputs.has_changes == 'true'
env:
PR_NUMBER: ${{ github.event.pull_request.number }}
PR_TITLE: ${{ github.event.pull_request.title }}
PR_HTML_URL: ${{ github.event.pull_request.html_url }}
GH_TOKEN: ${{ secrets.DOCS_GITHUB_TOKEN }}
run: |
set -e
BODY=$(cat <<EOF
Auto-generated CLI/SDK docs for vast-cli PR #${PR_NUMBER}.

Source PR: ${PR_HTML_URL}
Title: ${PR_TITLE}

This PR is opened automatically so Mintlify can produce a preview.
It will be updated on every push to the source PR and closed on merge.
EOF
)
# Look for an existing open PR for this preview branch
EXISTING=$(curl -fsSL \
-H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${DOCS_REPO}/pulls?state=open&head=vast-ai:${PREVIEW_BRANCH}" \
| python -c "import json,sys; d=json.load(sys.stdin); print(d[0]['number'] if d else '')")
if [ -z "${EXISTING}" ]; then
curl -fsSL -X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${DOCS_REPO}/pulls" \
-d "$(python -c "import json,sys,os; print(json.dumps({'title': f'[Preview] vast-cli #${PR_NUMBER}: {os.environ[\"PR_TITLE\"]}', 'head': '${PREVIEW_BRANCH}', 'base': '${DOCS_DEFAULT_BRANCH}', 'body': sys.stdin.read()}))" <<< "${BODY}")"
else
echo "Preview PR #${EXISTING} already exists, branch was force-pushed."
fi

# ----------------------------------------------------------------
# Master merge (publish): push to a deploy branch and open a PR.
# ----------------------------------------------------------------
- name: Push deploy branch
if: |
github.event_name == 'push' &&
steps.diff.outputs.has_changes == 'true'
run: |
cd docs-repo
DEPLOY_BRANCH="auto/cli-sdk-docs-${GITHUB_SHA::8}"
git checkout -b "${DEPLOY_BRANCH}"
git commit -m "Auto-generated CLI/SDK docs from vast-cli@${GITHUB_SHA::8}

Source: https://github.com/${{ github.repository }}/commit/${{ github.sha }}
Generated by .github/workflows/auto-generate-docs.yml"
git push origin "${DEPLOY_BRANCH}"
echo "DEPLOY_BRANCH=${DEPLOY_BRANCH}" >> "$GITHUB_ENV"

- name: Open deploy PR in docs repo
if: |
github.event_name == 'push' &&
steps.diff.outputs.has_changes == 'true'
env:
GH_TOKEN: ${{ secrets.DOCS_GITHUB_TOKEN }}
run: |
BODY=$(cat <<EOF
Auto-generated CLI/SDK reference documentation from vast-cli.

Source commit: https://github.com/${{ github.repository }}/commit/${{ github.sha }}
Generated by \`.github/workflows/auto-generate-docs.yml\`.

Merging this PR ships the regenerated reference pages to docs.vast.ai via Mintlify.
EOF
)
curl -fsSL -X POST \
-H "Authorization: token ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${DOCS_REPO}/pulls" \
-d "$(python -c "import json,sys,os; print(json.dumps({'title': f'Auto-generated CLI/SDK docs (vast-cli@${GITHUB_SHA::8})', 'head': '${DEPLOY_BRANCH}', 'base': '${DOCS_DEFAULT_BRANCH}', 'body': sys.stdin.read()}))" <<< "${BODY}")"

# ----------------------------------------------------------------
# Post-deploy 404 sweep.
#
# Mintlify reports "Deployment: success" even when individual MDX pages
# fail to build — those pages silently return HTTP 404. We've shipped 10
# such pages by accident before (SO-80, 2026-05-20: unescaped <PLACEHOLDER>
# tokens in argparse epilogs parsed as JSX and failed the page build).
# This step waits for Mintlify to deploy the freshly-pushed commit, then
# curls every CLI/SDK reference URL in docs.json and fails the workflow
# on any non-200. Catches silent generator regressions before they merge.
# ----------------------------------------------------------------
- name: Wait for Mintlify and sweep for 404s
if: |
steps.diff.outputs.has_changes == 'true' &&
(github.event_name == 'pull_request' || github.event_name == 'push')
env:
GH_TOKEN: ${{ secrets.DOCS_GITHUB_TOKEN }}
run: |
set -e
cd docs-repo
HEAD_SHA=$(git rev-parse HEAD)
echo "Waiting for Mintlify deploy on docs commit ${HEAD_SHA}..."

PREVIEW_URL=""
# Poll for up to ~10 minutes (40 * 15s). Mintlify usually deploys in
# 1-2 min, but cold builds on a fresh preview branch can take longer.
for i in $(seq 1 40); do
state=$(curl -fsSL \
-H "Authorization: token ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/repos/${DOCS_REPO}/commits/${HEAD_SHA}/check-runs" 2>/dev/null \
| jq -r '.check_runs[]? | select(.name=="Mintlify Deployment") | "\(.status) \(.conclusion // "null") \(.output.summary // "")"' || echo "")

status=$(echo "$state" | awk '{print $1}')
conclusion=$(echo "$state" | awk '{print $2}')

if [ "$status" = "completed" ]; then
if [ "$conclusion" = "success" ]; then
PREVIEW_URL=$(echo "$state" | grep -oE 'https://[^[:space:]!]+\.mintlify\.app' | head -1)
if [ -z "$PREVIEW_URL" ]; then
echo "::error::Mintlify reported success but no preview URL in summary: ${state}"
exit 1
fi
echo "Mintlify deployed: ${PREVIEW_URL} (after ${i} polls)"
break
else
echo "::error::Mintlify deployment ${conclusion}"
exit 1
fi
fi
sleep 15
done

if [ -z "$PREVIEW_URL" ]; then
echo "::error::Mintlify deploy did not complete on ${HEAD_SHA} within 10 minutes"
exit 1
fi

mapfile -t PAGES < <(jq -r '.. | strings? | select(startswith("cli/reference/") or startswith("sdk/python/reference/"))' docs.json | sort -u)
if [ "${#PAGES[@]}" -eq 0 ]; then
echo "::warning::No CLI/SDK pages found in docs.json — skipping sweep"
exit 0
fi
echo "Sweeping ${#PAGES[@]} CLI/SDK reference pages..."

FAIL_COUNT=0
FAILED=()
for path in "${PAGES[@]}"; do
[ -z "$path" ] && continue
code=$(curl -s -o /dev/null -w '%{http_code}' -L --max-time 20 "${PREVIEW_URL}/${path}" || echo "000")
if [ "$code" != "200" ]; then
echo "::error file=${path}.mdx::HTTP ${code} on ${PREVIEW_URL}/${path}"
FAILED+=("${path} (HTTP ${code})")
FAIL_COUNT=$((FAIL_COUNT+1))
fi
done

if [ $FAIL_COUNT -gt 0 ]; then
echo "::error::${FAIL_COUNT} of ${#PAGES[@]} pages failed the 404 sweep:"
printf ' - %s\n' "${FAILED[@]}"
exit 1
fi
echo "All ${#PAGES[@]} pages returned HTTP 200."

# ----------------------------------------------------------------
# Cleanup: close orphan preview PRs whose source PR was merged/closed.
# Runs on master pushes, when we know which previews are obsolete.
# ----------------------------------------------------------------
- name: Close stale preview PRs
if: github.event_name == 'push'
env:
GH_TOKEN: ${{ secrets.DOCS_GITHUB_TOKEN }}
run: |
# Best-effort: list open auto/cli-sdk-preview-pr-* branches, check
# the source PR state in vast-cli, close + delete branches whose
# source is no longer open.
curl -fsSL \
-H "Authorization: token ${GH_TOKEN}" \
"https://api.github.com/repos/${DOCS_REPO}/pulls?state=open&per_page=100" \
| python <<'PY'
import json, os, re, subprocess, sys, urllib.request
repo = os.environ["DOCS_REPO"]
tok = os.environ["GH_TOKEN"]
src = "${{ github.repository }}"
for pr in json.load(sys.stdin):
m = re.match(r"auto/cli-sdk-preview-pr-(\d+)$", pr["head"]["ref"])
if not m:
continue
src_num = m.group(1)
req = urllib.request.Request(
f"https://api.github.com/repos/{src}/pulls/{src_num}",
headers={"Authorization": f"token {tok}"},
)
try:
src_pr = json.loads(urllib.request.urlopen(req).read())
except Exception:
continue
if src_pr.get("state") == "closed":
print(f"Closing stale preview PR #{pr['number']} (source PR #{src_num} closed)")
close_req = urllib.request.Request(
f"https://api.github.com/repos/{repo}/pulls/{pr['number']}",
method="PATCH",
headers={"Authorization": f"token {tok}",
"Content-Type": "application/json"},
data=json.dumps({"state": "closed"}).encode(),
)
urllib.request.urlopen(close_req)
PY
Loading
Loading