One library change used to rebuild six apps. I redesigned how the monorepo builds and ships: merge to deploy, tag to release, and a CI that only builds what changed.
Per-app workflows, manifest files and a promote script. Every push ran 8 full npm installs, and 41% of runs were obsolete before they finished.
One workflow that plans what changed. Develop deploys to dev, master to staging, and a signed tag promotes the exact staging image to production.
The problem
The monorepo grew to 14 apps, and CI didn’t know which ones a change touched. Builds were slow, the Actions cache was 17 GB against a 10 GB limit and constantly evicting, and production releases meant a separate branch and manual promotion.
The idea
Make production a tag, not a rebuild: the image that passed staging is the image that ships. Then measure where CI time actually goes before optimising anything.
How I did it
- Wrote the CI/CD redesign on 28 May 2026 and cut the first production tag, v1.0.0, the same day.
- Replaced 24 workflows with 8, built around one plan step that starts 0–3 build machines, each handed its own list of apps.
- Added peer review on master: any one of four code owners other than the author can approve, written up as SOC 2 and ISO 27001 change-management evidence.
- Ran a CI study where every number is tied to a run ID. Almost none of the time was compiling; it was cache export, pulling a 2 GB base image, and one runner per app.
- Pruned 734 MB of dependencies nothing imported, cut cache scope about 7×, and cancelled obsolete runs automatically.
The impact
A one-app deploy now bills 6 minutes instead of 21 and takes about 5 minutes end to end. Pushing all ten apps bills 21–28 minutes instead of 69. At current usage that’s roughly $40–45 a month off the Actions bill, an estimate, and much faster feedback for the team. We’ve shipped 256 releases since v1.0.0, about 1.7 a day.
Measure the pipeline like you’d measure the product. The slow parts were nowhere near where we assumed.