76dcc632ca
Change-Id: I57c4c00d4d96796e2306e1506880ae2183cfc468 Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/463780 Auto-Submit: Jake Macdonald <jakemac@google.com> Reviewed-by: Nate Bosch <nbosch@google.com> Commit-Queue: Jake Macdonald <jakemac@google.com>
306 lines
9.0 KiB
Dart
306 lines
9.0 KiB
Dart
// Copyright (c) 2025, the Dart project authors. Please see the AUTHORS file
|
|
// for details. All rights reserved. Use of this source code is governed by a
|
|
// BSD-style license that can be found in the LICENSE file.
|
|
|
|
/// A tool to find the lowest Dart and Flutter release containing a given
|
|
/// commit.
|
|
///
|
|
/// Usage:
|
|
///
|
|
/// `dart tools/find_release.dart --commit=<sha> --channel=<dev|beta|stable>`
|
|
library;
|
|
|
|
import 'dart:convert';
|
|
import 'dart:io';
|
|
|
|
import 'package:args/args.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:pub_semver/pub_semver.dart';
|
|
|
|
final parser = ArgParser()
|
|
..addOption(
|
|
'commit',
|
|
abbr: 'c',
|
|
help: 'The commit to search for',
|
|
mandatory: true,
|
|
)
|
|
..addOption(
|
|
'channel',
|
|
help: 'The channel to search for the commit in, dev only supports the dart '
|
|
'sdk since flutter does not do dev releases.',
|
|
allowed: ['dev', 'stable', 'beta'],
|
|
mandatory: true,
|
|
)
|
|
..addFlag(
|
|
'help',
|
|
abbr: 'h',
|
|
negatable: false,
|
|
help: 'Show usage information',
|
|
);
|
|
|
|
void main(List<String> arguments) async {
|
|
try {
|
|
final parsedArgs = parser.parse(arguments);
|
|
if (parsedArgs['help'] as bool) {
|
|
print(parser.usage);
|
|
exit(0);
|
|
}
|
|
final commit = parsedArgs['commit'] as String;
|
|
final channel = parsedArgs['channel'] as String;
|
|
print('Searching for commit $commit in channel $channel');
|
|
|
|
final repo = await findRepoForCommit(commit);
|
|
if (repo == null) {
|
|
print('Commit not found in Dart or Flutter repositories.');
|
|
exit(1);
|
|
}
|
|
print('Found commit in $repo');
|
|
|
|
print('Fetching lowest release tag for $commit...');
|
|
final tag = await fetchLowestVersionTag(repo, commit);
|
|
if (tag == null) {
|
|
print('No release tags found for commit $commit in $repo');
|
|
exit(1);
|
|
}
|
|
print('Lowest release tag for $commit was: $tag');
|
|
|
|
if (repo == flutterSdkRepo || (channel == 'beta' || channel == 'stable')) {
|
|
print('Checking for lowest flutter release newer than $tag...');
|
|
final flutterRelease = await fetchLowestFlutterRelease(
|
|
repo,
|
|
channel,
|
|
tag,
|
|
);
|
|
if (flutterRelease == null) {
|
|
print(
|
|
'No Flutter releases found '
|
|
'${repo == dartSdkRepo ? 'with a Dart sdk ' : ''}'
|
|
'newer than $tag',
|
|
);
|
|
} else {
|
|
print('Lowest Flutter release: $flutterRelease');
|
|
}
|
|
} else {
|
|
print(
|
|
'Skipping flutter version check for channel $channel, only `beta` and '
|
|
'`stable` are supported for flutter version checks',
|
|
);
|
|
}
|
|
|
|
if (repo == dartSdkRepo) {
|
|
print('Checking for lowest Dart release newer than $tag...');
|
|
final dartRelease = await fetchLowestDartReleaseFromGcs(
|
|
'channels/$channel/release/',
|
|
tag,
|
|
);
|
|
if (dartRelease == null) {
|
|
print('No Dart releases found that were newer than $tag');
|
|
} else {
|
|
print('Lowest Dart release: $dartRelease');
|
|
}
|
|
} else {
|
|
print('Skipping dart version check as this was a flutter commit');
|
|
}
|
|
} on FormatException catch (e) {
|
|
print(e.message);
|
|
print(parser.usage);
|
|
exit(1);
|
|
}
|
|
}
|
|
|
|
const dartSdkRepo = 'dart-lang/sdk';
|
|
const flutterSdkRepo = 'flutter/flutter';
|
|
|
|
/// Searches the Dart and Flutter repos for the given [sha], and returns the
|
|
/// org/repo string where it was found.
|
|
Future<String?> findRepoForCommit(String sha) async {
|
|
if (await checkCommit(dartSdkRepo, sha)) return dartSdkRepo;
|
|
if (await checkCommit(flutterSdkRepo, sha)) return flutterSdkRepo;
|
|
return null;
|
|
}
|
|
|
|
/// Returns whether or not [sha] exists in [repo] (which should be an org/repo
|
|
/// string).
|
|
Future<bool> checkCommit(String repo, String sha) async {
|
|
try {
|
|
await makeGhApiRequest('repos/$repo/commits/$sha');
|
|
return true;
|
|
} on RequestException catch (_) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// Returns the lowest tag that parses as a semver version containing [commit].
|
|
Future<Version?> fetchLowestVersionTag(
|
|
String repo,
|
|
String commit,
|
|
) async {
|
|
// Note that this is not an official endpoint, it was reverse engineered from
|
|
// the commit page on github, and could break in the future.
|
|
final response = await makeGhApiRequest(
|
|
'$repo/branch_commits/$commit.json',
|
|
isApiRequest: false,
|
|
);
|
|
final tags = (response['tags'] as List).cast<String>();
|
|
Version? lowest;
|
|
for (final tag in tags) {
|
|
try {
|
|
final version = Version.parse(tag);
|
|
if (lowest == null || version < lowest) {
|
|
lowest = version;
|
|
}
|
|
} on FormatException catch (_) {
|
|
continue;
|
|
}
|
|
}
|
|
return lowest;
|
|
}
|
|
|
|
/// Makes a request to [path] against the github API.
|
|
///
|
|
/// If [isApiRequest] is false, treats this as a regular github request instead
|
|
/// of an official API request.
|
|
///
|
|
/// Throws a [RequestException] if the request fails.
|
|
Future<Map<String, Object?>> makeGhApiRequest(
|
|
String path, {
|
|
bool isApiRequest = true,
|
|
}) async {
|
|
var uri = Uri.https(
|
|
isApiRequest ? 'api.github.com' : 'github.com',
|
|
'/$path',
|
|
);
|
|
final response = await http.get(uri);
|
|
if (response.statusCode != 200) {
|
|
throw RequestException(response, uri);
|
|
}
|
|
return jsonDecode(response.body) as Map<String, Object?>;
|
|
}
|
|
|
|
/// Returns the lowest Dart release version in the GCS storage bucket starting
|
|
/// with [prefix] and greater than or equal to [minVersion].
|
|
///
|
|
/// Throws a [RequestException] if the request fails.
|
|
Future<Version?> fetchLowestDartReleaseFromGcs(
|
|
String prefix,
|
|
Version minVersion,
|
|
) async {
|
|
final uri = Uri.https(
|
|
'storage.googleapis.com',
|
|
'/storage/v1/b/dart-archive/o',
|
|
{
|
|
'delimiter': '/',
|
|
'prefix': prefix,
|
|
'alt': 'json',
|
|
},
|
|
);
|
|
final response = await http.get(uri);
|
|
if (response.statusCode != 200) {
|
|
throw RequestException(response, uri);
|
|
}
|
|
Version? lowest;
|
|
try {
|
|
final releases = ((jsonDecode(response.body)
|
|
as Map<String, Object?>)['prefixes'] as List)
|
|
.cast<String>();
|
|
for (var release in releases) {
|
|
final versionPart = release.split('/')[3];
|
|
try {
|
|
final version = Version.parse(versionPart);
|
|
if (version >= minVersion && (lowest == null || version < lowest)) {
|
|
lowest = version;
|
|
}
|
|
} on FormatException catch (_) {
|
|
continue;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
throw RequestException(response, uri);
|
|
}
|
|
return lowest;
|
|
}
|
|
|
|
/// Returns the lowest Flutter version greater than or equal to [minVersion].
|
|
///
|
|
/// The [minVersion] is either a Dart or Flutter version, depending on [repo].
|
|
///
|
|
/// If [repo] is [flutterSdkRepo], then it will only return a version from
|
|
/// [channel], otherwise it may contain a release from any channel. This is
|
|
/// because the release channels do not always correspond to each other.
|
|
///
|
|
/// Throws a [RequestException] if the request fails.
|
|
Future<Version?> fetchLowestFlutterRelease(
|
|
String repo, // The repo that `minVersion` corresponds to.
|
|
String channel,
|
|
Version minVersion,
|
|
) async {
|
|
final uri = Uri.https(
|
|
'storage.googleapis.com',
|
|
'/flutter_infra_release/releases/releases_linux.json',
|
|
);
|
|
final response = await http.get(uri);
|
|
if (response.statusCode != 200) {
|
|
throw RequestException(response, uri);
|
|
}
|
|
Version? lowest;
|
|
try {
|
|
final releases = ((jsonDecode(response.body)
|
|
as Map<String, Object?>)['releases'] as List)
|
|
.cast<Map<String, Object?>>();
|
|
for (var release in releases) {
|
|
// We do search for dart releases in any flutter channel because sometimes
|
|
// stable flutter releases contain dart sdk releases from beta channels.
|
|
if (repo == flutterSdkRepo && release['channel'] != channel) continue;
|
|
|
|
var versionStr = switch (repo) {
|
|
dartSdkRepo => release['dart_sdk_version'] as String?,
|
|
flutterSdkRepo => release['version'] as String,
|
|
_ => throw FormatException('Unknown repository $repo'),
|
|
};
|
|
if (versionStr == null) continue;
|
|
// Some versions look like `3.11.0 (build 3.11.0-93.1.beta)`
|
|
if (versionStr.contains('(build')) {
|
|
versionStr = versionStr.split('(build ').last;
|
|
versionStr = versionStr.substring(0, versionStr.length - 1);
|
|
}
|
|
|
|
try {
|
|
final version = Version.parse(versionStr);
|
|
if (version >= minVersion) {
|
|
// This is a potential candidate, now check if the flutter version
|
|
// is the lowest we have seen.
|
|
final flutterVersion = switch (repo) {
|
|
flutterSdkRepo => version,
|
|
dartSdkRepo => Version.parse(release['version'] as String),
|
|
_ => throw StateError('Unexpected repo string $repo'),
|
|
};
|
|
if (lowest == null || flutterVersion < lowest) {
|
|
lowest = flutterVersion;
|
|
}
|
|
}
|
|
} on FormatException catch (_) {
|
|
continue;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
throw RequestException(response, uri, e);
|
|
}
|
|
return lowest;
|
|
}
|
|
|
|
class RequestException implements Exception {
|
|
final http.Response response;
|
|
final Uri uri;
|
|
final Object? error;
|
|
|
|
RequestException(this.response, this.uri, [this.error]);
|
|
|
|
@override
|
|
String toString() => '''
|
|
uri: $uri
|
|
statusCode: ${response.statusCode}
|
|
body: ${response.body}
|
|
error: $error
|
|
''';
|
|
}
|