skip to content

In Flutter CI, how do you pin the SDK and reuse the pub cache, and why is pubspec's environment: flutter constraint not a pin?

level: middleimportance: should knowfreq 40%

answer

  1. only the lower bound is enforced
  2. channel stable is not a version
  3. exact version, logged every run
  4. PUB_CACHE, default $HOME/.pub-cache
  5. cache is an optimisation, lock is the truth

basics

~20 s

Pin the Flutter SDK by installing an exact release in the pipeline, for example through FVM or an exact version on the setup step; environment: flutter only enforces a lower bound. Cache the pub cache ($HOME/.pub-cache or PUB_CACHE) between runs.

solid answer

~40 s

`environment: flutter: '>=3.44.0'` in `pubspec.yaml` is a floor, not a pin: the Flutter SDK enforces only the **lower bound** of that constraint, so any newer SDK is accepted. A setup step that only says `channel: stable`, as many `subosito/flutter-action` workflows do, installs whatever stable is that day, so two runs of the same commit can build with different SDKs. Pin by installing one exact release: FVM (`dart pub global activate fvm`, then `fvm install` and `fvm flutter …`), an exact version on the setup step, or a `git clone -b 3.47.5` of the SDK, and print `flutter --version` in every run. Reuse downloads by caching the pub cache, `$HOME/.pub-cache` on macOS and Linux or wherever `PUB_CACHE` points, keyed on `pubspec.lock`. The cache only saves time; `pubspec.lock` with `--enforce-lockfile` decides the versions.

code

yaml · 6 lines
yaml
# GitHub Actions step as many workflows write it: NOT a pin
- uses: subosito/flutter-action@v2
  with:
    channel: stable
    cache: true
- run: flutter --version

go deeper

for a junior

Recall that CI should install one exact Flutter version and print flutter --version, and that the pub cache lives in $HOME/.pub-cache by default.

for a middle

Explain why environment: flutter enforces only a lower bound, why channel: stable is not a pin, and how caching differs from locking.

for a senior

Run SDK upgrades as their own changes through the full pipeline, keep developers and CI on the same pinned version, and design cache keys that survive upgrades.

for a principal

Set an upgrade cadence for the Flutter SDK across apps, balancing security and platform deadlines against the cost of each migration.

## Why pinning matters for a Flutter app A Flutter release build depends on the SDK in ways a library dependency does not: the SDK carries the engine, the framework, the Dart compiler and the `flutter` tool that drives Gradle and Xcode. Moving from one stable release to the next can change compile errors, analyzer lints, generated project files and even the iOS migration the tool applies. If the pipeline silently picks up a new SDK, a merge that changed nothing can go red, or worse, ship a binary built with an untested engine. ## Why the pubspec constraint is not a pin ```yaml environment: sdk: ^3.13.0 flutter: '>=3.44.0' ``` The pub documentation says a Flutter SDK constraint is satisfied when pub runs inside the `flutter` executable and the SDK's `version` file meets the constraint's **lower bound**; the Flutter SDK enforces only the lower bound. It protects against building with an SDK that is too old. It says nothing about which newer SDK the pipeline uses. ## Ways to pin | Approach | What it pins | Note | |---|---|---| | setup step with `channel: stable` only | nothing: latest stable that day | common in open-source workflows | | setup step with an exact version | one release | the version lives in the workflow file | | FVM in CI | the version recorded in the project's FVM config | `dart pub global activate fvm`, `fvm install`, `fvm flutter build …` | | `git clone --depth 1 -b 3.47.5` of the SDK | one tag | works on any CI system | | Codemagic `flutter:` setting | a channel, a version or `fvm` | set per workflow | Whichever you choose, make the pipeline print `flutter --version` so every log records the Flutter, Dart and engine versions actually used. ## Reusing the pub cache `flutter pub get` downloads hosted packages into the **system package cache**: `$HOME/.pub-cache` on macOS and Linux, `%LOCALAPPDATA%\Pub\Cache` on Windows, or the directory named by the `PUB_CACHE` environment variable. On an ephemeral CI machine that directory starts empty, so every run downloads every package again. - Persist that directory between runs with the CI system's cache feature, keyed on `pubspec.lock` so a dependency change creates a new entry. - Setup actions such as `subosito/flutter-action` offer a `cache: true` option that enables their own caching. - The job must still work on a cache miss; the cache is an optimisation. - Correctness comes from `pubspec.lock` plus `flutter pub get --enforce-lockfile`, not from the cache. ## Upgrading deliberately 1. Change the pinned version in one pull request, on its own. 2. Let the full pipeline run, including both platform builds, since SDK upgrades often surface in Gradle or Xcode rather than in Dart. 3. Raise the `environment: flutter` lower bound only when the code starts to need the newer SDK, so the constraint keeps meaning something. 4. Keep developers on the same version, for example through the same FVM config, so local and CI results agree. ## Diagnosing a sudden red build When a merge that touched nothing relevant fails, the SDK is the first suspect: 1. Compare the `flutter --version` output of the last green run and the failing one. A different framework revision or Dart version answers the question immediately. 2. If the setup step tracks a channel, the new stable release arrived between the two runs; pin it and schedule the upgrade as its own change. 3. If the SDK is identical, compare `pubspec.lock`; with `--enforce-lockfile` a dependency cannot have moved silently, so look at the cache or the runner image instead. 4. Check the runner image itself: Xcode and the Android SDK are also toolchain inputs, and an image update can break the iOS or Android build while Dart is unchanged. ## Common mistakes - Believing `flutter: '>=3.44.0'` stops CI from using 3.50. - Using `channel: stable` on the setup step and calling it pinned. - Caching the build output folder and restoring stale artefacts into a release build. - Treating a cache hit as proof that dependencies are the ones in the lock file.

  • Should the pub cache key include the Flutter version?
    It is optional. Hosted packages in the cache do not depend on the SDK, so keying on `pubspec.lock` is enough for correctness. Adding the pinned version starts a fresh cache after an SDK upgrade, which keeps it from accumulating packages the new lock file no longer uses.
  • What does raising environment: flutter to '>=3.47.0' achieve, then?
    It stops anyone, including CI, from resolving the project with an SDK older than 3.47.0, which matters when code uses newer APIs. It is a guard against too-old SDKs, not a guarantee of one exact SDK.

saying these in an interview costs you the question

  • environment: flutter: '>=3.44.0' stops CI from using a newer SDK.
  • A setup step with channel: stable pins the SDK version.
  • Restoring the pub cache guarantees the locked dependency versions.
  • The job may assume the cache is always present.
  • Pinning only matters for iOS builds, not Android ones.