Skip to main content

CI

The CI role says what runs on a server once you push the app: the checks on each push, and on request a release APK for one environment. A module writes that in the format of its host.

PartRoleModuleWhat it brings
CICIgithub_actions.github/workflows/ci.yml
smf create my_app -m github_actions

What runs​

On each push to a branch, in a fresh copy of the repository:

[ -f .env ] || cp .env.example .env
flutter pub get
dart format --output=none --set-exit-if-changed lib test
flutter analyze
flutter test --dart-define-from-file=.env

The first line and the argument of flutter test are there only in an app with the values in .env. The checks need no setup: .env is not in git, so a fresh copy has .env.example with every value empty. On your machine the first line keeps your .env, so you can run the same commands before a push.

On request, for one of the environments dev, staging and prod, the names of the flavors of the app:

dart tool/write_env.dart
flutter pub get
flutter build apk --release --flavor <environment> --dart-define-from-file=.env

The APK is build/app/outputs/flutter-apk/app-<environment>-release.apk, and the run keeps it. An app without values in .env skips the first line and the argument. The section "Continuous integration" of the README of the app lists the commands of that app.

Start a build​

With github_actions, open Actions, then CI, then Run workflow, and pick an environment, or run:

gh workflow run ci.yml -f environment=staging

ci.yml has to be on the default branch of the repository for that. The APK is an artifact of the run, named app-staging-release.

The job of the checks has 20 minutes and the job of the build 45. A new push cancels the run of an older push of its branch. A build runs to its end, and a second build for the same environment waits for it.

Values for each environment​

.env has one set of values, and a job writes its own. tool/write_env.dart, which the role generates in an app with the values in .env, takes the keys from .env.example and the value of each from the variable of the process with its name. It prints the keys that it found no value for, and never a value:

Wrote .env.
No value for: REVENUECAT_APPLE_KEY, REVENUECAT_GOOGLE_KEY, REVENUECAT_TEST_KEY, PAYWALL_PRIVACY_URL.
The script stops, and writes nothing, atWhy
A value with a double quote, a control character or a line separatorIt fits neither form of a value in .env, and could add a line of its own. The script names the key.
A .env that has a value alreadyIt never replaces what a developer filled in. Delete .env to write it again.

With github_actions, each key is a variable of the GitHub environment of that name. In the repository, open Settings, then Environments, and create one environment for each of dev, staging and prod that you build. Set the variables under Environment variables, or with the GitHub CLI:

gh variable set REST_BASE_URL --env staging --body 'https://staging.example.com/v1'

Repeat this for each environment that needs it. A build of an environment without a value for a key gets null from the getter of that key in AppEnv, and the app still builds.

GitHub documents environment variables on a private repository only for its paid plans, though a private repository on the Free plan takes them with this command too. If yours does not, set Repository variables instead, which give every build the same values.

Every value ends up in the app, where anyone can read it, so these are variables and never secrets of the repository. The workflow passes each value to the script through env: of the step, so no value becomes a part of the text of a script.

A key of your own needs a line under env: of the step that writes .env, or the build leaves it empty:

- name: Write .env from the variables of the environment
env:
MY_SERVICE_KEY: ${{ vars.MY_SERVICE_KEY }}
run: dart tool/write_env.dart

What has to be in git​

A job has only what is in git and what flutter pub get writes. So commit what the commands of your app wrote:

  • With Firebase, what dart tool/configure_firebase.dart writes. A flavor that is not configured fails its build on Android once another flavor is.
  • With launcher_icons and native_splash, the icon and the screen under android/ and ios/. No job makes them.

The version of Flutter​

The workflow runs the latest stable Flutter, so a new release can turn CI red with no change in your code: flutter analyze fails on a new deprecation, and the format check on a change of the formatter. To stay on the version that you develop with, add flutter-version under channel: stable in both jobs, with the version that flutter --version prints, and change it when you upgrade:

- uses: subosito/flutter-action@1a449444c387b1966244ae4d4f8c696479add0b2 # v2.23.0
with:
channel: stable
flutter-version: 3.44.2
cache: true

Each uses: line is pinned to a full commit, with its tag in a comment after it, so the file does not change under you. Nothing updates these pins. To update an action, change the commit and the tag together. A build that fails on a download usually passes when you run it again.

What stays with you​

The APK is signed as the Android project of the app signs a release, which is with the debug key in android/app/build.gradle.kts. So it is for testers. These are not part of the workflow, and you add them when the app needs them:

  • A signing key of your own. Sign the app in the documentation of Flutter tells what changes in the Android project. The key is a secret, so it goes into the secrets of the host and never into .env.
  • An upload to a store.
  • A job for iOS.

A module of your own​

A provider of the role writes ciRole.pipelineIn into the format of its host. See Provide a role.