FernDesk sync (Mintlify → help center)
Staging vs Production
FernDesk sections (not tags) are the env boundary:
Collections are mirrored under both sections (
Getting Started, Chat, Code, Bot, …).
Article keywords carry env:production|staging plus a Mintlify path fingerprint.
Mechanical sync (durable)
.github/workflows/ferndesk-sync.yml
pushtomain(docs paths)workflow_dispatch(choose production|staging)repository_dispatchtypeferndesk-sync(backend prod deploy hook)
FERNDESK_API_KEY.
Write-path rate limits (429)
POST /articles answers 429 / {"code":"rate_limited"} under load. The write
path (article create, update, publish, and collection create) backs off
exponentially — 5s doubling to a 180s cap, the slower ladder 429 needs; 5xx and
CF 1010 keep 3s doubling to 90s — and waits for Retry-After (delta-seconds or
HTTP-date) whenever the server sends it and it is larger.
Retries stay bounded on three axes, so a rate-limited run finishes and reports
instead of hanging:
Reads share the 12-attempt default. A
Retry-After above 300s is treated as
unusable and the exponential ladder is used instead. Hard 4xx (400/401/404)
fail immediately without retrying.
One page that exhausts its retries does not abort the rest of the run: the sync
continues, then logs a FAILURES line, records failed / failed_slugs in the
SUMMARY, and exits 1. Nothing is cached for a failed write, so the next run
retries that page cleanly. Re-run the sync (or the workflow) once the limit
clears.
A create that trips a slug conflict (409/422, or a message naming the slug) is
recovered by looking the article up and PATCHing it, so a stale cache or a
pagination miss does not surface as a failure.
Slugs and titles (FernDesk POST 400)
FernDesk accepts a slug only in[a-z0-9]+(-[a-z0-9]+)*. It answers POST /articles with 400 for anything else, so problems/bad_request used to be
sent verbatim as problems-bad_request and rejected: 19 problems-* pages
failed while problems-conflict, problems-forbidden, and problems-gone
created fine.
The MDX file name cannot change — it matches ErrorCode::as_str and the page’s
/problems/{code} type URI — so the slug is sanitized at the sync boundary by
sanitize_slug(): lowercased, every run of other characters (_, /, spaces,
--) collapsed to one hyphen, ends trimmed. problems/bad_request →
problems-bad-request, api/errors → api-errors.
Two source paths that sanitize to one slug (a_b.mdx and a-b.mdx) would
silently overwrite one article, so discover_pages raises instead of syncing.
Titles are separate: a bare snake_case code is not a usable article title, so a
page whose title is bad_request is published as Bad request (bad_request)
using its description. The title keeps the snake_case code, since that is the
wire contract readers search for.
scripts/tests/ferndesk-sync-retry.test.py holds both rules: it runs
discover_pages over this repository and fails if any problem page produces an
illegal slug, keeps an underscore, collides with another page, or keeps a bare
snake_case title.
Factory Droid path (agent)
When Manager Deploy lands prod and content needs judgment (rewrites, gap fill, migration QA), launch Factory Droid only:- model:
custom:deepseek/deepseek-v4.1-flash - effort: medium
- never Cursor Cloud / CloudAgent, never
deepseek-v4-pro
scripts/ferndesk_sync.py against the tip of CortexLM/docs main with FERNDESK_TARGET=production, then report the SUMMARY JSON. Do not paste the API key into the prompt — use the Droid/host secret store.
Safety
- Sync upserts by slug; it does not delete FernDesk-only articles.
- Mintlify remains the source of truth in git until cutover is complete.
python3 scripts/tests/ferndesk-sync-retry.test.pycovers the write-path retry policy offline (faked transport, virtual clock).
Cloudflare / GitHub Actions
FernDesk sits behind Cloudflare. GHA’s stock Pythonurllib TLS fingerprint triggers
Error 1010 (browser_signature_banned). The sync script uses curl_cffi with
impersonate="chrome" in CI (Chrome-like User-Agent + Accept; retries 403/1010 with
backoff). Local runs fall back to urllib only if curl_cffi is not installed.
Separately: the FernDesk UI still needs Connect domain for docs.cortex.foundation
HTTPS. That custom-domain 403 is unrelated to CF 1010 on api.ferndesk.com.
