Skip to main content

Build & Publish Pipeline

How a snippet edit becomes a PR preview and, once merged, the live page at docs.aviatrix.com. For the snippet → shell → nav generation internals, see VERSIONING.md. For local build commands, see the Quick Start and 🔧 Build System sections of README.md.

Architecture: source vs. generated

This repo holds only source: snippets, navigation templates, and the Python generators. The build output — docs.json, the docs/enterprise/{8.2,9.0}/ shells, and navigation-parser/subsections/generated/ — is git-ignored and never committed on main or a feature branch. It exists locally only after you run make all, and in CI only for the duration of a workflow run. Instead, a separate workflow (below) rebuilds that output fresh on every push and force-pushes it to a dedicated deploy branch that Mintlify actually serves from. Source branches and deploy branches are kept strictly separate:

CI: validation (build-docs.yml)

Runs on every pull request (any branch) and every push to main/master. Purely a correctness gate — it never publishes anything.
  1. make clean && make all — must produce a complete docs.json.
  2. python3 scripts/check_version_gaps.py — a page missing from a newer version must be documented by a remove entry in navigation-parser/subsections/overrides/.
  3. python3 scripts/check_nav_shells.py — every nav entry resolves to an actual generated shell file.
  4. python3 scripts/lint_docs.py — frontmatter, {props.X} references, version-templated doc links, absolute image paths.
  5. python3 scripts/check_image_duplicates.py — no duplicate image assets.
  6. python3 scripts/check_links_images.py — internal links and image references resolve.
  7. python3 -m pytest scripts/tests/ — generator unit tests.
  8. A final check that docs.json exists, is valid JSON, and contains the expected navigation/name structure.
On failure, the image/link checks post a summary to the job’s GITHUB_STEP_SUMMARY.

CD: publish (publish-mint-deploy.yml)

Rebuilds and force-pushes the servable tree to a deploy branch. Routing:
  • Push to main → main-prod (production; serves docs.aviatrix.com).
  • Pull request against any base except main-prod (opened/synchronize/reopened) → preview-<feature-branch> (slashes in the branch name become -), plus a preview URL. Release-cycle bases such as v10.1-dev are included.
  • workflow_dispatch with a target_override input → an arbitrary branch name. Rarely needed now that every PR is covered; useful for previewing a branch that has no PR open yet.
Previews are fully automatic and refresh on every push to the PR. Nothing has to be run by hand and nothing has to be registered in the Mintlify dashboard — see Getting a preview URL. main-prod is excluded from the pull_request trigger on purpose: it is the production deploy target, not a base branch contributors PR against. Steps:
  1. Check out the PR’s head ref (or the pushed ref).
  2. make all — regenerate docs.json and the shells.
  3. rsync the working tree into /tmp/deploy, excluding everything Mintlify does not need to serve (scripts/, navigation-parser/, Makefile, .github/, .husky/, lint/format configs, node_modules/, package*.json, *.py, and the source .gitignore itself — excluding the .gitignore matters, otherwise git add -A on the deploy branch would silently drop the generated output it’s supposed to ship).
  4. Force-push that pruned tree as a single commit to the target branch (preview-<feature-branch>, or main-prod for a production push).
  5. On a PR event, call Mintlify’s POST /project/preview/{projectId} API directly (with 429-backoff retry, up to 4 attempts) to trigger a build for the pushed branch and get back a statusId and previewUrl. This step checks MINTLIFY_API_KEY / MINTLIFY_PROJECT_ID are set first and skips cleanly if not — see Getting a preview URL for why the explicit call is necessary at all.
  6. Poll GET /project/update-status/{statusId} every 10 seconds, up to a 5-minute deadline. The loop breaks as soon as status is any non-empty value other than the documented in-progress states queued/in_progress — not only on an exact success/failure match — and reports that raw value. This is hardening for an undocumented status Mintlify has been observed to return outside its stated queued/in_progress/success/failure enum (see Getting a preview URL); without it, that case would loop silently for the full timeout instead of being recognized.
  7. Upsert a single marked comment (an HTML comment containing the string mintlify-preview) on the source PR with the outcome: “ready” with the preview link on success, a build-failure message (with Mintlify’s summary) on failure, a “build failed to start” message if the trigger call itself failed, a distinct “Mintlify reported an unusual status” message for any other terminal-looking value (e.g. skipped), a “still building” note if the deadline passed with no terminal status, or a “credentials not configured” note if MINTLIFY_API_KEY / MINTLIFY_PROJECT_ID are unset — plus the source SHA. Upserting rather than posting once keeps the comment accurate across pushes.
The Mintlify dashboard is configured with main-prod as the production deploy source; that is what actually serves docs.aviatrix.com.

Getting a preview URL (preview-<branch> is not enough)

Nothing to do — open a PR and the URL is posted as a comment. This section explains the mechanism, because the moving part is not obvious. Force-pushing preview-<branch> builds the servable tree, but Mintlify’s PR-preview feature only fires for pull requests targeting the project’s configured deployment branch (main-prod), and nobody opens PRs against that branch. So this workflow calls Mintlify’s preview-trigger API directly (POST /project/preview/{projectId}) right after the force-push, to get back a statusId/previewUrl it can poll and post. That explicit call has been proven live multiple times to be what actually provisions a live, publicly-routable preview site. Known, accepted tradeoff: this causes a duplicate build on every push. Force-pushing preview-<branch> also triggers an automatic Mintlify build on its own, via its GitHub App (observed in the dashboard as a build authored by the workflow’s shell-builder commit identity), independent of the explicit trigger call above. Every push therefore produces two builds. A rework was tried to eliminate this: drop the explicit trigger, rely solely on the automatic push-triggered build, and construct the preview URL deterministically (https://aviatrix-14b37c43-<target-branch>.mintlify.site) rather than reading it from the trigger response, polling that URL for HTTP 200 instead of a statusId. That rework was reverted after a live smoke test: a branch that only got the automatic build returned a fresh 404 at the constructed URL on repeated, independently-verified curls — the automatic build alone does not reliably (or does not at all) produce a publicly-routable site. Do not reintroduce the shell-builder-only / URL-construction approach without solving that gap first; the duplicate-build cost is accepted as the price of a preview that actually works. The mechanism, as restored:
  1. Trigger: POST /project/preview/{projectId} with the pushed branch name, retrying on HTTP 429 with backoff (up to 4 attempts). The response’s statusId and previewUrl are captured as step outputs.
  2. Poll: GET /project/update-status/{statusId} every 10 seconds, up to a 5-minute deadline, until status is anything other than the documented in-progress states queued/in_progress.
  3. Hardening added on this pass: the poll loop used to only break on an exact success/failure match. In one live test, the triggered build was marked "Skipped — Superseded by a newer commit" — an undocumented status outside the API’s stated queued/in_progress/success/failure enum — and the old loop ran out its full 5-minute timeout without recognizing it. The loop now breaks on any non-empty, non-in-progress status and reports it as-is; the PR comment renders a distinct “Mintlify reported an unusual status” message for anything other than success/failure, instead of misreporting it as “still building”.
  4. The workflow upserts a single marked comment on the PR with the outcome and the source SHA.
MINTLIFY_API_KEY and MINTLIFY_PROJECT_ID repo secrets are checked before the trigger call is made at all — unset either to skip the trigger/poll/comment entirely (a deliberate opt-out knob), not just to suppress a comment for calls that already happened. Manual fallback. scripts/preview-branch.sh runs the same build → force-push → API-trigger → poll sequence for a branch that has no open PR (so the automatic push-triggered build is the only other thing that fires), or to force-refresh a stale preview. It was not touched by the URL-reachability rework or this revert, and its trigger/poll mechanism already matches publish-mint-deploy.yml as restored above.
The /refresh-preview [branch] slash command wraps it. Requires gh (authenticated with workflow scope) plus the same MINTLIFY_API_KEY / MINTLIFY_PROJECT_ID credentials the CI workflow uses.

Cleanup (cleanup-preview-branch.yml)

When a PR closes (merged or not), this workflow deletes its preview-<branch>. It is a no-op if the branch was never created (for example, a PR with no source changes).

PDF generation

Not part of CI — run locally, and requires PrinceXML (brew install --cask prince):
Each target starts a temporary local mintlify dev server, extracts the relevant page URLs from docs.json, renders them with prince, and tears the server down.

See also

  • README.md — repo layout, local dev quick start.
  • VERSIONING.md — snippet → shell → nav generation internals, per-version authoring patterns.
  • CONTRIBUTING.md — editing standards, voice/style, branching conventions.