How do you use FVM so CI builds a Flutter app with exactly the SDK version developers pin, including in a repository with several apps?
answer
- .fvmrc is the single source
- fvm install with no argument
- fvm flutter for every step
- CI detected, prompts skipped
- nearest .fvmrc per app
basics
~20 sCommit an exact version in .fvmrc, then in CI install FVM, run fvm install with no argument to fetch the pinned SDK, and run every step through fvm flutter. In a multi-app repository, each app resolves the nearest .fvmrc.
solid answer
~50 sThe committed `.fvmrc` is the single source of truth. A CI job installs FVM (for example `dart pub global activate fvm` or the install script), runs `fvm install` with no argument, which reads `.fvmrc`, caches that SDK and links it into the project, and then runs every command as `fvm flutter ...` so no step picks up another Flutter. FVM detects CI from variables such as `CI` or `GITHUB_ACTIONS`: prompts are skipped with safe defaults and the local git cache is off unless configured. Pin exact versions, not channels, so the SDK cannot move between runs, and cache the FVM directory between jobs to avoid recloning. In a repository with several apps, FVM uses the nearest `.fvmrc` walking up from the working directory, so a root pin covers shared packages and an app-level `.fvmrc` overrides it for that app.
code
bash · 10 lines# CI job (any provider)
dart pub global activate fvm
fvm install # reads .fvmrc, installs and applies the pinned SDK
fvm flutter --version # log the SDK actually used
fvm flutter analyze
fvm flutter test
fvm flutter build appbundle --release
# multi-app repository: run from each app's folder
(cd apps/legacy && fvm install && fvm flutter test)go deeper
Know that CI should build with the version in .fvmrc and that fvm install reads it.
Explain fvm install without arguments, why every step uses fvm flutter, and how FVM detects and behaves on CI.
Build a reproducible pipeline: exact pins, cached FVM directory, logged SDK version, and correct per-app pins in a multi-app repository.
Decide how SDK upgrades flow through a multi-app repository: one shared pin or per-app pins, and who owns reviewing an .fvmrc change.
## The goal A CI build is only meaningful if it uses the same Flutter SDK as the developers who wrote the code. FVM makes the version a **file in the repository** rather than a property of each machine, so CI and developers read the same answer. ## A CI job, step by step 1. **Install FVM** — with `dart pub global activate fvm`, the official install script, or a standalone executable. 2. **Install the pinned SDK** — `fvm install` with **no argument** finds `.fvmrc`, installs that version into the FVM cache if needed, runs its setup and applies it to the project (links, `pub get`). 3. **Run everything through FVM** — `fvm flutter analyze`, `fvm flutter test`, `fvm flutter build apk --release`, `fvm dart run build_runner build`. Plain `flutter` would follow the runner's `PATH` instead. 4. **Cache the FVM directory** — the default `~/fvm`, or the path set by `FVM_CACHE_PATH`, keyed on the `.fvmrc` contents, so later jobs skip the clone. ## How FVM behaves on CI FVM treats a run as CI when it sees common CI variables: | Variable | Typical source | |---|---| | `CI` | most providers | | `GITHUB_ACTIONS` | GitHub Actions | | `GITLAB_CI` | GitLab CI | | `CIRCLECI`, `TRAVIS`, `JENKINS_URL`, `BAMBOO_BUILD_NUMBER` | other runners | On CI it: - **skips interactive prompts** and takes the default answer, so a constraint mismatch fails instead of hanging; - **disables the local git cache by default**, which helps on short-lived runners (`useGitCache` or `FVM_USE_GIT_CACHE` override it); - **does not record the project** in FVM's list of known projects. The update check can also be turned off for quieter logs. ## Pinning for reproducibility - **Exact version** (`"flutter": "3.47.5"`) — the same SDK on every run. - **Channel** (`"flutter": "stable"`) — whatever that channel checkout holds on the runner, which changes over time. Avoid it for CI. - **`fvm use stable --pin`** — convenient for developers: writes stable's current exact release, keeping `.fvmrc` exact. An SDK upgrade then becomes a reviewed one-line change to `.fvmrc`, and CI proves it before merge. ## Repositories with several apps FVM finds a project's pin by walking **up** from the current directory to the nearest `.fvmrc`: | Layout | Effect | |---|---| | `.fvmrc` at the root only | every app and package uses the root version | | root `.fvmrc` plus `apps/legacy/.fvmrc` | `apps/legacy` uses its own version; the rest use the root's | | no root `.fvmrc`, one per app | each app is independent; shared packages need their own pin or inherit nothing | In CI, run each app's steps from that app's directory so the right pin is found. When a Melos workspace is detected, `fvm use` also points its `sdkPath` at `.fvm/flutter_sdk`, unless `updateMelosSettings` is false. ## Checking a pipeline - Print `fvm flutter --version` early, so logs show which SDK ran. - Fail fast if `.fvmrc` is missing: `fvm install` without a pin exits with an error asking for a version. - Keep developer commands identical, so what passes locally is what CI runs.
- Why is a channel name in .fvmrc a problem for CI even if developers are happy with it?A channel pin means whatever the cached channel checkout contains. A fresh runner clones the channel's current head, a cached runner keeps an older one, and developers each have their own, so the SDK differs between runs. Pin an exact version, or use `fvm use stable --pin` to write stable's current release.
- A CI step calls flutter build instead of fvm flutter build. What can go wrong?Plain `flutter` follows the runner's `PATH`, which may hold a preinstalled SDK or none. The build can then use a different Flutter and Dart than the pin, producing different code generation, lints or failures that nobody sees locally. Route every step through `fvm flutter` or put `.fvm/flutter_sdk/bin` first on `PATH`.
saying these in an interview costs you the question
- Pins stable in .fvmrc and expects identical CI builds
- Installs FVM in CI but calls plain flutter afterwards
- Hardcodes the Flutter version in the CI script as well as .fvmrc
- Expects fvm install to hang waiting for input on CI
- Assumes a subfolder package ignores the root .fvmrc