Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,24 @@ on:
branches: [ master ]

jobs:
setup_flutter_versions:
runs-on: ubuntu-latest
outputs:
flutter-versions: ${{ steps.id_flutter_versions.outputs.versions }}
steps:
- uses: actions/checkout@v4
- uses: dart-lang/setup-dart@v1
- name: Fetch Flutter versions
id: id_flutter_versions
run: dart tool/fetch_versions.dart >> $GITHUB_OUTPUT

test_flutter_versions:
needs: setup_flutter_versions
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
flutter: [ '3.24.0', '3.27.0', '3.29.0', '3.32.0', '3.35.0', 'stable', 'beta' ]
flutter: ${{ fromJson(needs.setup_flutter_versions.outputs.flutter-versions) }}
name: Tests on Flutter ${{ matrix.flutter }}
steps:
- uses: actions/checkout@v4
Expand Down
80 changes: 80 additions & 0 deletions test/fetch_versions_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import 'package:flutter_test/flutter_test.dart';
import '../tool/fetch_versions.dart';

void main() {
test(
'parseVersions extracts last 5 minor versions, skips 1st, returns 4 + stable/beta',
() {
final json = {
"releases": [
// Minor 3.38
{
"version": "3.38.5",
"channel": "stable",
"release_date": "2025-12-12T10:00:00Z"
},
{
"version": "3.38.4",
"channel": "stable",
"release_date": "2025-12-11T10:00:00Z"
},

// Minor 3.35
{
"version": "3.35.7",
"channel": "stable",
"release_date": "2025-11-12T10:00:00Z"
},

// Minor 3.32
{
"version": "3.32.8",
"channel": "stable",
"release_date": "2025-10-12T10:00:00Z"
},

// Minor 3.29
{
"version": "3.29.3",
"channel": "stable",
"release_date": "2025-09-12T10:00:00Z"
},

// Minor 3.27
{
"version": "3.27.4",
"channel": "stable",
"release_date": "2025-08-12T10:00:00Z"
},

// Minor 3.24
{
"version": "3.24.2",
"channel": "stable",
"release_date": "2025-07-12T10:00:00Z"
},

// Beta (ignored by filter, but 'beta' string is added manually)
{
"version": "3.40.0-beta",
"channel": "beta",
"release_date": "2025-12-13T10:00:00Z"
},
]
};

final versions = parseVersions(json);

expect(versions, [
// "3.38.5", // Skipped
"3.35.7",
"3.32.8",
"3.29.3",
"3.27.4",
// "3.24.2", // Removed
"stable",
"beta"
]);
expect(versions.length, 6);
});
}
77 changes: 77 additions & 0 deletions tool/fetch_versions.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import 'dart:convert';
import 'dart:io';

Future<void> main() async {
try {
final json = await _fetchJson();
final versions = parseVersions(json);
print('versions=${jsonEncode(versions)}');
} catch (e) {
stderr.writeln('Error: $e');
exit(1);
}
}

Future<Map<String, dynamic>> _fetchJson() async {
final url = Uri.parse(
'https://storage.googleapis.com/flutter_infra_release/releases/releases_linux.json');
final httpClient = HttpClient();
try {
final request = await httpClient.getUrl(url);
final response = await request.close();
if (response.statusCode != 200) {
throw HttpException('Failed to fetch releases: ${response.statusCode}');
}
final responseBody = await response.transform(utf8.decoder).join();
return jsonDecode(responseBody) as Map<String, dynamic>;
} finally {
httpClient.close();
}
}

List<String> parseVersions(Map<String, dynamic> json) {
final releases = (json['releases'] as List).cast<Map<String, dynamic>>();

// Filter for stable channel
final stableReleases = releases.where((r) => r['channel'] == 'stable');

// Group by Major.Minor (e.g. 3.38)
final Map<String, Map<String, dynamic>> latestByMinor = {};

for (final release in stableReleases) {
final version = release['version'] as String;
final parts = version.split('.');
if (parts.length < 2) continue;
final minor = '${parts[0]}.${parts[1]}';

final releaseDate = DateTime.parse(release['release_date']);

if (!latestByMinor.containsKey(minor)) {
latestByMinor[minor] = release;
} else {
final currentBest = latestByMinor[minor]!;
final currentBestDate = DateTime.parse(currentBest['release_date']);
// Keep the most recent patch
if (releaseDate.isAfter(currentBestDate)) {
latestByMinor[minor] = release;
}
}
}

// Sort groups by release date descending
final sortedByDate = latestByMinor.values.toList()
..sort((a, b) {
final dateA = DateTime.parse(a['release_date']);
final dateB = DateTime.parse(b['release_date']);
return dateB.compareTo(dateA);
});

// Take the last 5 minor versions
final top5 = sortedByDate.take(5).map((r) => r['version'] as String).toList();

// Skip the first one (latest stable) to avoid duplication with 'stable' channel
// Return the next 4
final historicVersions = top5.skip(1).toList();

return [...historicVersions, 'stable', 'beta'];
}