Per-PR checks
Every auto-fix check on this page shares one behavior worth knowing about: because this repo is public, a workflow’s own
GITHUB_TOKEN push lands its own re-triggered run in “action required” instead of running automatically - GitHub gates it behind a one-click manual approval (Actions tab > the pending run > Approve and run) rather than silently re-running. This isn’t a failure and doesn’t mean anything is broken: the fix commit has already landed by this point, and approving just lets the check confirm (correctly) that there’s nothing left to do. Safe to leave unapproved indefinitely if the check isn’t a required status for merging.Database enum sidebarTitle auto-fix
Only triggers on a PR touchingen/database/tables/enums/**, a tree wholly generated by an external ADO pipeline, with no source file in this repo. Every page’s title follows one fixed shape, Enum values for <Name>, which otherwise renders unabbreviated in the sidebar. Unlike every other check on this page, this one doesn’t just warn or fail - it derives a short sidebarTitle from <Name>, and if any changed page in the PR is missing it, pushes a fix commit directly onto the PR branch. Only files matching the exact Enum values for <Name> shape are touched; anything else is left alone rather than guessed at. Because the fix is idempotent (a page that already has sidebarTitle is left untouched), the fix commit’s own push re-triggers this same check but produces no further commit - it self-terminates rather than looping. Skipped entirely for PRs from a fork, since the default token can’t push back to a fork’s branch.
Generated-reference auto-fix
Consolidates 4 fixers that all touch the same overlapping generated-reference trees (en/api/mdo-providers/reference/, en/api/archive-providers/reference/, en/api/reference/webapi/, en/database/tables/) into one GitHub Action job, triggered on the union of their paths. Running these as separate workflows, each with its own commit-and-push step, would make them race to push to the same PR branch ref with zero coordination. One job means one checkout and one push, which removes the race by construction within this workflow.
This workflow, database-enum-sidebar-title.yml, openapi-conversion-guard.yml, recent-pages-sync.yml, and the Reserved version frontmatter auto-fix below are still 5 separate workflows that can all push to the same PR branch - database-enum-sidebar-title.yml’s path is a subset of this workflow’s en/database/tables/ scope, and Recent-pages sync’s en/** is a superset of both, so more than one can trigger on the same PR. That remaining gap was closed without merging the workflows’ domains together (database/openapi intentionally stay separate from this “generated reference” domain): all 5 now share one concurrency: group: autofix-push-${{ github.ref }} instead of each having its own workflow-scoped group, with cancel-in-progress: false so a second workflow queues behind the first’s push instead of racing it.
The job runs its 5 fixers in sequence, each idempotent, and pushes at most one fix commit covering whatever changed:
- MDO-provider title/sidebarTitle - only acts on files this PR actually changed under
en/api/mdo-providers/reference/**, a tree wholly generated by an external ADO pipeline with no source file in this repo. Most page titles are short, but a subset share one structured shape -relationssearchlistprovider<entity><entity?><active|passive>, from a closed 5-entity vocabulary (appointment, contact, document, person, sale) - which reads poorly as a title and renders as one long unbroken string in the sidebar. This fixer PascalCases every recognized word into a fulltitle(e.g.relationssearchlistproviderappointmentappointmentpassive->RelationsSearchListProviderAppointmentAppointmentPassive), then derives a shortsidebarTitlethat drops the shared prefix and space-joins the rest (-> “Appointment Appointment Passive”). Only the confirmedrelationssearchlistprovider*shape is touched - other long, irregular one-off names in the same tree (e.g.ticketselectionwithstaticmenu) are deliberately left alone rather than guessed at. - Reference-page overflow - re-applies a fix to every page on two small, human-curated lists (
tools/ci/wide-mode-pages.txt,tools/ci/code-wrap-pages.txt) on every run, not just this PR’s changed files, so a future ADO regen that drops the fix gets it re-added automatically.wide-mode-pages.txtpages get Mintlify’smode: "wide"frontmatter (hides the right TOC, keeps the left sidebar) - an existing, differentmode:value is left untouched and reported as a conflict rather than overwritten.code-wrap-pages.txtpages get their fenced code blocks wrapped in a<div className="wrap-code-samples">marker, whichstyle.cssscopes awhite-space: pre-wrapoverride to - used where losing the TOC was judged worse than a wrapped code line, and deliberately not a site-wide CSS change. Append a page path (no extension) to either list whenever a new overflowing page is found. - WebAPI bare-relative links - rescans the whole
en/api/reference/webapi/tree rather than only this PR’s changed files, since a large regenerated-content drop can touch every file at once and passing that many changed-file paths through a workflow environment variable isn’t reliable. This tree is generated wholesale by an external ADO pipeline with no awareness of Mintlify’s routing: every internal link is emitted as a bare same-folder filename (SuperOffice.WebApi.Agents.AgentBase.md, optionally with an escaped anchor), which resolves fine on disk but 404s live. Rewrites any bare-relative destination to a root-relative path (Some.Class.md->/en/api/reference/webapi/Some.Class, unescaping\_), skipping fenced code/inline code spans. A destination carrying a path segment - never observed in this tree - is left untouched and reported rather than guessed at. - Dead
<see cref>references - runs last, since it only ever emits root-relative links (never the bare-relative shape the previous step corrects), so nothing after it needs to re-touch its output. Rescans all ofarchive-providers/mdo-providers/webapi(same idempotent full-tree-rescan reasoning as step 3 - these are also regenerated wholesale).<see cref="T:Namespace.Type">Label</see>(and its self-closing and HTML-escaped forms) is .NET XML-doc-comment syntax DocFX used to resolve into a real link; Mintlify’s MDX renderer either drops it silently or renders it as garbled entity text. The fixer builds a type -> page lookup from each reference page’s own self-declaration sentence (“This provider/list name is implemented by the class …”) plus a secondary match againsten/api/reference/webapi/filenames by short type name, then rewrites every occurrence to a real link where one resolves, or to plain display text otherwise (always plain text inside frontmatter, since a markdown link in a YAML string renders as nothing).
Reserved version frontmatter auto-fix
Only triggers on a PR touching.md/.mdx files (repo-wide, apart from a short list of excluded generated/reference paths - see the workflow file for the current list). Mintlify’s search back end treats version as a reserved search-filter field and 400s on the non-string value our own version property produced - so the property was renamed to so_version repo-wide. Like the two auto-fix checks above, this doesn’t just warn - it rewrites version: to so_version: directly in any changed file’s frontmatter and pushes a fix commit onto the PR branch, so an external pipeline that reintroduces the old key gets corrected automatically rather than needing a human to notice a warning and fix it by hand. The rename is a pure key swap - the value and every other frontmatter line are left untouched. Idempotent (a file that already uses so_version: is left alone, so the fix commit’s own push produces no further commit), and skipped entirely for PRs from a fork, since the default token can’t push back to a fork’s branch.
OpenAPI conversion auto-fix
Only triggers on a PR touchingapi-sources/openapi/agent/** or api-sources/openapi/rest/** - hand-maintained Swagger 2.0 sources refreshed by an external NetServer/ADO build, not authored in this repo. Mintlify renders OpenAPI 3, so every source file has a converted counterpart under openapi/agent/ or openapi/rest/, plus an entry in config/nav-restful-agent-openapi.json or config/nav-restful-rest-openapi.json wiring it into navigation - both are workaround output this repo generates and commits ahead of a native OpenAPI 3 pipeline that hasn’t shipped yet. Before this check existed, a source refresh had no mechanism forcing the derived output to be regenerated in the same PR, so it could silently go stale.
For each PR, the check converts only the added/modified source files (not a full re-conversion of every spec) via tools/convert-swagger-to-openapi.ps1, removes the output for any deleted source file, then regenerates both nav files from the current openapi/{agent,rest}/ directory listing so a newly added or removed spec is wired into (or dropped from) navigation without a manual edit. If any of that produces a real diff, it’s pushed back onto the PR branch as a fix commit, same as the other auto-fix checks on this page.
The converter also works around one known upstream bug: two source files (Swagger-v1-QuoteAgent.json, Swagger-v1-Selection.json) $ref a schema definition that the NetServer Swagger generator omits from the same file, which otherwise makes conversion fail outright. The converter copies the missing definition in from a sibling file that still has it, against a throwaway in-memory copy - the tracked api-sources/ file itself is never edited, since that folder is pipeline-owned.
This whole check, plus tools/convert-swagger-to-openapi.ps1 itself, is a workaround for the lack of a native OpenAPI 3 pipeline output. Once that pipeline ships, delete this workflow and the converter script outright rather than trying to keep them in sync with a pipeline that no longer needs them; tools/ci/generate-openapi-nav.py is the one piece that may be worth keeping as a standing nav-drift check regardless of how the underlying specs get produced.
Recent-pages sync auto-fix
Triggers on a PR touchingindex.mdx itself, or anything under en/**/integrations/**. The homepage’s “What’s new” list is driven by index.mdx’s own recent_pages frontmatter - see Metadata properties for the property’s shape - and resolved into a separate <RecentPages items={[...]} /> call in the page body, baked in as a literal prop rather than fetched at runtime.
Each run: detects brand-new pages this PR added under en//integrations/ only (not repo-wide - reference trees like database//automation/crmscript/reference/ regenerate too often to be a useful signal) and adds them; stamps a missing since with today’s date, and refreshes a pinned entry’s since if this PR touched that entry’s own target page; drops any entry whose target is generated: true; drops non-pinned entries older than 120 days; caps the combined list at 7, with pinned entries getting guaranteed slots that still count against the cap. If any of that changes either the frontmatter or the body call, both are rewritten and pushed back onto the PR branch as a fix commit, same as the other auto-fix checks on this page. Idempotent (a page already in sync produces no further commit), and skipped entirely for PRs from a fork, since the default token can’t push back to a fork’s branch.
Markdownlint
Lints only the.md/.mdx files changed in a PR (or pushed directly to main) using this repo’s own .markdownlint.yaml rules. Results appear in the workflow run’s log in the GitHub Actions tab - a flagged line does not block the merge, but is worth fixing before requesting review. See markdownlint for how to interpret and fix a specific rule violation, and how to run the same linter locally.
Landing page template guard
Checks only the.mdx files changed in a PR (or pushed directly to main) for a mode: "custom" landing page that mixes the two custom-landing Blueprint templates’ card-body markup: a category-landing page (id="category-landing") containing a subcategory-style linklist block, or a subcategory page (id="subcategory") containing a category-landing-style concept-links list. style.css only styles .linklist under #subcategory, so a mismatch like this renders with unstyled, browser-default markup instead of the intended layout, without tripping any other check, since the two templates are only inconsistent with each other, not individually invalid MDX. It never fails the check - a hit shows up as a warning annotation on the relevant line in the PR’s Files Changed tab.
Release-notes API structure guard
Only triggers on a PR touchingrelease-notes/*/api/**. release-notes/{major}/api/ uses a flat structure - generated files sit directly under MAJOR/api/, using their original generated filename, with no per-minor-version subfolder (see Add or update release notes). This check warns when a changed file sits one directory level deeper than that instead - release-notes/{major}/api/{subfolder}/.... Historical dotted-version trees (release-notes/10.1/api/, release-notes/9.2/api/, etc.) use their own, different, frozen structure and are excluded by design - the check’s path match requires a digit-only major version, which those don’t have. It never fails the check - a hit shows up as a warning annotation on the PR’s Files Changed tab, naming the file.
Index page relative-links guard
Checks onlyindex.md/index.mdx files changed in a PR (or pushed directly to main), excluding the machine-generated reference trees. A page built from a literal index.md/index.mdx file is served at its folder’s own bare path with no trailing slash, so a relative link (./..., ../...) authored on that page resolves against the wrong parent folder in production - the target genuinely exists on disk, so it’s invisible to every check that resolves links via filesystem path math (mint validate doesn’t check links at all, and tools/triage-broken-links.py resolves against the file’s disk location, not the live URL). This bug class was found and fixed repo-wide once already; this guard exists so it can’t quietly reappear. It flags both markdown links (inline and reference-style) and raw HTML/JSX href values, skips MDX import statements, code blocks/spans, and frontmatter. It never fails the check - a hit shows up as a warning annotation on the relevant line in the PR’s Files Changed tab. See How to use links in docs for the underlying rule.
Md extension links guard
Checks every.md/.mdx file changed in a PR (or pushed directly to main), excluding the machine-generated reference trees. Mintlify serves a page at its extension-stripped path, never at the source file’s literal .md/.mdx name - so a link authored with the extension still attached (.../index.md#some-heading) is broken in production, even though the target file genuinely exists on disk and resolves cleanly under tools/triage-broken-links.py’s own filesystem-path check. This bug class was found and fixed repo-wide once already; this guard exists so it can’t quietly reappear. It flags markdown links (inline and reference-style) and raw HTML/JSX href values, skips external URLs, MDX import statements, code blocks/spans, and frontmatter. It never fails the check - a hit shows up as a warning annotation on the relevant line in the PR’s Files Changed tab. See How to use links in docs for the underlying rule.
Stale OpenAPI links guard
Checks every.md/.mdx file changed in a PR (or pushed directly to main), excluding the machine-generated reference trees. en/api/reference/restful/agent/* and en/api/reference/restful/rest/* have no markdown source file - they’re generated at build time from openapi/agent/openapi-v3-*.json and openapi/rest/openapi-v3-*.json - so a link using the old DocFx URL shape ({Tag_Agent}/{operationId}, {PascalCaseEntity}/index, or a bare tag with no operation) only 404s live; it’s invisible to every check that resolves links against this repo’s own tracked files. This bug class has surfaced independently several times, always reactively; this guard builds a live tag/operation lookup from the current OpenAPI specs and flags anything that doesn’t resolve against it, so it can’t quietly reappear. It never fails the check - a hit shows up as a warning annotation on the relevant line in the PR’s Files Changed tab. See How to use links in docs for the underlying rule.
Redirect coverage guard
Runs on every PR (fetch-depth: 0, needed to diff against origin/main). Detects any .md/.mdx file this PR renamed, moved, or deleted (via git’s own rename detection, not a plain add/delete pair), excluding snippets/ and the machine-generated reference trees - those aren’t standalone routable pages, so a missing redirect for one isn’t a real gap. For each one, checks whether config/redirects.json already covers the old URL, including the .html-suffixed duplicate form documented in Redirects. Separately, for a renamed en/ page, checks whether its translations were moved to the same relative location too - matched by path (swapping the language segment), not by uid. A translation left on its old path after the English page moves is a quieter version of the same failure mode. Never fails the check - a gap shows up as a warning annotation on the PR’s Files Changed tab, and adds the mintlify/nav-redirects label (or translation, for a mirroring gap) to the PR so it’s visible without opening the Actions tab; the label is removed automatically once a later push resolves the gap.
Office-format download guard
Checks every PR (and direct push tomain) for a newly added .xlsx/.docx/.pptx file anywhere under downloads/, via git diff --name-status against origin/main - a pre-existing tracked file isn’t re-flagged just because an unrelated PR touches something else in that folder. This is one of three blocking exceptions in an otherwise advisory guard family: Mintlify does not serve these three extensions at any plan tier (confirmed live - 404 on every .xlsx/.docx/.pptx under downloads/, while .pdf/.zip in the same folder return 200 - and against Mintlify’s own supported-file-types documentation), so a file added with one of these extensions is never downloadable at all once merged, not a stylistic or lint-level concern the other checks on this page warn about. All pre-existing instances were fixed once already, by zipping each file and repointing every link/redirect at the .zip; this guard exists so the same 404 can’t quietly reappear the normal way (see How to use links in docs for the authoring rule, added alongside this guard).
Unlike the changed-files-list checks elsewhere on this page, this script computes its own git diff against the base ref rather than receiving filenames from tj-actions/changed-files through a shell-interpolated ${{ }} expression - so the injection vector those checks guard against with env:-passed input doesn’t arise here in the first place, since no PR-controlled string is ever substituted into the workflow’s run: script text.
DocFX legacy-tag guard
Checks every PR (and direct push tomain) for a newly added occurrence of any of three .NET XML-doc-comment tags DocFX used to resolve at build time but Mintlify’s MDX renderer doesn’t - <see cref="T:..."> (or its self-closing/HTML-escaped forms), <xref:some.uid>/<xref href="some.uid" data-throw-if-not-resolved="false"></xref> (or either’s HTML-escaped form), and <list type="table">/<list type="bullet"> (or its HTML-escaped form). One workflow, one checkout, three sequential check scripts (check-no-new-docfx-see-cref.py, check-no-new-docfx-xref.py, check-no-new-docfx-list.py) - consolidated into a single job rather than three near-identical separate workflow files, the same reasoning as the Generated-reference auto-fix above. All three use the same base-ref git diff approach, at line granularity, not whole-file: only lines the PR’s own diff actually adds are checked, so an unrelated edit to a file that already has one of these patterns isn’t re-flagged. This is the second of three blocking exceptions in the guard family.
Each tag reached the same “silently breaks or garbles the page” fate once DocFX’s own resolution went away:
<see cref>- either breaks the build outright (an unmatched<) or silently drops the link, leaving only the bare type name visible. A repo-wide audit found thousands of pre-existing occurrences, concentrated in the generatedarchive-providers/mdo-providers/webapireference trees. The real fix - a resolve-or-plain-text conversion, now run automatically by the Generated-reference auto-fix above - keeps those three trees clean going forward; this guard is the safety net against a new, hand-authored occurrence anywhere else in the repo, which the auto-fixer never touches.<xref>- Mintlify’s MDX renderer doesn’t resolve either syntax form. A repo-wide sweep converted the known occurrences to real links, but that alone isn’t durable: a later ADO/generator content drop can silently reintroduce an already-fixed line by regenerating a page from its source, with no awareness of the Mintlify-side fix.<list type="...">- either dropped entirely as an unrecognized custom element, or (once escaped to avoid an MDX parse error) rendered as literal garbled tag soup on the page. The last remaining occurrences were found and hand-fixed; this check exists so the same tag can’t quietly reappear on a future content drop regenerating one of these pages from its original source.
Workflow script-injection guard
Checks every.github/workflows/*.yml file on every PR (and direct push to main) that touches .github/workflows/** - not just the changed files, the whole workflow set, since a change to one workflow can’t introduce a new occurrence in another. Flags any run: step that interpolates an untrusted ${{ }} expression (github.*, steps.*.outputs.*, needs.*.outputs.*, inputs.*) directly into the shell script text, instead of routing it through that step’s own env: block. Direct interpolation is a known GitHub Actions script-injection vector: the expression is substituted into the script’s source text before bash parses it, so an attacker-influenced value (a PR-diff filename, in practice) can break out of its intended argument position and run arbitrary commands on the runner.
This guard exists because two sibling workflows were once found to have exactly this bug; the fix landed directly on main before this guard existed, so the vulnerable pattern was never exposed in a public diff while it was live. This is the third of three blocking exceptions in the guard family: a workflow that can run arbitrary attacker-supplied shell commands on the runner isn’t a lint-level concern, the same reasoning as the Office-format download guard above.
Vale
Prose-style linting (tone, terminology, formatting), configured entirely by this repo’s.vale.ini - no separate setup needed once the add-on is toggled on in the Mintlify dashboard. Set to Warning level to match markdownlint’s advisory-only posture. Results show on the PR’s checks list from Mintlify’s own GitHub App.
Build validation - why there is no separate mint validate step
mint validate is a build/schema validator (docs.json schema, MDX parse errors, OpenAPI spec validation) - it is unrelated to Vale and does not check broken links or accessibility. Those are separate CLI commands (mint broken-links, mint a11y). It is not run as its own GitHub Action here because the PR preview deployment already performs the equivalent build validation on every PR, live, as part of generating the preview - adding a duplicate mint validate Action would just re-check what the preview build already checked. A failed build already surfaces as the red “Mintlify Deployment” check described in Deployment.
Broken links (per-PR)
Mintlify’s Broken Links add-on scans the whole site’s internal links (not external URLs) on every PR and reports results the same way as the Vale check above. It is set to Warning level for the same advisory-first reasoning as the other checks.A Mintlify community report (October 2025) describes a case where a page serving a 404 still returned an HTTP 200 status, which could make this check under-report a real broken link. Do not treat “no broken links flagged” here as a guarantee - see the scheduled check and fallback tool below for a second opinion.
CRMScript reference drift
Only triggers on a PR touchingapi-sources/crmscript/**, the generated en/automation/crmscript/reference/**, or either generator script - not on every PR like the checks above. It regenerates the reference fresh from source and fails if the result does not match what is committed, catching both directions of drift with one mechanism: a source .yml edited without regenerating, and a generated .mdx hand-edited directly without touching its source. The failure message names the specific source file to check. See Update the CRMScript reference for the full mechanism and why this one is blocking rather than advisory - api-sources/crmscript/*.yml also feeds the CRMScript interpreter’s own built-in intellisense, so a stale source is a product-facing risk, not just a docs one.
MDX escaping regression tests
Only triggers on a PR touchingtools/migration/fix-generated-mdx-escaping.ps1 or its own test file. That script sanitizes ADO-generated reference content (webapi/mdo-providers/archive-providers) so raw C# generics, XML-doc tags, and DocFx-style heading anchors don’t break MDX parsing. Editing that script without a way to check the fix against known-tricky input has caused real regressions before, each only caught by hand-reconstructing raw ADO input after the fact. This check runs a small Pester suite (fix-generated-mdx-escaping.Tests.ps1) covering raw/HTML-escaped/already-converted heading anchors, generic-type angle brackets inside heading text, fenced-code/inline-code protection, and idempotency on a second run - and fails the check if any of those regress, the same way the CRMScript drift check above fails on a real drift rather than just warning. Blocking rather than advisory for the same reason: a regression here silently breaks the MDX build on the next ADO content drop, not necessarily this PR’s own changes.
Stale generated pages
Covers three content trees generated by a pipeline outside this repo:en/api/archive-providers/reference/, en/api/mdo-providers/reference/, and en/database/tables/. Each has a matching nav-<tree>.json that gets rewritten whenever new or changed pages are pushed. When a source item on the generator side is deleted or renamed, the push removes its entry from the nav file, but nothing else removes the now-unreferenced page from this repo - it stays as a live, unlinked page indexed by site search. Unlike the CRMScript check above, there is no source file in this repo to regenerate from, so this only checks one direction: any page on disk that is no longer referenced by its tree’s nav file fails the check, naming the file. Pages not marked generated: true in frontmatter are never touched, even if they look stale - fix by either deleting the file (run tools/ci/find-stale-generated-pages.py --delete locally) or adding it back to the relevant nav-*.json if it turns out to be a real nav omission rather than an actual deletion or rename.
Scheduled check: repo-wide broken-link detection
A per-PR link check only catches breaks introduced by that PR. A separate, Mintlify-hosted automation periodically re-scans the entire site, catching drift from any source - including direct admin-bypass pushes that skip PR review entirely. Active since August 11, 2026, first run August 15, 2026 (nextRunAt confirmed via mint automations list).
-
Type:
broken-link-detection -
Cadence:
0 6 1,15 * *- the 1st and 15th of each month, run at 06:00. Standard cron has no native “every 2 weeks” field; this is the closest non-drifting approximation of a biweekly cadence (actual gaps land at 13-16 days depending on the month). - Auto-merge: off. When the automation finds and fixes a broken link, it opens a PR like any other change - it goes through the same manual review as everything else in this repo, it does not merge itself.
- What it does: for each broken internal link, it checks whether the target page was renamed or moved and updates the link, or - if the target was removed entirely - edits the surrounding copy to remove or replace the reference.
- Cost: billed in Mintlify credits per run (rough public estimate: ~285 credits/run) - a real operating cost, not a free background job.
mint automations list confirms it registered with status: active, no automerge field (off by default), and the cron/type above.
Fallback: manual broken-link check
tools/triage-broken-links.py plus the mint broken-links CLI remain the trusted manual method for a broken-link sweep, separating genuine breaks from known false positives. Reach for this:
- Right after a large structural change (a folder move, a nav overhaul) rather than waiting for the next scheduled automation run.
- As a second opinion if the scheduled automation or the per-PR add-on reports “no breaks found” but something looks off - see the HTTP-200-masking-a-404 caveat above.
Related content
- Mintlify deployment and CI/CD - the deploy pipeline and PR preview mechanics these checks build on.
- markdownlint - rule details and local usage.
- Reviewing a PR - the review checklist that links back here.
- Update the CRMScript reference - the regeneration workflow the drift check enforces.
- Acceptance testing - the on-demand checklist to run after a refactor, design change, or site reorg, rather than every PR.