feat: better version message on existing flutter version error (#2354)

Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
Erick
2024-07-15 14:28:45 -03:00
committed by GitHub
parent 9e084c9f4e
commit 64d08c6441
6 changed files with 303 additions and 180 deletions
@@ -261,8 +261,8 @@ Please create a release using "shorebird release" and try again.
required ReleasePlatform platform,
}) async {
final createReleaseProgress = logger.progress('Creating release');
final flutterVersion = await shorebirdFlutter.getVersionString(
revision: flutterRevision,
final flutterVersion = await shorebirdFlutter.getVersionForRevision(
flutterRevision: flutterRevision,
);
try {
final release = await codePushClient.createRelease(
@@ -358,19 +358,33 @@ Use `shorebird flutter versions list` to list available versions.
// All artifacts associated with a given release must be built
// with the same Flutter revision.
if (existingRelease.flutterRevision != flutterRevision) {
final flutterVersion = await shorebirdFlutter.getVersionForRevision(
flutterRevision: flutterRevision,
);
final formattedCurrentReleaseVersion = shorebirdFlutter.formatVersion(
revision: flutterRevision,
version: flutterVersion,
);
final formattedExistingReleaseVersion = shorebirdFlutter.formatVersion(
revision: existingRelease.flutterRevision,
version: existingRelease.flutterVersion,
);
logger
..err('''
${styleBold.wrap(lightRed.wrap('A release with version $version already exists but was built using a different Flutter revision.'))}
''')
..info('''
Existing release built with: ${lightCyan.wrap(existingRelease.flutterRevision)}
Current release built with: ${lightCyan.wrap(flutterRevision)}
Existing release built with: ${lightCyan.wrap(formattedExistingReleaseVersion)}
Current release built with: ${lightCyan.wrap(formattedCurrentReleaseVersion)}
${styleBold.wrap(lightRed.wrap('All platforms for a given release must be built using the same Flutter revision.'))}
To resolve this issue, you can:
* Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterRevision}')}".
* Re-run the release command with "${lightCyan.wrap('--flutter-version=${existingRelease.flutterVersion ?? existingRelease.flutterRevision}')}".
* Delete the existing release and re-run the release command with the desired Flutter version.
* Bump the release version and re-run the release command with the desired Flutter version.''');
throw ProcessExit(ExitCode.software.code);
@@ -44,7 +44,7 @@ class ShorebirdFlutter {
final targetDirectory = Directory(_workingDirectory(revision: revision));
if (targetDirectory.existsSync()) return;
final version = await getVersionString(revision: revision);
final version = await getVersionForRevision(flutterRevision: revision);
final installProgress = logger.progress(
'Installing Flutter $version (${shortRevisionString(revision)})',
@@ -133,33 +133,40 @@ class ShorebirdFlutter {
/// Converts a full git revision to a short revision string.
String shortRevisionString(String revision) => revision.substring(0, 10);
/// Given a revision and a version, formats them into a single string.
///
/// e.g. 3.16.3 and b9b2390296b9b2390296 -> 3.16.3 (b9b2390296)
String formatVersion({required String revision, required String? version}) {
version ??= 'unknown';
return '$version (${shortRevisionString(revision)})';
}
/// Returns the current Shorebird Flutter version and revision.
/// Returns unknown if the version check fails.
Future<String> getVersionAndRevision() async {
String? version = 'unknown';
late final String? version;
try {
version = await getVersionString();
} catch (_) {}
} catch (_) {
version = 'unknown';
}
return '$version (${shortRevisionString(shorebirdEnv.flutterRevision)})';
return formatVersion(
version: version,
revision: shorebirdEnv.flutterRevision,
);
}
/// Returns the current Shorebird Flutter version.
/// Throws a [ProcessException] if the version check fails.
/// Returns `null` if the version check succeeds but the version cannot be
/// parsed.
Future<String?> getVersionString({String? revision}) async {
final result = await git.forEachRef(
contains: revision ?? shorebirdEnv.flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
directory: _workingDirectory(),
Future<String?> getVersionString() async {
final flutterRevision = shorebirdEnv.flutterRevision;
return getVersionForRevision(
flutterRevision: flutterRevision,
);
return LineSplitter.split(result)
.map((e) => e.replaceFirst('origin/flutter_release/', ''))
.toList()
.firstOrNull;
}
/// The current Shorebird Flutter version as a [Version]. Returns null if the
@@ -180,6 +187,24 @@ class ShorebirdFlutter {
return version;
}
/// Returns the human readable version for a given git revision
/// e.g. b9b2390296b9b2390296 -> 3.16.3
Future<String?> getVersionForRevision({
required String flutterRevision,
}) async {
final result = await git.forEachRef(
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
directory: _workingDirectory(),
);
return LineSplitter.split(result)
.map((e) => e.replaceFirst('origin/flutter_release/', ''))
.toList()
.firstOrNull;
}
/// Returns the git revision for the provided [version].
/// e.g. 3.16.3 -> b9b23902966504a9778f4c07e3a3487fa84dcb2a
Future<String?> getRevisionForVersion(String version) async {
@@ -213,7 +238,7 @@ class ShorebirdFlutter {
Future<void> useRevision({required String revision}) async {
await installRevision(revision: revision);
final version = await getVersionString(revision: revision);
final version = await getVersionForRevision(flutterRevision: revision);
final useFlutterProgress = logger.progress('Using Flutter $version');
shorebirdEnv.flutterRevision = revision;
useFlutterProgress.complete();
@@ -55,8 +55,8 @@ void main() {
when(() => logger.progress(any())).thenReturn(progress);
when(
() => shorebirdFlutter.getVersionString(
revision: any(named: 'revision'),
() => shorebirdFlutter.getVersionForRevision(
flutterRevision: any(named: 'flutterRevision'),
),
).thenAnswer(
(_) async => '3.22.0',
@@ -207,7 +207,9 @@ void main() {
);
when(
() => shorebirdFlutter.getVersionString(revision: flutterRevision),
() => shorebirdFlutter.getVersionForRevision(
flutterRevision: flutterRevision,
),
).thenAnswer((_) async => flutterVersion);
});
@@ -370,6 +370,25 @@ Note: ${lightCyan.wrap('shorebird patch --platforms=android --flavor=$flavor --t
createdAt: DateTime(2023),
updatedAt: DateTime(2023),
);
when(
() => shorebirdFlutter.getVersionForRevision(
flutterRevision: flutterRevision,
),
).thenAnswer((_) async => flutterVersion);
when(
() => shorebirdFlutter.formatVersion(
revision: flutterRevision,
version: flutterVersion,
),
).thenReturn('3.12.1');
when(
() => shorebirdFlutter.formatVersion(
revision: existingRelease.flutterRevision,
version: existingRelease.flutterVersion,
),
).thenReturn('3.12.1');
});
test('logs error and exits with code 70', () async {
@@ -201,33 +201,37 @@ Tools • Dart 3.0.6 • DevTools 2.23.1''');
});
group('getVersionAndRevision', () {
test('returns unknown (<revision>) when unable to determine version',
() async {
group('when unable to determine version', () {
const error = 'oops';
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(
ProcessException(
'git',
[
'for-each-ref',
'--format',
'%(refname:short)',
'refs/remotes/origin/flutter_release/*',
],
error,
ExitCode.software.code,
),
);
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionAndRevision),
completion(equals('unknown (${flutterRevision.substring(0, 10)})')),
);
setUp(() {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(
ProcessException(
'git',
[
'for-each-ref',
'--format',
'%(refname:short)',
'refs/remotes/origin/flutter_release/*',
],
error,
ExitCode.software.code,
),
);
});
test('returns unknown (<revision>)', () async {
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionAndRevision),
completion(equals('unknown (${flutterRevision.substring(0, 10)})')),
);
});
});
test('returns correct version and revision', () async {
@@ -240,151 +244,178 @@ Tools • Dart 3.0.6 • DevTools 2.23.1''');
group('getRevisionForVersion', () {
const version = '3.16.3';
const exception = ProcessException('git', ['rev-parse']);
test('throws exception when process exits with non-zero code', () async {
const exception = ProcessException('git', ['rev-parse']);
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenThrow(exception);
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
throwsA(exception),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
group('when process exits with non-zero code', () {
setUp(() {
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenThrow(exception);
});
test('throws exception', () async {
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
throwsA(exception),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
});
});
test('returns null when cannot parse revision', () async {
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenAnswer((_) async => '');
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
completion(isNull),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
group('when cannot parse revision', () {
setUp(() {
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenAnswer((_) async => '');
});
test('returns null', () async {
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
completion(isNull),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
});
});
test('returns revision when able to parse the string', () async {
group('when able to parse the string', () {
const revision = '771d07b2cf97cf107bae6eeedcf41bdc9db772fa';
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenAnswer(
(_) async => '''
setUp(() {
when(
() => git.revParse(
revision: any(named: 'revision'),
directory: any(named: 'directory'),
),
).thenAnswer(
(_) async => '''
$revision
''',
);
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
completion(equals(revision)),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
);
});
test('returns revision', () async {
await expectLater(
runWithOverrides(
() => shorebirdFlutter.getRevisionForVersion(version),
),
completion(equals(revision)),
);
verify(
() => git.revParse(
revision: 'refs/remotes/origin/flutter_release/$version',
directory: any(named: 'directory'),
),
).called(1);
});
});
});
group('getVersionString', () {
test('throws ProcessException when process exits with non-zero code',
() async {
group('when process exits with non-zero code', () {
const error = 'oops';
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(
ProcessException(
'git',
[
'for-each-ref',
'--format',
'%(refname:short)',
'refs/remotes/origin/flutter_release/*',
],
error,
ExitCode.software.code,
),
);
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
throwsA(isA<ProcessException>()),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
setUp(() {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenThrow(
ProcessException(
'git',
[
'for-each-ref',
'--format',
'%(refname:short)',
'refs/remotes/origin/flutter_release/*',
],
error,
ExitCode.software.code,
),
);
});
test('throws ProcessException', () async {
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
throwsA(isA<ProcessException>()),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
});
});
test('returns null when cannot parse version', () async {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenAnswer((_) async => '');
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
completion(isNull),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
group('when cannot parse version', () {
setUp(() {
when(
() => git.forEachRef(
directory: any(named: 'directory'),
contains: any(named: 'contains'),
format: any(named: 'format'),
pattern: any(named: 'pattern'),
),
).thenAnswer((_) async => '');
});
test('returns null', () async {
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
completion(isNull),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
});
});
test('returns version when able to parse the string', () async {
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
completion(equals('3.10.6')),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
group('when able to parse the string', () {
test('returns version', () async {
await expectLater(
runWithOverrides(shorebirdFlutter.getVersionString),
completion(equals('3.10.6')),
);
verify(
() => git.forEachRef(
directory: p.join(flutterDirectory.parent.path, flutterRevision),
contains: flutterRevision,
format: '%(refname:short)',
pattern: 'refs/remotes/origin/flutter_release/*',
),
).called(1);
});
});
});
@@ -409,7 +440,7 @@ $revision
});
});
group('when getVersionStringReturns an invalid string', () {
group('when getVersionString returns an invalid string', () {
setUp(() {
when(
() => git.forEachRef(
@@ -429,7 +460,7 @@ $revision
});
});
group('when getVersionStringReturns a valid string', () {
group('when getVersionString returns a valid string', () {
setUp(() {
when(
() => git.forEachRef(
@@ -878,5 +909,37 @@ origin/flutter_release/3.10.6''';
verify(() => shorebirdEnv.flutterRevision = newRevision).called(1);
});
});
group('formatVersion', () {
test('returns the correct formated value', () {
expect(
runWithOverrides(
() => shorebirdFlutter.formatVersion(
version: '3.10.6',
revision: '771d07b2cf97cf107bae6eeedcf41bdc9db772fa',
),
),
equals(
'3.10.6 (771d07b2cf)',
),
);
});
group('when version is null', () {
test('returns unknown for the version', () {
expect(
runWithOverrides(
() => shorebirdFlutter.formatVersion(
version: null,
revision: '771d07b2cf97cf107bae6eeedcf41bdc9db772fa',
),
),
equals(
'unknown (771d07b2cf)',
),
);
});
});
});
});
}