Deployment
How We Build and Ship Code at CaterCow
At CaterCow, our engineering philosophy is built on a foundation of speed, safety, and developer autonomy, with automated Slack notifications keeping the entire team in the loop at every stage. We've created a CI/CD process using Semaphore that automates the tedious parts of testing and deployment while keeping engineers in full control of the release lifecycle. This allows us to ship new features and fixes to our Rails, Nuxt, and React Native applications between 3 and 10 times per week.
Our Testing and Branching Strategy
Our entire workflow is anchored by a branching strategy inspired by GitFlow, which provides a clear distinction between code in development and code that's ready for production.
featureBranches: All new work, from bug fixes to major features, happens on feature branches (e.g.,493-allow-photos-of-menu-items). These are typically tagged with GitHub issue numbers and merge intodevelop. To keep our CI process fast, these branches only run tests relevant to the files that have changed. Our end-to-end integration tests are only run on these branches when the branch name containse2e.developBranch: This branch serves as our pre-production staging ground. Every push todevelopautomatically triggers our full, parallelized test suite, which includes Ruby unit tests, Jest unit tests, and Playwright integration tests. This ensures that our main development branch is always stable.masterBranch: This branch represents our production-ready code. Only code that has been tested and vetted on thedevelopbranch is merged intomaster.
Our testing posture combines automated checks with manual verification. Automated tests catch most issues, but we also use manual QA to ensure features work as intended for their target audience.
The Path to Production
Getting code from a developer's machine to our customers follows a deliberate, multi-stage process that prioritizes stability. Automated Slack notifications are configured for all tests and deployments, ensuring visibility across the team.
QA and Staging Environments
Once a feature is complete on the develop branch, it can be deployed to a QA environment. These deployments are manually triggered from Semaphore and allow team members outside of engineering to test new functionality in a non-development setting and provide feedback.
When we're ready for a new release, a pull request is created to merge the code from develop into master. After the merge, a build is automatically deployed to our staging environment. Staging is a critical step for us; it’s a final smoke test for developers to check their work against a realistic environment before it goes live.
To make testing as realistic as possible, live production data is copied to staging daily. Sensitive information like emails and phone numbers is scrubbed, but the vast majority of PII and operational data remains.
Production Deployments and Release Communication
The final deployment to production is a manual process triggered from Semaphore after the staging pipeline succeeds. Our Nuxt, Rails, and React Native web applications are all packaged together in a single Helm release, which automates the entire process of updating our Kubernetes cluster. While engineers are encouraged to keep others in the loop, our process empowers anyone on the team to release code as needed, fostering a culture of ownership and responsibility.
To ensure transparency, after a successful production deployment, a release PDF is generated and posted to a Slack channel for the internal team. This PDF is compiled by a script that pulls the titles and bodies from all associated pull requests. This elegantly makes our PRs serve a dual purpose: explaining the technical changes to engineers while also communicating the new features and their value to the wider CaterCow team.
Mobile App Deployments
Our process for the attendee React Native app follows a similar principle of automation. Builds are triggered via a Semaphore pipeline, but the actual compilation and bundling is handled by Expo Application Services (EAS). The entire process is described in code within our repository, and for production releases, it only requires a final confirmation step within the respective app stores.
Database, Versioning, and Feature Flags
Safe Database Migrations
Since our database is our primary stateful service, we handle migrations with care. Migrations are defined in our Rails application and run as a Kubernetes Job with a pre-install/pre-upgrade Helm hook. If a migration fails for any reason, the Helm deployment is immediately aborted, and no new application code is rolled out. We adhere to safe migration practices when possible, which ensures that even if a migration succeeds, the old application code could still function correctly if we needed to roll back for any reason.
Unified Versioning and Feature Flags
Simplicity is key to our architecture. All customers use the exact same version of the app; we do not maintain special builds for specific clients.
We use feature flags sparingly for phased rollouts. When we do, they are typically implemented as simple boolean flags added to tables for users, teams, or restaurant groups in our database. This allows us to enable new functionality for specific segments of our user base before rolling it out to everyone.