feat: better version message on existing flutter version error (#2354)
Co-authored-by: Felix Angelov <felix@shorebird.dev>
This commit is contained in:
@@ -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)',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user