diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 841120e8..e99044de 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -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 diff --git a/test/fetch_versions_test.dart b/test/fetch_versions_test.dart new file mode 100644 index 00000000..0195885d --- /dev/null +++ b/test/fetch_versions_test.dart @@ -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); + }); +} diff --git a/tool/fetch_versions.dart b/tool/fetch_versions.dart new file mode 100644 index 00000000..a4f1a586 --- /dev/null +++ b/tool/fetch_versions.dart @@ -0,0 +1,77 @@ +import 'dart:convert'; +import 'dart:io'; + +Future main() async { + try { + final json = await _fetchJson(); + final versions = parseVersions(json); + print('versions=${jsonEncode(versions)}'); + } catch (e) { + stderr.writeln('Error: $e'); + exit(1); + } +} + +Future> _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; + } finally { + httpClient.close(); + } +} + +List parseVersions(Map json) { + final releases = (json['releases'] as List).cast>(); + + // Filter for stable channel + final stableReleases = releases.where((r) => r['channel'] == 'stable'); + + // Group by Major.Minor (e.g. 3.38) + final Map> 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']; +}