Skip to content

Commit

Permalink
Update conductor to write engine.version file (flutter#163350)
Browse files Browse the repository at this point in the history
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 flutter#162265
  • Loading branch information
christopherfujino authored Feb 27, 2025
1 parent 89f1eba commit eb66d03
Show file tree
Hide file tree
Showing 8 changed files with 198 additions and 42 deletions.
45 changes: 45 additions & 0 deletions dev/conductor/core/lib/src/next.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '''
Expand Down
9 changes: 6 additions & 3 deletions dev/conductor/core/lib/src/proto/conductor_state.pbenum.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
12 changes: 7 additions & 5 deletions dev/conductor/core/lib/src/proto/conductor_state.pbjson.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
8 changes: 5 additions & 3 deletions dev/conductor/core/lib/src/proto/conductor_state.proto
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
20 changes: 11 additions & 9 deletions dev/conductor/core/lib/src/repository.dart
Original file line number Diff line number Diff line change
Expand Up @@ -411,16 +411,18 @@ abstract class Repository {
}

Future<String> commit(String message, {bool addFirst = false, String? author}) async {
final bool hasChanges =
(await git.getOutput(
<String>['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(
<String>['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(
<String>['add', '--all'],
'add all changes to the index',
Expand Down
17 changes: 6 additions & 11 deletions dev/conductor/core/lib/src/state.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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 '
Expand Down Expand Up @@ -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.
Expand Down
121 changes: 116 additions & 5 deletions dev/conductor/core/test/next_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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';
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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'],
),
Expand All @@ -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,
Expand All @@ -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';
Expand Down
8 changes: 2 additions & 6 deletions dev/conductor/core/test/repository_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.';
Expand Down Expand Up @@ -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'),
);
});
Expand All @@ -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),
]);
Expand Down

0 comments on commit eb66d03

Please sign in to comment.