Update conductor to write engine.version file (#163350)

This adds a new phase to the conductor after applying cherrypicks, to
update the engine.version file with the revision from the previous
commit. Note, this will produce a different PR, because it has to be in
a different commit after squash & merge.

Automates https://github.com/flutter/flutter/issues/162265
This commit is contained in:
Christopher Fujino 2025-02-27 14:24:40 -08:00 committed by GitHub
parent 89f1eba3c8
commit eb66d03350
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 198 additions and 42 deletions

View File

@ -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 <String>['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 = '''

View File

@ -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<ReleasePhase> values = <ReleasePhase>[
APPLY_FRAMEWORK_CHERRYPICKS,
UPDATE_ENGINE_VERSION,
PUBLISH_VERSION,
VERIFY_RELEASE,
RELEASE_COMPLETED,

View File

@ -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 = {

View File

@ -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 {

View File

@ -411,6 +411,7 @@ abstract class Repository {
}
Future<String> commit(String message, {bool addFirst = false, String? author}) async {
if (addFirst) {
final bool hasChanges =
(await git.getOutput(
<String>['status', '--porcelain'],
@ -418,9 +419,10 @@ abstract class Repository {
workingDirectory: (await checkoutDirectory).path,
)).trim().isNotEmpty;
if (!hasChanges) {
throw ConductorException('Tried to commit with message $message but no changes were present');
throw ConductorException(
'Tried to commit with message $message but no changes were present',
);
}
if (addFirst) {
await git.run(
<String>['add', '--all'],
'add all changes to the index',

View File

@ -156,6 +156,8 @@ String phaseInstructions(pb.ConductorState state) {
return <String>[
'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;
}
}
throw globals.ConductorException('There is no next ReleasePhase!');
throw globals.ConductorException('There is no next ReleasePhase after $currentPhase!');
}
// Indent two spaces.

View File

@ -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: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
]);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
@ -207,7 +207,7 @@ void main() {
'Create candidate branch version $candidateBranch for $releaseChannel',
],
),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision3),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
const FakeCommand(
command: <String>['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: <String, String>{
'HOME': <String>['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>[
FakeCommand(command: <String>['git', 'fetch', 'upstream']),
FakeCommand(command: <String>['git', 'checkout', 'cherrypicks-$candidateBranch']),
FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision1),
FakeCommand(command: <String>['git', 'add', 'bin/internal/engine.version', '--force']),
FakeCommand(
command: <String>[
'git',
'commit',
'--message',
'Create engine.version file pointing to $revision1',
],
),
FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: revision2),
FakeCommand(
command: <String>[
'git',
'push',
'mirror',
'HEAD:refs/heads/cherrypicks-$candidateBranch',
],
),
]);
writeStateToFile(fileSystem.file(stateFile), state, <String>[]);
final Checkouts checkouts = Checkouts(
fileSystem: fileSystem,
parentDirectory: fileSystem.directory(checkoutsParentDirectory)
..createSync(recursive: true),
platform: platform,
processManager: processManager,
stdio: stdio,
);
final CommandRunner<void> runner = createRunner(checkouts: checkouts);
await runner.run(<String>['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';

View File

@ -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: <String>['git', 'checkout', FrameworkRepository.defaultBranch]),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit1),
const FakeCommand(
command: <String>['git', 'status', '--porcelain'],
stdout: 'MM path/to/file.txt',
),
const FakeCommand(command: <String>['git', 'commit', '--message', message]),
const FakeCommand(command: <String>['git', 'rev-parse', 'HEAD'], stdout: commit2),
]);