Flude's documentation pipeline survived for years the same way most internal tooling does. It just worked. Nobody in their right mind wanted to touch it. The engine package generated its API reference using a clunky symlink hack we mentioned in the escape-to-GitHub-Actions episode. Platform constraints forced our hand here. Free GitHub Pages limits site publishing to public repositories only. The engine's source code absolutely had to stay private. We bypassed this by running ln -s ../engine ./user-docs/engine during the CI build. The public user-docs repo quietly built a site out of someone else's code. Flude processed those files on the fly to push the final static result out the door.
That duct-tape setup actually got the job done. It also created a concrete wall of architectural coupling. Publishing a single line of API documentation required the private engine to leak its source into a public neighbor. The user-docs project got stuck carrying the weight of an external build process. We couldn't build or test the engine's docs in isolation. The entire toolchain only triggered from inside the documentation repo. Every single run pulled down megabytes of fresh code. The design-docs project suffered from the exact same disease on a smaller scale. Its GitHub Pages deployment ran centrally out of the giant Pipeline umbrella repo. It relied on an SSH deploy key to punch through into a completely different repository.
Finding a host that asks fewer questions
We weighed two escape routes for the engine. One option involved shipping the parsed intermediate representation as a versioned artifact like a wheel or GitHub Release. Downstream projects could grab that package and render the docs locally. That sounds clean on paper. In reality, it introduces a nightmare of schema versioning we desperately wanted to avoid. The brute-force approach won out in the end. The engine now parses, renders, and deploys its own reference from its own CI pipeline. Zero intermediate contracts. Zero artifact publishing rituals.
We still had to bypass the old GitHub Pages restriction. The platform demanded a public repo—so we ditched the platform. Cloudflare Pages offers a free tier that deploys finished build artifacts while completely ignoring repository visibility. The engine finally publishes its own reference while staying hidden from the world. The actual migration required nothing more than copying Doxyfile, hugo-site, and sidebar.toml directly into the engine repository. The generated content/ and public/ directories went straight into .gitignore. They no longer haunt our commit history as static fixtures.
We rolled this exact logic down the rest of the chain. Both design-docs and user-docs migrated to Cloudflare Pages. Each project received a dedicated deploy.yml workflow. GitHub Actions provides native read access to the runner's repository, making deploy keys obsolete. The old cloudflare/pages-action@v1 had already been archived by the community. We skipped the legacy tooling and moved all five repositories straight to the supported wrangler-action.
One token to rule them all
Our perfectly clean plan slammed into a brick wall of reality right about here. Every deployment requires a CF_API_TOKEN. The textbook reaction is generating a narrowly scoped token with least privilege for each repository. Cloudflare violently disagrees with this approach. You cannot scope an API token to a single Pages project. They grant permissions across the entire account—or nothing at all. Any "narrow" token we issued would instantly gain full access to our entire infrastructure. Worse yet, Cloudflare displays the token value exactly once during creation. If you miss it, you have to roll the credential entirely.
The rollout process turned into a timed sprint. We roll the token once. Then we distribute the new string across five repositories within a few frantic minutes. Everything happens through an interactive prompt. Secrets never touch a script, a file on disk, or a Slack message. Five independent repositories now share a single token—and a single rotation window.
Auditing the dead code
Once the deployments turned green, we had to ask ourselves a tough question. How much of this machinery was actually doing anything useful? The audit unearthed a pile of historical garbage. A repository_dispatch trigger from every submodule used to restart integration tests in the umbrella repo. It fired before the gitlink even bumped. The CI was aggressively testing a state that hadn't changed. We gutted that nonsense and replaced it with a simple six-hour schedule on the bump job itself. We lost absolutely nothing in speed because those PRs never auto-merged anyway.
The language test matrix in the umbrella repo turned out to be a carbon copy of the engine's internal CI. The original version even enforced a stricter coverage threshold. A job designed to verify the built site was quietly testing an architecture that died months ago. It merged Hugo's output with a VitePress tree to check cross-links. We had long since split them into separate Cloudflare Pages projects on different domains. The only connection left is a standard external link. You never spot these ghosts reading a clean architecture diagram. They only surface when you dig through the raw runner logs.
Severing the final tie
One tiny cross-repository hook remained. The failure-alert step in every workflow dynamically pulled a shared Telegram notifier from Pipeline at runtime. That single step required another authentication token. A supposedly independent repository was still reaching back to daddy for help. We copy-pasted that action file into every single project. Maintaining five copies instead of one shared source is the real cost of independence.
You are reading this blog on a site built with the exact same pattern. It uses self-deployment with zero external deploy keys. It runs its own copy of the notifier. It lives on that exact same Cloudflare architecture.
Next time, we'll explain how we hunted down a public name for this documentation. The original working title turned out to be locked behind corporate lawyers, sending us down a rabbit hole of weird domain names.
Originally published on our blog: https://blog.flude.guide/blog/decomposing-the-pipeline-monorepo
Also read us:




Top comments (0)