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 ofREADME.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.
make clean && make all— must produce a completedocs.json.python3 scripts/check_version_gaps.py— a page missing from a newer version must be documented by aremoveentry innavigation-parser/subsections/overrides/.python3 scripts/check_nav_shells.py— every nav entry resolves to an actual generated shell file.python3 scripts/lint_docs.py— frontmatter,{props.X}references, version-templated doc links, absolute image paths.python3 scripts/check_image_duplicates.py— no duplicate image assets.python3 scripts/check_links_images.py— internal links and image references resolve.python3 -m pytest scripts/tests/— generator unit tests.- A final check that
docs.jsonexists, is valid JSON, and contains the expectednavigation/namestructure.
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 asv10.1-devare included. workflow_dispatchwith atarget_overrideinput → an arbitrary branch name. Rarely needed now that every PR is covered; useful for previewing a branch that has no PR open yet.
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:
- Check out the PR’s head ref (or the pushed ref).
make all— regeneratedocs.jsonand the shells.rsyncthe 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.gitignoreitself — excluding the.gitignorematters, otherwisegit add -Aon the deploy branch would silently drop the generated output it’s supposed to ship).- Force-push that pruned tree as a single commit to the target branch
(
preview-<feature-branch>, ormain-prodfor a production push). - 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 astatusIdandpreviewUrl. This step checksMINTLIFY_API_KEY/MINTLIFY_PROJECT_IDare set first and skips cleanly if not — see Getting a preview URL for why the explicit call is necessary at all. - Poll
GET /project/update-status/{statusId}every 10 seconds, up to a 5-minute deadline. The loop breaks as soon asstatusis any non-empty value other than the documented in-progress statesqueued/in_progress— not only on an exactsuccess/failurematch — and reports that raw value. This is hardening for an undocumented status Mintlify has been observed to return outside its statedqueued/in_progress/success/failureenum (see Getting a preview URL); without it, that case would loop silently for the full timeout instead of being recognized. - 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 onsuccess, a build-failure message (with Mintlify’ssummary) onfailure, 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 ifMINTLIFY_API_KEY/MINTLIFY_PROJECT_IDare unset — plus the source SHA. Upserting rather than posting once keeps the comment accurate across pushes.
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:
- Trigger:
POST /project/preview/{projectId}with the pushed branch name, retrying on HTTP 429 with backoff (up to 4 attempts). The response’sstatusIdandpreviewUrlare captured as step outputs. - Poll:
GET /project/update-status/{statusId}every 10 seconds, up to a 5-minute deadline, untilstatusis anything other than the documented in-progress statesqueued/in_progress. - Hardening added on this pass: the poll loop used to only break on an
exact
success/failurematch. In one live test, the triggered build was marked"Skipped — Superseded by a newer commit"— an undocumented status outside the API’s statedqueued/in_progress/success/failureenum — 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 thansuccess/failure, instead of misreporting it as “still building”. - 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.
/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):
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.