# 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.

| Part | Role | Module | What it brings |
| --- | --- | --- | --- |
| CI | CI | [`github_actions`](https://doc.saymyframe.com/modules/github-actions.md) | `.github/workflows/ci.yml` |

```bash
smf create my_app -m github_actions
```

## What runs

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

```bash
[ -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`](https://doc.saymyframe.com/guides/env.md). 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](https://doc.saymyframe.com/guides/flavors.md) of the app:

```bash
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:

```bash
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:

```text
Wrote .env.
No value for: REVENUECAT_APPLE_KEY, REVENUECAT_GOOGLE_KEY, REVENUECAT_TEST_KEY, PAYWALL_PRIVACY_URL.
```

| The script stops, and writes nothing, at | Why |
| --- | --- |
| A value with a double quote, a control character or a line separator | It 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 already | It 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:

```bash
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:

```yaml
- 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](https://doc.saymyframe.com/guides/firebase.md#a-flavor-that-is-not-configured) on Android once another flavor is.
- With [`launcher_icons`](https://doc.saymyframe.com/modules/launcher-icons.md) and [`native_splash`](https://doc.saymyframe.com/modules/native-splash.md), 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:

```yaml
- 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](https://docs.flutter.dev/deployment/android#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](https://doc.saymyframe.com/extending/role-provider.md#ci).
