I’m a senior fullstack engineer but at the small startups, I worked at before Commit I often had to get my hands dirty with DevOps. Things move quickly at a startup, product iteration is prioritized, and solid best practices DevOps are often relegated to the nice-to-have-tomorrow list. Instead of a proper CI/CD pipeline, we would test manually and hack together some homemade deployment `bash` scripts and hope for the best. While this approach can (and does) work, it often results in flaky, inconsistent deployments and numerous regression bugs–along with a host of other more insidious problems with code hygiene, test coverage, local deployment environment dependence, and more.
Well, tomorrow will be better. Commit is supporting me with time to learn how to use a modern hosted CI/CD management tool! This tutorial will demonstrate how to build a Flutter app backed by Firebase and run through a CI/CD pipeline hosted by CircleCI. Ready to learn with me? Here’s what we’re going to do:
- Build a simple Hello World demo web app (PWA) with Flutter
- Create a custom Docker image to build, test, and deploy this app
- Build a CI/CD pipeline in CircleCI that will:
- Spin up an executor container based on our custom Docker image
- Run unit tests
- Measure test coverage
- Provision a Firebase testing project
- Run integration tests in Chrome against that Firebase test project
- Test authentication and authorization for Firestore db access
- Save screenshots (for future visual regression testing)
- Deploy the app to a Firebase production environment
- Discover a bug in Flutter along the way and fork, fix, push, use, and PR it
This code for this tutorial is available in the GitHub repository. You can follow along and see the directory structure by forking that and using your own Firebase and CircleCI accounts.
Setting Up Projects
- Install Docker on your local computer
- Create an account on Docker Hub
- Create or use an existing Github repository (or Bitbucket, anything that CircleCI can connect to)
- You can also fork my repo and use that
- Go to CircleCI and create an account, add a project, and connect it to your repo
- Create a Firebase project
- Install the Firebase CLI
- Authenticate the CLI to your Firebase account and project
- Flutter Fire is the Flutter package to help you connect Flutter to Firebase. Read through their Getting Started guide to configure your Flutter app to connect to your Firebase project. You’ll want to at least connect/enable Hosting, Authentication, and Firestore DB.
- You’ll have to change the default Firebase hosting directory to match the output of Flutter builds. Checkout the change in `firebase.json`:
Adding Secrets to CircleCI
Your build + test + deploy pipeline in CircleCI will need access to a few pieces of information to access your Firebase project. Some are secret, some aren’t but we’ll treat them all as secret because CircleCI has a great way of injecting these into your pipeline executor (the Docker container running the pipeline) through environment variables. CircleCI does its best to protect these secrets and even tries to auto-scrub them from any outputs but it’s still up to you to avoid leaking them in silly ways like printing to the console or committing them to your repo. Be safe.
Read about CircleCI environment variable secrets and add 4 variables:
- FIREBASE_TEST_PROJECT: The ID/slug of your Firebase test project. This is the project you setup and connected to earlier. You can find the ID by going to Project Settings → Project ID:
2. FIREBASE_PRODUCTION_PROJECT: For this demo, I just used the same Firebase project to deploy production builds but that of course wouldn’t work for a real product. Instead you would create a 2nd production project. Regardless of if you have 1 or 2 projects, you’ll need to enter the ID of the project you want to use for production.
3. FIREBASE_TOKEN: This is the token used to authenticate the CLI inside your Docker executor (run by CircleCI) to connect to the Firebase API. Read the Firebase CI Login instructions for how to get yours.
4. FIREBASE_ADMIN_KEY_TEST: This is the API credentials for the CLI we will build with Node.js to provision our Firebase test project. To generate it, go to your Firebase test Project settings → Service accounts and click ‘Generate new private key.’
This will download a JSON file. Just copy-paste the JSON text into the environment variable value and we’ll see later how we parse it out.
The environment variables for your CircleCI project should look something like this:
In order to build the Flutter app and test it with headless browsers on CircleCI infrastructure, we’ll need a Docker container that includes the Flutter SDK, browsers, and the CircleCI tools. We’ll also write a Node.js command-line interface program so we’ll need a Node environment. I couldn’t find an existing Docker image that had all of these things so we’ll have to make one. You can also read about using customer Docker images on CircleCI.
CircleCI makes life easier for us by providing a few pre-made images that include browsers (or actually, the provision to install browsers as a build step), Node, and CircleCI tools. We’ll start with an official CircleCI image and build from that:
Now we have to build the Dockerfile with a specific tag and push it up to Docker Hub so it can be pulled in by CircleCI. In the directory where your `Dockerfile` is you execute something like this:
Remember that Firebase test project you set up earlier? We’re going to need it to be identical–stateless–between test runs. We want the same users and database tables and rows to test against every time, regardless of how the tests might alter the database during the pipeline. To do that we’ll write a command-line interface program that the CircleCI config script can call to provision the Firebase project. The CLI will use those API secrets we added earlier to authenticate to the Firebase admin API.
Now we can run a command like `./cicd.js prep-test-env` to provision our test project. Building this as a CLI makes it easy to add configuration options later that can be set from the pipeline script.
You’ll also notice that we have some Firestore security rules written to protect our precious dummy posts from the prying eyes of unauthenticated users. These are stored in the `firestore.rules` file and are deployed as part of the pipeline:
Writing Integration Tests
Now we have to write the actual integration tests that CircleCI will run. Our CircleCI config script will start a Chrome web driver as a background process and then start a headless Chrome browser to run the tests in. We run the following end-to-end tests:
- Tapping the increment button increments the counter text
- We can’t load any posts from the database when not authenticated
- We can authenticate and see the signed-in user’s email address
- We see posts after authenticating
During this process, we also take a few screenshots and save them as “artifacts” that we can later retrieve through the CircleCI console or API. These are handy for a human to view as a sanity check or to help debug problems with the tests. They could also be used to do visual regression testing with a tool like Percy.
The Flutter integration testing code is based on the official example and expanded to include the Firebase-specific testing:
Unfortunately, at the time of writing this, Flutter has a bug that prevents screenshots from being saved during the integration testing. It looks like this is due to the “recent” adoption of null-safety by default. I haven’t had the time yet to get my fix’s PR up to Flutter’s contribution standards so you have two options. You can either disable the screenshots in the CircleCI config file or you could use my fix by reading my other post about Forking, fixing, and Using a Flutter Package.
Running the Pipeline
We now have a system that looks like this:
Here’s what’s happening:
- You develop a Flutter app on your local machine
- The app connects to Firebase services in your DEV environment
- You push a commit to GitHub
- This triggers CircleCI to start your pipeline based on your config YAML script
- CircleCI spins up a Docker container from your custom image
- The image installs browsers, starts a web driver, provisions the Firebase TEST environment, and runs tests
- If the tests succeed, the app is deployed to the PRODUCTION environment
After pushing a commit you should see your pipeline start and eventually look something like this:
Doesn’t that look tidy? Notice that the Chrome driver background process doesn’t have a green checkmark. That’s because killing the background process (with anything including SIGINT) results in CircleCI thinking there’s been a failure and it fails the whole pipeline. So we just leave the background process hanging and that appears to be the official recommended approach.
If you go to the URL you setup in your hosting service, you should see the app:
That’s it! If you have any questions or notice errors or bit rot, please create an issue in the GitHub repository.
Noland Germain is a Senior Software Engineer with 15 years of experience ranging from embedded firmware to fullstack PWAs. Most recently, he’s been developing white-labeled Flutter and React apps for industrial and commercial B2B applications.