The problem was not that the build was long; it repaired too much
Blupoli’s pipeline had grown by accumulation. Each new need found a locally reasonable answer: one script normalized links, another fixed metadata, another repaired branding, another copied assets, and another patched localized routes. The problem appeared when the complete sequence looked less like a build and more like a chain of repairs.
PR #162 started from a simpler rule: canonical sources, deterministic build, then validation. That replaced a pattern of generate, patch, normalize, patch again, and finally check. If output repeatedly needs repairs after generation, either the source or the builder that created it is not fully owning its responsibility.
The goal was to reduce real work rather than hide it behind wrappers. The PR documented before-and-after metrics so the simplification could be demonstrated instead of merely described.
From 45 scripts to 22
Before the refactor there were 45 .mjs files under scripts/. Afterwards there were 22. The reduction did not come from renaming twenty commands behind one orchestrator. It came from removing passes, normalizers, and repair scripts whose responsibility could move into an existing canonical source or builder.
That required asking why every script existed. Some were remnants of migrations that had already become permanent product decisions. Others repaired output that could now be produced correctly. Some walked the entire HTML tree to apply a transformation at almost the same point where another builder was already visiting it.
The useful question was not “can this file be deleted?” but “who should actually own this responsibility?” That prevented a cosmetic cleanup that would have preserved the same complexity under different names.
From 34 build steps to 12
npm run build went from 34 steps to 12. The current sequence builds the Puzzles base, enriches static HTML, generates localized routes, localizes games and blog content, runs the remaining editorial visual and SEO stages, builds editorial architecture and the web app, applies final release status, and closes with check-output.
Every global pass has mental and technical cost. A script that scans the whole tree can create order dependencies, add runtime, and make it harder to know where an incorrect value originated. Fewer phases make ownership more visible.
Twelve is not a magic number and the pipeline is still intentionally multi-stage. The goal was to retain boundaries that protect genuinely different responsibilities and remove stages that existed mainly to repair a previous one.
Check went from six phases to three
npm run check dropped from six steps to three. check-source now integrates JavaScript syntax, catalogue and engine integrity, self-tests, onboarding coverage, and English i18n and taxonomy checks. Locale readiness and translation status remain separate because they are useful manual tools as well as gates.
The consolidation has one important rule: a gate may group validations, but it should not modify files to make itself pass. check-source verifies source. It does not repair source.
That distinction between building and validating became one of the most important outcomes of the refactor. A final gate that mutates output is not really a gate; it is another hidden build phase.
Devlog became a canonical source instead of a post-build rename
The same PR completed an editorial migration that had been hanging around the pipeline. content/building-public.json became content/devlog.json, editorial.json.buildingPublic became editorial.json.devlog, and the public section now uses devlog and /devlog/ routes. Older /building/ and /building-in-public/ paths remain only for compatibility.
Previously, scripts existed to migrate, normalize, or rename that identity after generation. Moving the decision into source removed normalize-devlog-name, migrate-devlog-editorial, and related repair behaviour.
It is a compact example of the whole philosophy: if the product is called Devlog, the builder should receive Devlog. Generating another identity first and spending phases converting it afterwards creates work with no product value.
Post-build repair scripts were the first large category to disappear
Removed scripts included link-game-control-sizing, normalize-localized-blog-links, normalize-public-game-counts, normalize-editorial-metadata, normalize-devlog-name, migrate-devlog-editorial, separate-blupoli-content, normalize-blupoli-branding, and link-journal-diagram-css.
Each name tells a small debt story: something had already been generated and a later phase needed to link, normalize, separate, or correct it. The refactor moved those decisions toward the source or builder capable of producing the right output initially.
Deleting a repair script is only safe when its responsibility reappears in the correct owner. Removing files without moving behaviour would reduce line count while increasing bugs. The review therefore focused on ownership rather than mass deletion.
Editorial architecture lost an entire patch layer
The refactor also removed build-public-journal, localize-editorial-covers, normalize-editorial-covers, editorial-quality-gate, prepare-web-editorial-locales, finalize-blog-assets, enrich-journal-index, and serve-dist. Canonical covers are generated once, while editorial validation lives in finalize-editorial.
This connects directly with write-devlog and write-blog. The long-term direction is that each skill produces complete editorial source — content, metadata, and original visuals — and the build transforms and validates it rather than inventing missing editorial pieces late in the process.
The architecture is not fully at that endpoint yet. Two stages remain as explicit debt because they still create real value, and documenting those exceptions was as important as deleting the old ones.
blog-editorial-visuals remains because it still creates real content
blog-editorial-visuals.mjs was deliberately not removed. It still generates infographics that are missing from the canonical sources of some existing articles. Removing it immediately would drop real content rather than merely simplify plumbing.
The correct next step is not to hide the script inside another wrapper. It is to migrate those visuals into canonical assets produced by editorial skills. Once the source contains everything required, the build can validate and copy instead of generating editorial meaning.
This exception prevented “fewer scripts” from becoming an absolute objective. Architecture should become simpler, but not by silently removing capabilities that still have no replacement.
seo-finalize also keeps one unresolved responsibility
seo-finalize.mjs overlaps partially with the editorial builder but still generates related-article and series navigation. It therefore remains. Much SEO metadata can already be produced earlier, but this specific relationship data needs a better owner before the phase can disappear.
The principle is the same: do not delete until responsibility has somewhere better to live. Explicit debt is healthier than hidden debt because the pipeline becomes a map of what remains to migrate.
When related navigation becomes canonical editorial data or builder-owned output, seo-finalize can shrink or disappear without changing published behaviour.
Fewer global walks through generated HTML
copy-shared-footer-assets, inject-observability, and audit-improvements stopped existing as standalone phases. Builders were already visiting documents at points where deterministic assets, observability, and metadata could be applied without another full-tree pass.
Global passes are tempting because they let a feature ship quickly without changing existing architecture. Their cost appears later as implicit ordering, extra runtime, and uncertainty over which script last modified a node.
Absorbing them does not mean building one enormous loop. It means using the phase that already has the right context and requiring that phase to emit more complete output.
i18n simplified with one deliberate exception
localize-interface.mjs was genuinely absorbed into localize-routes.mjs. localize-games.mjs stayed separate because it structurally protects and replaces each game payload. Not every operation containing the word localize has the same invariants.
A simplistic cleanup might have merged both just to reduce command count. Keeping game localization separate preserves a boundary around embedded data and engine structure.
The result is fewer phases without losing clarity: routes and interface share enough context to live together, while games retain a specialized transformation that protects different constraints.
Release status no longer needs a prepare phase
The old pipeline had a specific release preparation stage. Now build.mjs generates releaseStatus and catalog-stats.json directly, while apply-release-status remains only as the final publication finalizer.
This makes it easier to reason about which catalogue is being built and when a game is considered publishable. The finalizer can focus on removing surfaces or links for unpublished games and validating resulting counts.
Again, the improvement is not only one fewer command. It is a clearer boundary: the build produces state; the finalizer applies publication policy.
check-output became a gate that does not repair
The final gate now checks that legacy branding is absent, Blupoli branding and Blog/Puzzles boundaries are correct, native-engine CSS is present, and visual configuration stays in parity across locales. The old enforce-game-visual-parity step disappeared.
Before removing it, native CSS correction moved into the builder. That detail is essential: check-output may report invalid output, but it should not silently fix it. Failure should lead back to the canonical source or builder responsible for the defect.
A non-mutating final gate preserves traceability. The invalid state remains visible instead of being transformed into apparently valid output by a last-minute repair.
CI simplification took several iterations
The build refactor overlapped with a practical GitHub Actions problem: a pull request could run full validation and a Firebase preview through separate paths, duplicating work on the self-hosted runner. PRs #165 and #166 consolidated Quality and preview into one flow and removed the duplicate automatic preview workflow.
Quality became the general gate: verify plus Playwright, followed by Firebase preview reusing the already-built dist. If Firebase returns a preview-channel quota error such as HTTP 429, preview can fail without pretending that the code validation itself failed.
Castle Wall stress testing also became path-sensitive. Editorial or unrelated E2E changes no longer trigger an expensive generator stress run that cannot be affected by those files.
Cancel obsolete work, but not a valid deployment
The new concurrency policy uses cancel-in-progress for updates to the same pull request. When a newer commit arrives, there is no value in making the self-hosted runner finish validating an obsolete revision that can no longer be merged as-is.
Production exposed a more delicate edge. After the Zebra theme merge, a validated deployment could be blocked by later workflow behaviour. PR #169 adjusted the flow so production deploys a main revision whose Quality run succeeded, while a failing later Quality run does not cancel a previously validated deployment.
The lesson was that concurrency cannot treat every job as equivalent. An old PR revision is obsolete. A validated production artifact is not obsolete merely because another revision started testing.
Quality returned to main
One intermediate iteration reduced work by avoiding another Quality run after merge. The production issue showed that this removed a useful guarantee. PR #169 restored Quality on every push to main and made production deployment depend on that exact workflow succeeding.
This is not a contradiction with CI simplification. The objective was never “run less at any cost.” It was eliminate duplication and place checks where they add real safety. Validating the exact main commit being published does add real safety.
The resulting flow is more selective: one complete validation on the pull request, one validation of the final main revision, and deployment only after the correct signal.
Deployment became path-aware too
Production workflows were restricted so changes with no site impact do not trigger deployment. Internal documentation or unrelated paths should not consume the runner and Firebase when there is nothing publishable to update.
The optimization looks small next to deleting 23 scripts, but queue time matters on a self-hosted runner. Every unnecessary build delays work that genuinely needs a browser, full compilation, or stress test.
Expressing relevance in workflow paths turns CI into part of architecture: different commits deserve different phases, and that difference should be visible rather than implicit.
The simplification made sources of truth visible again
When generated output passes through many repair scripts, it becomes difficult to answer which file is truly canonical. After this iteration, more decisions live clearly in games/, content/, static/, and specific builders. Validators inspect those decisions instead of creating them.
That helps both people and agents. An editorial task can update content/devlog.json and its article without guessing which normalizer will rewrite the result later. A game task can look for source configuration rather than reverse-engineering dist.
Fewer implicit transformations also make diffs more explainable. When output changes, there are fewer candidate phases that may have rewritten the same value.
We did not hide complexity inside a new orchestrator
It would have been easy to reduce package.json to one or two commands by introducing a large script that internally called the same 34 steps. That would improve a superficial metric while making inspection worse. PR #162 deliberately avoided that trap.
The twelve current build stages remain visible because they represent real responsibilities. When a stage can truly disappear, its behaviour moves to the canonical source or builder that should already own it. When it cannot, it stays documented as debt.
The reduction is therefore auditable: scripts 45 to 22, build 34 to 12, check 6 to 3, and workflows 5 to 4. Those are fewer real units of work, not merely fewer words in a command.
The broader lesson: a healthy build produces, then validates
The clearest description of the new direction is canonical source, deterministic transformation, then non-mutating validation. When a late phase needs to repair output, the first question should be why the error was not prevented at the source or in the responsible builder.
Not every exception can disappear immediately. blog-editorial-visuals and part of seo-finalize remain explicit debt because they still provide real behaviour. Keeping them visible is healthier than pretending the pipeline has already reached its final form.
Going from 34 build steps to 12 is not the end of the architecture. It is evidence that Blupoli is moving away from accumulated repair passes and toward layers that are responsible for the output they create.
Measuring before and after kept simplification objective
The refactor used simple counts because “the pipeline feels cleaner” is not enough evidence. Counting scripts, build steps, check phases, and workflows made it possible to compare concrete architecture before and after. Those numbers do not describe every quality dimension, but they prove that work actually disappeared rather than merely receiving new names.
They also create a useful baseline against future rebound. An active monorepo can accumulate helper scripts quickly if every incident is solved with another global pass. Knowing that the post-refactor state is 22 scripts, 12 build steps, 3 check phases, and 4 workflows makes renewed accumulation easier to notice and discuss.
The correct numbers do not need to remain frozen forever. A genuinely new responsibility may deserve a new phase. The important rule is that growth should be deliberate and architectural, not the automatic response to output that another stage produced incorrectly.
The self-hosted runner made duplication cost visible
With unlimited compute, two workflows doing almost the same work can look like minor housekeeping. On a self-hosted runner the duplication becomes tangible: full builds wait in a queue, Playwright browser jobs compete for the same execution slot, and preview work can run after equivalent validation already completed elsewhere.
That is why consolidating Quality and preview was not only YAML cleanup. It released real capacity for work that needs the runner. Canceling obsolete pull-request revisions has the same effect: there is little value in spending several more minutes validating a commit that a newer revision has already replaced.
This experience reinforced a useful idea for Blupoli: CI is an internal product. Its latency changes iteration speed and the confidence with which pull requests can be reviewed. Optimizing it is not simply saving machine minutes; it shortens the distance between a code decision and trustworthy evidence about that decision.
Firebase preview is valuable without being the definition of code quality
Firebase preview-channel quotas can return HTTP 429 even when the build, tests, and Playwright suite are all correct. Treating that external infrastructure limit as if the product itself had failed made the workflow less informative. Reviewers would see a red state and first investigate code that had already passed its functional checks.
The consolidated flow keeps preview at the end because it remains useful for visual review, but a quota failure no longer needs to rewrite the meaning of the validation that came before it. The workflow can show that preview publication failed while preserving the fact that the tested revision passed the project’s quality gates.
Separating those signals improves diagnosis. A failing test should point toward a regression in code or behaviour. A preview-service quota should keep its infrastructure context. The more accurately CI communicates failure type, the less time is lost debugging the wrong layer.
Simpler architecture improves documentation for humans and agents
A 34-step build forces documentation to explain too many historical exceptions: which normalizer must run after localization, which repair pass changes branding, or which script fixes a route generated by another script. As those dependencies disappear, documentation can describe concepts and canonical ownership instead of memorizing a recipe.
This matters especially in a repository where development agents are expected to work from AGENTS.md and local skills. An agent can make a source-level change more confidently when the instructions point to a canonical file and a stable verification command. If the final result depends on several post-build rewrites, an apparently correct edit may be silently changed later by a script the task never mentioned.
Build simplification is therefore also an interface improvement for development. Less implicit behaviour means shorter instructions, clearer code review, and a lower probability of modifying the wrong layer to achieve the right visible result.
The pipeline now makes remaining debt easier to see
Removing many historical repairs did not make the project debt-free. It did something more useful: it made the remaining exceptions legible. blog-editorial-visuals is visible because some legacy articles still need generated infographics. seo-finalize is visible because related and series navigation still needs a better canonical owner.
When a pipeline is full of repair passes, one more exception disappears into the noise. With fewer stages, every remaining non-deterministic or source-generating phase becomes easier to question. That creates pressure to finish migrations rather than letting temporary scripts become permanent by accident.
The same applies to CI. Path-specific Castle Wall stress tests, preview quota handling, and main-branch Quality are now explicit policies rather than incidental consequences of several overlapping workflows. The architecture is simpler not because every edge case vanished, but because the edges now have names and owners.
What changes in everyday development
A shorter pipeline reduces the cost of asking “where should this change live?” If branding is wrong in output, check-output reports the defect but the repair belongs in canonical source or the responsible builder. If source i18n is wrong, check-source and locale gates own the signal. If editorial assets are missing, the editorial pipeline has a defined boundary instead of several post-build normalizers competing to repair it.
That clarity also helps new shared systems enter the repository. Mobile-first architecture, common timing, and other platform layers have fewer hidden transformations to fight. This does not make those features trivial, but it reduces the chance that a correct source change is later overwritten by historical pipeline behaviour.
The success of build simplification is therefore not only a faster or shorter command. It is the reduction in the number of places a developer must touch — or even know about — to make one change correctly.
Related reading
The problem was anticipated in From adding games to building a verifiable platform. Editorial automation is covered in From Git to Devlog, and international quality gates in Six-language i18n and quality gates.