diff --git a/dev/conductor/core/lib/src/next.dart b/dev/conductor/core/lib/src/next.dart index 864517935e..0323aaced4 100644 --- a/dev/conductor/core/lib/src/next.dart +++ b/dev/conductor/core/lib/src/next.dart @@ -150,6 +150,51 @@ class NextContext extends Context { } } + await pushWorkingBranch(framework, state.framework); + case pb.ReleasePhase.UPDATE_ENGINE_VERSION: + final Remote upstream = Remote.upstream(state.framework.upstream.url); + final FrameworkRepository framework = FrameworkRepository( + checkouts, + initialRef: state.framework.workingBranch, + upstreamRemote: upstream, + previousCheckoutLocation: state.framework.checkoutPath, + ); + final String rev = await framework.reverseParse('HEAD'); + final File engineVersionFile = (await framework.checkoutDirectory) + .childDirectory('bin') + .childDirectory('internal') + .childFile('engine.version'); + + engineVersionFile.writeAsStringSync(rev); + + // Must force add since it is gitignored + await framework.git.run( + const ['add', 'bin/internal/engine.version', '--force'], + 'adding engine.version file', + workingDirectory: (await framework.checkoutDirectory).path, + ); + final String revision = await framework.commit( + 'Create engine.version file pointing to $rev', + ); + // append to list of cherrypicks so we know a PR is required + state.framework.cherrypicks.add( + pb.Cherrypick.create() + ..appliedRevision = revision + ..state = pb.CherrypickState.COMPLETED, + ); + + if (!autoAccept) { + final bool response = await prompt( + 'Are you ready to push your framework branch to the repository ' + '${state.framework.mirror.url}?', + ); + if (!response) { + stdio.printError('Aborting command.'); + updateState(state, stdio.logs); + return; + } + } + await pushWorkingBranch(framework, state.framework); case pb.ReleasePhase.PUBLISH_VERSION: final String command = ''' diff --git a/dev/conductor/core/lib/src/proto/conductor_state.pbenum.dart b/dev/conductor/core/lib/src/proto/conductor_state.pbenum.dart index f40a50ef2e..a62cdfd86a 100644 --- a/dev/conductor/core/lib/src/proto/conductor_state.pbenum.dart +++ b/dev/conductor/core/lib/src/proto/conductor_state.pbenum.dart @@ -20,15 +20,18 @@ import 'package:protobuf/protobuf.dart' as $pb; class ReleasePhase extends $pb.ProtobufEnum { static const ReleasePhase APPLY_FRAMEWORK_CHERRYPICKS = ReleasePhase._(0, _omitEnumNames ? '' : 'APPLY_FRAMEWORK_CHERRYPICKS'); + static const ReleasePhase UPDATE_ENGINE_VERSION = + ReleasePhase._(1, _omitEnumNames ? '' : 'UPDATE_ENGINE_VERSION'); static const ReleasePhase PUBLISH_VERSION = - ReleasePhase._(1, _omitEnumNames ? '' : 'PUBLISH_VERSION'); + ReleasePhase._(2, _omitEnumNames ? '' : 'PUBLISH_VERSION'); static const ReleasePhase VERIFY_RELEASE = - ReleasePhase._(2, _omitEnumNames ? '' : 'VERIFY_RELEASE'); + ReleasePhase._(3, _omitEnumNames ? '' : 'VERIFY_RELEASE'); static const ReleasePhase RELEASE_COMPLETED = - ReleasePhase._(3, _omitEnumNames ? '' : 'RELEASE_COMPLETED'); + ReleasePhase._(4, _omitEnumNames ? '' : 'RELEASE_COMPLETED'); static const $core.List values = [ APPLY_FRAMEWORK_CHERRYPICKS, + UPDATE_ENGINE_VERSION, PUBLISH_VERSION, VERIFY_RELEASE, RELEASE_COMPLETED, diff --git a/dev/conductor/core/lib/src/proto/conductor_state.pbjson.dart b/dev/conductor/core/lib/src/proto/conductor_state.pbjson.dart index 30ce3a32ed..53bf1cbc87 100644 --- a/dev/conductor/core/lib/src/proto/conductor_state.pbjson.dart +++ b/dev/conductor/core/lib/src/proto/conductor_state.pbjson.dart @@ -22,16 +22,18 @@ const ReleasePhase$json = { '1': 'ReleasePhase', '2': [ {'1': 'APPLY_FRAMEWORK_CHERRYPICKS', '2': 0}, - {'1': 'PUBLISH_VERSION', '2': 1}, - {'1': 'VERIFY_RELEASE', '2': 2}, - {'1': 'RELEASE_COMPLETED', '2': 3}, + {'1': 'UPDATE_ENGINE_VERSION', '2': 1}, + {'1': 'PUBLISH_VERSION', '2': 2}, + {'1': 'VERIFY_RELEASE', '2': 3}, + {'1': 'RELEASE_COMPLETED', '2': 4}, ], }; /// Descriptor for `ReleasePhase`. Decode as a `google.protobuf.EnumDescriptorProto`. final $typed_data.Uint8List releasePhaseDescriptor = $convert - .base64Decode('CgxSZWxlYXNlUGhhc2USHwobQVBQTFlfRlJBTUVXT1JLX0NIRVJSWVBJQ0tTEAASEwoPUFVCTE' - 'lTSF9WRVJTSU9OEAESEgoOVkVSSUZZX1JFTEVBU0UQAhIVChFSRUxFQVNFX0NPTVBMRVRFRBAD'); + .base64Decode('CgxSZWxlYXNlUGhhc2USHwobQVBQTFlfRlJBTUVXT1JLX0NIRVJSWVBJQ0tTEAASGQoVVVBEQV' + 'RFX0VOR0lORV9WRVJTSU9OEAESEwoPUFVCTElTSF9WRVJTSU9OEAISEgoOVkVSSUZZX1JFTEVB' + 'U0UQAxIVChFSRUxFQVNFX0NPTVBMRVRFRBAE'); @$core.Deprecated('Use cherrypickStateDescriptor instead') const CherrypickState$json = { diff --git a/dev/conductor/core/lib/src/proto/conductor_state.proto b/dev/conductor/core/lib/src/proto/conductor_state.proto index a37ff917f8..71c6eec1af 100644 --- a/dev/conductor/core/lib/src/proto/conductor_state.proto +++ b/dev/conductor/core/lib/src/proto/conductor_state.proto @@ -11,14 +11,16 @@ message Remote { enum ReleasePhase { APPLY_FRAMEWORK_CHERRYPICKS = 0; + UPDATE_ENGINE_VERSION = 1; + // Git tag applied to framework RC branch HEAD and pushed upstream. - PUBLISH_VERSION = 1; + PUBLISH_VERSION = 2; // Package artifacts verified to exist on cloud storage. - VERIFY_RELEASE = 2; + VERIFY_RELEASE = 3; // There is no further work to be done. - RELEASE_COMPLETED = 3; + RELEASE_COMPLETED = 4; } enum CherrypickState { diff --git a/dev/conductor/core/lib/src/repository.dart b/dev/conductor/core/lib/src/repository.dart index 037cada1df..112efb07b5 100644 --- a/dev/conductor/core/lib/src/repository.dart +++ b/dev/conductor/core/lib/src/repository.dart @@ -411,16 +411,18 @@ abstract class Repository { } Future commit(String message, {bool addFirst = false, String? author}) async { - final bool hasChanges = - (await git.getOutput( - ['status', '--porcelain'], - 'check for uncommitted changes', - workingDirectory: (await checkoutDirectory).path, - )).trim().isNotEmpty; - if (!hasChanges) { - throw ConductorException('Tried to commit with message $message but no changes were present'); - } if (addFirst) { + final bool hasChanges = + (await git.getOutput( + ['status', '--porcelain'], + 'check for uncommitted changes', + workingDirectory: (await checkoutDirectory).path, + )).trim().isNotEmpty; + if (!hasChanges) { + throw ConductorException( + 'Tried to commit with message $message but no changes were present', + ); + } await git.run( ['add', '--all'], 'add all changes to the index', diff --git a/dev/conductor/core/lib/src/state.dart b/dev/conductor/core/lib/src/state.dart index f9853c7844..24389f376c 100644 --- a/dev/conductor/core/lib/src/state.dart +++ b/dev/conductor/core/lib/src/state.dart @@ -156,6 +156,8 @@ String phaseInstructions(pb.ConductorState state) { return [ 'Either all cherrypicks have been auto-applied or there were none.', ].join('\n'); + case ReleasePhase.UPDATE_ENGINE_VERSION: + return 'The conductor will now update the engine.version file to point at the previous commit.'; case ReleasePhase.PUBLISH_VERSION: if (!requiresFrameworkPR(state)) { return 'Since there are no code changes in this release, no Framework ' @@ -223,18 +225,11 @@ String githubAccount(String remoteUrl) { /// Will throw a [ConductorException] if [ReleasePhase.RELEASE_COMPLETED] is /// passed as an argument, as there is no next phase. ReleasePhase getNextPhase(ReleasePhase currentPhase) { - switch (currentPhase) { - case ReleasePhase.PUBLISH_VERSION: - return ReleasePhase.VERIFY_RELEASE; - case ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS: - case ReleasePhase.VERIFY_RELEASE: - case ReleasePhase.RELEASE_COMPLETED: - final ReleasePhase? nextPhase = ReleasePhase.valueOf(currentPhase.value + 1); - if (nextPhase != null) { - return nextPhase; - } + final ReleasePhase? nextPhase = ReleasePhase.valueOf(currentPhase.value + 1); + if (nextPhase != null) { + return nextPhase; } - throw globals.ConductorException('There is no next ReleasePhase!'); + throw globals.ConductorException('There is no next ReleasePhase after $currentPhase!'); } // Indent two spaces. diff --git a/dev/conductor/core/test/next_test.dart b/dev/conductor/core/test/next_test.dart index 42956db112..0f5dea8878 100644 --- a/dev/conductor/core/test/next_test.dart +++ b/dev/conductor/core/test/next_test.dart @@ -23,7 +23,7 @@ void main() { const String candidateBranch = 'flutter-1.2-candidate.3'; const String workingBranch = 'cherrypicks-$candidateBranch'; const String revision1 = 'd3af60d18e01fcb36e0c0fa06c8502e4935ed095'; - const String revision3 = 'ffffffffffffffffffffffffffffffffffffffff'; + const String revision2 = 'ffffffffffffffffffffffffffffffffffffffff'; const String releaseVersion = '1.2.0-3.0.pre'; const String releaseChannel = 'beta'; const String stateFile = '/state-file.json'; @@ -70,7 +70,7 @@ void main() { ); }); - group('APPLY_FRAMEWORK_CHERRYPICKS to PUBLISH_VERSION', () { + group('APPLY_FRAMEWORK_CHERRYPICKS to UPDATE_ENGINE_VERSION', () { const String mirrorRemoteUrl = 'https://github.com/org/repo.git'; const String upstreamRemoteUrl = 'https://github.com/mirror/repo.git'; const String engineUpstreamRemoteUrl = 'https://github.com/mirror/engine.git'; @@ -154,7 +154,7 @@ void main() { 'Create candidate branch version $candidateBranch for $releaseChannel', ], ), - const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision3), + const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision2), ]); writeStateToFile(fileSystem.file(stateFile), state, []); final Checkouts checkouts = Checkouts( @@ -207,7 +207,7 @@ void main() { 'Create candidate branch version $candidateBranch for $releaseChannel', ], ), - const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision3), + const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision2), const FakeCommand( command: ['git', 'push', 'mirror', 'HEAD:refs/heads/$workingBranch'], ), @@ -226,7 +226,7 @@ void main() { final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile)); - expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION); + expect(finalState.currentPhase, ReleasePhase.UPDATE_ENGINE_VERSION); expect(stdio.stdout, contains('There was 1 cherrypick that was not auto-applied')); expect( stdio.stdout, @@ -241,6 +241,117 @@ void main() { expect(stdio.error, isEmpty); }); }); + group('UPDATE_ENGINE_VERSION to PUBLISH_VERSION', () { + const String mirrorRemoteUrl = 'https://github.com/org/repo.git'; + const String upstreamRemoteUrl = 'https://github.com/mirror/repo.git'; + const String engineUpstreamRemoteUrl = 'https://github.com/mirror/engine.git'; + const String frameworkCheckoutPath = '$checkoutsParentDirectory/framework'; + const String engineCheckoutPath = '$checkoutsParentDirectory/engine'; + const String oldEngineVersion = '000000001'; + + late FakeProcessManager processManager; + late FakePlatform platform; + late pb.ConductorState state; + + setUp(() { + processManager = FakeProcessManager.empty(); + platform = FakePlatform( + environment: { + 'HOME': ['path', 'to', 'home'].join(localPathSeparator), + }, + operatingSystem: localOperatingSystem, + pathSeparator: localPathSeparator, + ); + state = + (pb.ConductorState.create() + ..releaseChannel = releaseChannel + ..releaseVersion = releaseVersion + ..framework = + (pb.Repository.create() + ..candidateBranch = candidateBranch + ..checkoutPath = frameworkCheckoutPath + ..mirror = + (pb.Remote.create() + ..name = 'mirror' + ..url = mirrorRemoteUrl) + ..upstream = + (pb.Remote.create() + ..name = 'upstream' + ..url = upstreamRemoteUrl) + ..workingBranch = workingBranch) + ..engine = + (pb.Repository.create() + ..candidateBranch = candidateBranch + ..checkoutPath = engineCheckoutPath + ..dartRevision = 'cdef0123' + ..workingBranch = workingBranch + ..upstream = + (pb.Remote.create() + ..name = 'upstream' + ..url = engineUpstreamRemoteUrl)) + ..currentPhase = ReleasePhase.UPDATE_ENGINE_VERSION); + // create engine repo + fileSystem.directory(engineCheckoutPath).createSync(recursive: true); + // create framework repo + final Directory frameworkDir = fileSystem.directory(frameworkCheckoutPath); + final File engineRevisionFile = frameworkDir + .childDirectory('bin') + .childDirectory('internal') + .childFile('engine.version'); + engineRevisionFile.createSync(recursive: true); + engineRevisionFile.writeAsStringSync(oldEngineVersion, flush: true); + }); + + test('creates a PR with an updated engine.version file', () async { + // Respond "yes" to the prompt to push branch + stdio.stdin.add('y'); + processManager.addCommands(const [ + FakeCommand(command: ['git', 'fetch', 'upstream']), + FakeCommand(command: ['git', 'checkout', 'cherrypicks-$candidateBranch']), + FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision1), + FakeCommand(command: ['git', 'add', 'bin/internal/engine.version', '--force']), + FakeCommand( + command: [ + 'git', + 'commit', + '--message', + 'Create engine.version file pointing to $revision1', + ], + ), + FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: revision2), + FakeCommand( + command: [ + 'git', + 'push', + 'mirror', + 'HEAD:refs/heads/cherrypicks-$candidateBranch', + ], + ), + ]); + writeStateToFile(fileSystem.file(stateFile), state, []); + final Checkouts checkouts = Checkouts( + fileSystem: fileSystem, + parentDirectory: fileSystem.directory(checkoutsParentDirectory) + ..createSync(recursive: true), + platform: platform, + processManager: processManager, + stdio: stdio, + ); + + final CommandRunner runner = createRunner(checkouts: checkouts); + await runner.run(['next', '--$kStateOption', stateFile]); + + final pb.ConductorState finalState = readStateFromFile(fileSystem.file(stateFile)); + + expect(finalState.currentPhase, ReleasePhase.PUBLISH_VERSION); + expect( + fileSystem + .file('$frameworkCheckoutPath/bin/internal/engine.version') + .readAsStringSync(), + revision1, + ); + }); + }); group('PUBLISH_VERSION to VERIFY_RELEASE', () { const String releaseVersion = '1.2.0-3.0.pre'; diff --git a/dev/conductor/core/test/repository_test.dart b/dev/conductor/core/test/repository_test.dart index 6b448a343b..81d2370561 100644 --- a/dev/conductor/core/test/repository_test.dart +++ b/dev/conductor/core/test/repository_test.dart @@ -73,7 +73,7 @@ vars = { ); }); - test('commit() throws if there are no local changes to commit', () { + test('commit() throws if there are no local changes to commit and addFirst = true', () { const String commit1 = 'abc123'; const String commit2 = 'def456'; const String message = 'This is a commit message.'; @@ -106,7 +106,7 @@ vars = { final FrameworkRepository repo = FrameworkRepository(checkouts); expect( - () async => repo.commit(message), + () async => repo.commit(message, addFirst: true), throwsExceptionWith('Tried to commit with message $message but no changes were present'), ); }); @@ -129,10 +129,6 @@ vars = { ), const FakeCommand(command: ['git', 'checkout', FrameworkRepository.defaultBranch]), const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: commit1), - const FakeCommand( - command: ['git', 'status', '--porcelain'], - stdout: 'MM path/to/file.txt', - ), const FakeCommand(command: ['git', 'commit', '--message', message]), const FakeCommand(command: ['git', 'rev-parse', 'HEAD'], stdout: commit2), ]);