The moment I mention to other developers that we build and deploy our production frontend directly from our development machines, I can see in their eyes that they are deciding whether I am joking or crazy. Why give up the safety and consistency of clean deployments from the cloud!? But after four years of actually using this approach, I’m convinced that thoughtfully automated local deployments can rival the safety and efficiency of typical CI/CD pipelines.
The term “Continuous Integration and Continuous Deployment” (CI/CD) varies in meaning from person to person. So let’s focus on a specific definition: automatic testing and deployment triggered by code changes. In many cases, this means using GitHub Actions to run tests and deploy code when changes land in the main branch.
CI/CD pipelines often follow this pattern:
(You can cut out step 2 – we do – by using some branch protection rules. More on that later.)
While this approach ensures consistent, well-tested deployments, it falls short in urgent situations. Consider a critical bug discovered in production: How long do your tests take to run? Why wait for a complete test suite after a one-line fix? Even worse, why run tests if you are rolling back to a previously verified state?
But that’s just the tests. What does “deployment” entail in CI/CD? Often it is rebuilding the entire state of the machine from scratch, sometimes waiting for lots of downloads and installs.
This leads to my main point: The most effective CI/CD removes the “Deployment” component entirely.
Conceptually, CI should be simple. This is what ours looks like:
Our CI remains simple because we leverage GitHub’s branch protection features. By requiring PRs to be up to date with the main branch before merging, we guarantee that merged code has passed all checks against the current state of the codebase. This eliminates the need for post-merge testing – the code in main will be identical to what we just tested in the PR once it’s merged.
Since all code at the tip of the main branch is tested, we know that main is safe to deploy. But “safe” does not inherently mean “ready.” So we do not trigger any automatic deployment after a merge succeeds.
Separating deployments from CI opens up crucial flexibility. When you can deploy any branch at any time, you gain powerful options for handling unexpected issues. Manual deployment triggers, when properly automated, are just as reliable as CI/CD-initiated deployments – but with added control.
At Trilliant Health, our frontend team has spent four years deploying our SPA directly from developer machines. Rather than building some portion of deployment logic directly inside complex GitHub Actions workflows, we invested in creating a streamlined CLI tool which contains all the deployment logic. This makes deployments automated but not automatic – a crucial distinction.
This approach has proven its worth during critical incidents. In the rare cases where we’ve shipped showstopping bugs in the frontend (twice in four years), we’ve reverted them within five minutes of discovery. We could move that fast because we didn’t have to create a branch, push to GitHub, open a Pull Request, wait for a review, wait for tests to pass in CI, click merge, wait for more tests and then wait for an underpowered runner to install all the dependencies from scratch, build and – finally – deploy our application.
Instead, we checked out the previously deployed commit, typed a single CLI command and waited for the build and deploy to complete.
Most concerns about this deployment strategy fall into two categories:
“Are you worried about bad local state or accidental deployments?”
Our deployment script includes basic safeguards and confirmation steps. Even if a developer's local environment causes issues, any other team member can quickly deploy from their machine. (We’ve never needed this failsafe.)
“If developers can deploy whatever they want, won't they be tempted to?”
If you don’t trust your developers, why did you hire them?
In some situations, Continuous Deployment makes a lot of sense. But I would encourage you to consider whether it actually works well for you.
With proper safeguards, there’s no need for deployments to wait on remote test suites and builds. Leveraging local builds for production deployment can turn CI/CD (minus the CD) into a tool that enhances your process without constraining it.
Here are answers to common follow-up questions:
“This would never work for us.”
Ok. (Also, that isn’t a question, sir.)
“We do true CI/CD. Every commit goes straight to the main branch, is tested and then immediately deployed. Is there a version of this that works in that scenario?”
Not really, though I would recommend a clear, documented way to roll back quickly without tests if your test suite is more than a couple of minutes long.
“You are talking about an SPA. Would this work in a full stack app?”
The principle – removing deployment barriers – applies broadly. Even with server-side builds, you can create faster deployment triggers that don't depend on complete CI test cycles.
“How do you know if your deployment is about to overwrite someone else's?”
For large teams with frequent deployments, you might need to enhance your CLI tool to have some awareness of other active deployments – not dissimilar from how you’d add these checks in your CI/CD pipeline.
But if you are like most teams, this isn’t a software problem – it is a process problem. On our team, developers announce deployments in a shared chat channel. No additional engineering required.
“It seems arduous to trigger deployment manually when you've already merged the code.”
This depends on your deployment complexity and frequency. Our process takes a couple of minutes, and an individual developer averages less than one deployment per day.
For higher-frequency deployments, I’d suggest creating a solution that is good enough to trigger locally but is also triggered in CI/CD, with a very easy way to turn automatic deployments off and on. You will miss out on the benefit of your developers having more confidence triggering things themselves, but you could still easily deploy changes on demand whenever necessary.