You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
(I wrote this issue text using a Claude Code on the Web session, using Opus 5. I then had Claude Fable 5 review it in a local Claude session; the agent there said it verified the claims against "the repo, the Flutter tree, pub_semver 2.2.0, and GitHub".)
Our flutter version bound in pubspec.yaml intermittently rejects Flutter versions that are newer than the bound, breaking CI. This has now bitten us three times over about a year, each time handled with a different ad-hoc workaround. Let's fix it properly.
Symptom
Most recently, on #2419, CI fails at flutter pub get:
The current Flutter SDK version is 3.47.0-1.0.pre-506.
Because zulip requires Flutter SDK version >=3.47.0-1.0.pre-91,
version solving failed.
pre-506 is 415 commits newer than pre-91.
Cause
This isn't a bug in pub — it's semver-correct behavior, triggered by a change in how Flutter names main-channel versions.
In 3.47.0-1.0.pre-91, the pre-release part is 1.0.pre-91, which splits on . into the identifiers 1, 0, pre-91. Per semver, an identifier that's all digits compares numerically, but one containing letters or hyphens compares as an ASCII string. So pre-91 is a single alphanumeric identifier, and pre-506 sorts before it: 5 < 9, and the comparison stops there, never reaching the fact that one number has three digits and the other two.
Before flutter/flutter#172141 (2025-07) the names looked like 3.33.0-1.0.pre.832 — with a dot, so the commit count was its own all-digits identifier and compared numerically, correctly. We first took the new format in 55d055f (2025-07-24).
Confirmed against pub_semver 2.2.0:
3.47.0-1.0.pre-506 vs 3.47.0-1.0.pre-91 -> < # the failure
3.33.0-1.0.pre.832 vs 3.33.0-1.0.pre.1085 -> < # old format, correct
3.47.0-1.0.pre-506 vs 3.47.0-1.0.pre-100 -> > # fine, same digit count
'>=3.47.0-1.0.pre-91'.allows(3.48.0-1.0.pre-3) -> true
The last line is why this only bites within a release cycle: across a branch cut the base version differs and compares numerically.
For reference, the N in pre-N is the commit count from git describe — commits on main since the last release tag. It resets to 0 at each branch cut and then climbs, with no fixed width. That's the whole problem.
What we've done about it so far
632f8e0 (2025-07-28) taught tools/check's version-name check to accept both the old and new formats.
7cab2ad (2025-09-17) hit exactly this version-solving failure (bound pre-89, CI on pre-175) and worked around it by bumping the bound "to a version where that number starts with 1".
Bumping the bound isn't available on #2419: that upgrade deliberately stops at pre-91 because pre-92 breaks the iOS notification service extension (flutter/flutter#190909, still open — notifications show placeholder text). Any bound at pre-100 or above means taking that commit.
Also, the "starts with 1" rule from 7cab2ad isn't actually sufficient. Since the comparison is an ASCII string compare, a bound C is safe against every numerically-greater N only if str(C) <= str(N) for all of them — which holds just for powers of ten (pre-10, pre-100, pre-1000). A bound of pre-175 breaks as soon as the counter reaches 1000, because "1000" < "175". That didn't happen in the 3.37 cycle, but the 3.33 cycle did reach pre-1285 (2453ae7).
Re-pinning CI to the pubspec.yaml commit would work, but it gives up what we deliberately regained in 57db606 five months ago: CI running against latest main, so upstream breakage shows up here early.
Proposed fix
Stop asking pub to express a constraint it can't, and check the floor ourselves — by commit ancestry rather than by version name, since pubspec.yaml already records the commit sha alongside the bound.
Loosen the bound to the branch-cut prefix, keeping the tested commit in the comment. By semver's prefix rule "pre" < "pre-N" for every N, so this admits every pre-release in the cycle and everything later:
environment:
# (…existing comment…)## The `flutter` bound below is deliberately loose: Flutter's main-channel# version names don't sort correctly (flutter/flutter#172141 made the# commit count part of an alphanumeric pre-release identifier, so `pre-506`# sorts before `pre-91`), so a precise bound rejects newer Flutters.# The real floor is checked by `tools/check flutter_version`.# TODO(#1851) restore a precise bound if upstream makes these sortablesdk: '>=3.14.0-29.0.dev <4.0.0'flutter: '>=3.47.0-1.0.pre'# f65eec98830b2a4c0c7d5d0b13e3e914b4fb00f7
The comment has to sit above the sdk: line, as shown: pubspec_flutter_version in tools/lib/deps.sh matches the flutter: line only immediately after the sdk: line. With that placement it parses the loosened bound unchanged.
Add the real floor check to run_flutter_version in tools/check, above the files_check pubspec.yaml gate — the floor is about the developer's environment, not about the diff, so it should run whatever you changed. The pubspec_flutter_version call and the flutter_git setup currently sit below the gate, so they move up along with it:
local status=0
"${flutter_git[@]}" merge-base --is-ancestor "${flutter_commit}" @ \
|| status=$?case"${status}"in
0) ;;
1) # error: your Flutter is older than pubspec.yaml requires;# suggest `flutter upgrade`return 1;;
*) # error: commit not found in the Flutter tree;# suggest `git -C "${flutter_tree}" fetch origin`return 1;;
esac
The three exit codes split cleanly: 0 = floor satisfied, 1 = Flutter too old, 128 = commit missing from the tree. Worth keeping 128 distinct: on a --filter=blob:none clone, git attempts to fetch the missing commit from the remote before failing, so that path makes a network round-trip.
Report upstream to Flutter that main-channel version names haven't been orderable since #172141, and cross-link from Update the flutter_version check to follow upstream #1851. A quick search turned up no existing issue for it. That's the only fix that would let us restore a precise bound.
Notes and caveats
In CI this check is trivially satisfied — the tree is a fresh clone of latest main. Its value is local: catching a contributor whose Flutter is stale. That's exactly the role the pub bound used to play.
It's mildly wrong for someone on their own Flutter branch (testing an upstream PR, say): their Flutter can be newer in every way that matters while the pubspec commit isn't literally an ancestor. The error message should point at --exclude flutter_version.
This doesn't replace pub's handling of bounds that third-party packages declare on Flutter. We're only taking over our own self-imposed floor.
(I wrote this issue text using a Claude Code on the Web session, using Opus 5. I then had Claude Fable 5 review it in a local Claude session; the agent there said it verified the claims against "the repo, the Flutter tree, pub_semver 2.2.0, and GitHub".)
Our
flutterversion bound inpubspec.yamlintermittently rejects Flutter versions that are newer than the bound, breaking CI. This has now bitten us three times over about a year, each time handled with a different ad-hoc workaround. Let's fix it properly.Symptom
Most recently, on #2419, CI fails at
flutter pub get:pre-506is 415 commits newer thanpre-91.Cause
This isn't a bug in pub — it's semver-correct behavior, triggered by a change in how Flutter names main-channel versions.
In
3.47.0-1.0.pre-91, the pre-release part is1.0.pre-91, which splits on.into the identifiers1,0,pre-91. Per semver, an identifier that's all digits compares numerically, but one containing letters or hyphens compares as an ASCII string. Sopre-91is a single alphanumeric identifier, andpre-506sorts before it:5<9, and the comparison stops there, never reaching the fact that one number has three digits and the other two.Before flutter/flutter#172141 (2025-07) the names looked like
3.33.0-1.0.pre.832— with a dot, so the commit count was its own all-digits identifier and compared numerically, correctly. We first took the new format in 55d055f (2025-07-24).Confirmed against
pub_semver2.2.0:The last line is why this only bites within a release cycle: across a branch cut the base version differs and compares numerically.
For reference, the
Ninpre-Nis the commit count fromgit describe— commits onmainsince the last release tag. It resets to 0 at each branch cut and then climbs, with no fixed width. That's the whole problem.What we've done about it so far
tools/check's version-name check to accept both the old and new formats.TODO(#1851)inrun_flutter_version. That's Update theflutter_versioncheck to follow upstream #1851, still open — and it's the same upstream root cause as this issue.pre-89, CI onpre-175) and worked around it by bumping the bound "to a version where that number starts with 1".pubspec.yamlcommit in 0b4af24 / ae46d7c (2026-02), then unpinned in 57db606 (2026-03), closing Return to latest Flutter, with CI passing #2139 "Return to latest Flutter, with CI passing".Why the existing workarounds don't apply now
Bumping the bound isn't available on #2419: that upgrade deliberately stops at
pre-91becausepre-92breaks the iOS notification service extension (flutter/flutter#190909, still open — notifications show placeholder text). Any bound atpre-100or above means taking that commit.Also, the "starts with 1" rule from 7cab2ad isn't actually sufficient. Since the comparison is an ASCII string compare, a bound
Cis safe against every numerically-greaterNonly ifstr(C) <= str(N)for all of them — which holds just for powers of ten (pre-10,pre-100,pre-1000). A bound ofpre-175breaks as soon as the counter reaches 1000, because"1000" < "175". That didn't happen in the 3.37 cycle, but the 3.33 cycle did reachpre-1285(2453ae7).Re-pinning CI to the
pubspec.yamlcommit would work, but it gives up what we deliberately regained in 57db606 five months ago: CI running against latestmain, so upstream breakage shows up here early.Proposed fix
Stop asking pub to express a constraint it can't, and check the floor ourselves — by commit ancestry rather than by version name, since
pubspec.yamlalready records the commit sha alongside the bound.Loosen the bound to the branch-cut prefix, keeping the tested commit in the comment. By semver's prefix rule
"pre" < "pre-N"for everyN, so this admits every pre-release in the cycle and everything later:The comment has to sit above the
sdk:line, as shown:pubspec_flutter_versionintools/lib/deps.shmatches theflutter:line only immediately after thesdk:line. With that placement it parses the loosened bound unchanged.Add the real floor check to
run_flutter_versionintools/check, above thefiles_check pubspec.yamlgate — the floor is about the developer's environment, not about the diff, so it should run whatever you changed. Thepubspec_flutter_versioncall and theflutter_gitsetup currently sit below the gate, so they move up along with it:The three exit codes split cleanly: 0 = floor satisfied, 1 = Flutter too old, 128 = commit missing from the tree. Worth keeping 128 distinct: on a
--filter=blob:noneclone, git attempts to fetch the missing commit from the remote before failing, so that path makes a network round-trip.Report upstream to Flutter that main-channel version names haven't been orderable since #172141, and cross-link from Update the
flutter_versioncheck to follow upstream #1851. A quick search turned up no existing issue for it. That's the only fix that would let us restore a precise bound.Notes and caveats
main. Its value is local: catching a contributor whose Flutter is stale. That's exactly the role the pub bound used to play.--exclude flutter_version.