diff --git a/dev/conductor/lib/globals.dart b/dev/conductor/lib/globals.dart index ff8adffd5a..68267470fa 100644 --- a/dev/conductor/lib/globals.dart +++ b/dev/conductor/lib/globals.dart @@ -7,6 +7,8 @@ import 'package:file/file.dart'; import 'package:file/local.dart'; import 'package:platform/platform.dart'; +import 'proto/conductor_state.pb.dart' as pb; + const String kUpstreamRemote = 'https://github.com/flutter/flutter.git'; const String gsutilBinary = 'gsutil.py'; @@ -140,11 +142,60 @@ String fromArgToEnvName(String argName) { } /// Return a web link for the user to open a new PR. +/// +/// Includes PR title and body via query params. String getNewPrLink({ required String userName, required String repoName, - required String candidateBranch, - required String workingBranch, + required pb.ConductorState state, }) { - return 'https://github.com/flutter/$repoName/compare/$candidateBranch...$userName:$workingBranch?expand=1'; + assert(state.releaseChannel.isNotEmpty); + assert(state.releaseVersion.isNotEmpty); + late final String candidateBranch; + late final String workingBranch; + late final String repoLabel; + switch (repoName) { + case 'flutter': + candidateBranch = state.framework.candidateBranch; + workingBranch = state.framework.workingBranch; + repoLabel = 'Framework'; + break; + case 'engine': + candidateBranch = state.engine.candidateBranch; + workingBranch = state.engine.workingBranch; + repoLabel = 'Engine'; + break; + default: + throw ConductorException('Expected repoName to be one of flutter or engine but got $repoName.'); + } + assert(candidateBranch.isNotEmpty); + assert(workingBranch.isNotEmpty); + final String title = '[flutter_releases] Flutter ${state.releaseChannel} ' + '${state.releaseVersion} $repoLabel Cherrypicks'; + final StringBuffer body = StringBuffer(); + body.write(''' +# Flutter ${state.releaseChannel} ${state.releaseVersion} $repoLabel + +## Scheduled Cherrypicks + +'''); + if (repoName == 'engine') { + if (state.engine.dartRevision.isNotEmpty) { + // shorten hashes to make final link manageable + body.writeln('- Roll dart revision: dart-lang/sdk@${state.engine.dartRevision.substring(0, 9)}'); + } + body.writeAll( + state.engine.cherrypicks.map((pb.Cherrypick cp) => '- commit: ${cp.trunkRevision.substring(0, 9)}'), + '\n', + ); + } else { + body.writeAll( + state.framework.cherrypicks.map((pb.Cherrypick cp) => '- commit: ${cp.trunkRevision.substring(0, 9)}'), + '\n', + ); + } + return 'https://github.com/flutter/$repoName/compare/$candidateBranch...$userName:$workingBranch?' + 'expand=1' + '&title=${Uri.encodeQueryComponent(title)}' + '&body=${Uri.encodeQueryComponent(body.toString())}'; } diff --git a/dev/conductor/lib/state.dart b/dev/conductor/lib/state.dart index c8167ac9d0..e0eabab44f 100644 --- a/dev/conductor/lib/state.dart +++ b/dev/conductor/lib/state.dart @@ -139,8 +139,7 @@ String phaseInstructions(pb.ConductorState state) { final String newPrLink = getNewPrLink( userName: githubAccount(state.engine.mirror.url), repoName: 'engine', - candidateBranch: state.engine.candidateBranch, - workingBranch: state.engine.workingBranch, + state: state, ); return [ 'Your working branch ${state.engine.workingBranch} was pushed to your mirror.', @@ -170,9 +169,8 @@ String phaseInstructions(pb.ConductorState state) { final String newPrLink = getNewPrLink( userName: githubAccount(state.framework.mirror.url), - repoName: 'framework', - candidateBranch: state.framework.candidateBranch, - workingBranch: state.framework.workingBranch, + repoName: 'flutter', + state: state, ); return [ 'Your working branch ${state.framework.workingBranch} was pushed to your mirror.', diff --git a/dev/conductor/test/globals_test.dart b/dev/conductor/test/globals_test.dart new file mode 100644 index 0000000000..cf28b19056 --- /dev/null +++ b/dev/conductor/test/globals_test.dart @@ -0,0 +1,130 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:conductor/globals.dart'; +import 'package:conductor/proto/conductor_state.pb.dart' as pb; + +import './common.dart'; + +void main() { + test('assertsEnabled returns true in test suite', () { + expect(assertsEnabled(), true); + }); + + group('getNewPrLink', () { + const String userName = 'flutterer'; + const String releaseChannel = 'beta'; + const String releaseVersion = '1.2.0-3.4.pre'; + const String candidateBranch = 'flutter-1.2-candidate.3'; + const String workingBranch = 'cherrypicks-$candidateBranch'; + const String dartRevision = 'fe9708ab688dcda9923f584ba370a66fcbc3811f'; + const String engineCherrypick1 = 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0'; + const String engineCherrypick2 = '94d06a2e1d01a3b0c693b94d70c5e1df9d78d249'; + const String frameworkCherrypick = + 'a5a25cd702b062c24b2c67b8d30b5cb33e0ef6f0'; + + final RegExp titlePattern = RegExp(r'&title=(.*)&'); + final RegExp bodyPattern = RegExp(r'&body=(.*)$'); + + late pb.ConductorState state; + + setUp(() { + state = pb.ConductorState( + engine: pb.Repository( + candidateBranch: candidateBranch, + cherrypicks: [ + pb.Cherrypick(trunkRevision: engineCherrypick1), + pb.Cherrypick(trunkRevision: engineCherrypick2), + ], + dartRevision: dartRevision, + workingBranch: workingBranch, + ), + framework: pb.Repository( + candidateBranch: candidateBranch, + cherrypicks: [ + pb.Cherrypick(trunkRevision: frameworkCherrypick), + ], + workingBranch: workingBranch, + ), + releaseChannel: releaseChannel, + releaseVersion: releaseVersion, + ); + }); + + test('throws on an invalid repoName', () { + expect( + () => getNewPrLink( + repoName: 'flooter', + userName: userName, + state: state, + ), + throwsExceptionWith( + 'Expected repoName to be one of flutter or engine but got flooter.', + ), + ); + }); + + test('returns a valid URL for engine', () { + final String link = getNewPrLink( + repoName: 'engine', + userName: userName, + state: state, + ); + expect( + link, + contains('https://github.com/flutter/engine/compare/'), + ); + expect( + link, + contains('$candidateBranch...$userName:$workingBranch?expand=1'), + ); + expect( + Uri.decodeQueryComponent( + titlePattern.firstMatch(link)?.group(1) ?? ''), + '[flutter_releases] Flutter $releaseChannel $releaseVersion Engine Cherrypicks'); + final String expectedBody = ''' +# Flutter $releaseChannel $releaseVersion Engine + +## Scheduled Cherrypicks + +- Roll dart revision: dart-lang/sdk@${dartRevision.substring(0, 9)} +- commit: ${engineCherrypick1.substring(0, 9)} +- commit: ${engineCherrypick2.substring(0, 9)}'''; + expect( + Uri.decodeQueryComponent(bodyPattern.firstMatch(link)?.group(1) ?? ''), + expectedBody, + ); + }); + + test('returns a valid URL for framework', () { + final String link = getNewPrLink( + repoName: 'flutter', + userName: userName, + state: state, + ); + expect( + link, + contains('https://github.com/flutter/flutter/compare/'), + ); + expect( + link, + contains('$candidateBranch...$userName:$workingBranch?expand=1'), + ); + expect( + Uri.decodeQueryComponent( + titlePattern.firstMatch(link)?.group(1) ?? ''), + '[flutter_releases] Flutter $releaseChannel $releaseVersion Framework Cherrypicks'); + final String expectedBody = ''' +# Flutter $releaseChannel $releaseVersion Framework + +## Scheduled Cherrypicks + +- commit: ${frameworkCherrypick.substring(0, 9)}'''; + expect( + Uri.decodeQueryComponent(bodyPattern.firstMatch(link)?.group(1) ?? ''), + expectedBody, + ); + }); + }); +} diff --git a/dev/conductor/test/next_test.dart b/dev/conductor/test/next_test.dart index b519c38b20..a96bcea2bf 100644 --- a/dev/conductor/test/next_test.dart +++ b/dev/conductor/test/next_test.dart @@ -23,9 +23,9 @@ void main() { const String workingBranch = 'cherrypicks-$candidateBranch'; final String localPathSeparator = const LocalPlatform().pathSeparator; final String localOperatingSystem = const LocalPlatform().pathSeparator; - const String revision1 = 'abc123'; - const String revision2 = 'def456'; - const String revision3 = '789aaa'; + const String revision1 = 'd3af60d18e01fcb36e0c0fa06c8502e4935ed095'; + const String revision2 = 'f99555c1e1392bf2a8135056b9446680c2af4ddf'; + const String revision3 = '98a5ca242b9d270ce000b26309b8a3cdc9c89df5'; const String releaseVersion = '1.2.0-3.0.pre'; const String releaseChannel = 'beta'; late MemoryFileSystem fileSystem; @@ -203,7 +203,7 @@ void main() { candidateBranch: candidateBranch, cherrypicks: [ pb.Cherrypick( - trunkRevision: 'abc123', + trunkRevision: revision2, state: pb.CherrypickState.PENDING, ), ], @@ -212,6 +212,7 @@ void main() { mirror: pb.Remote(name: 'mirror', url: remoteUrl), ), releaseChannel: releaseChannel, + releaseVersion: releaseVersion, ); writeStateToFile( fileSystem.file(stateFile), @@ -350,6 +351,7 @@ void main() { const String frameworkCheckoutPath = '$checkoutsParentDirectory/framework'; const String engineCheckoutPath = '$checkoutsParentDirectory/engine'; const String oldEngineVersion = '000000001'; + const String frameworkCherrypick = '431ae69b4dd2dd48f7ba0153671e0311014c958b'; late FakeProcessManager processManager; late FakePlatform platform; late pb.ConductorState state; @@ -371,7 +373,7 @@ void main() { checkoutPath: frameworkCheckoutPath, cherrypicks: [ pb.Cherrypick( - trunkRevision: 'abc123', + trunkRevision: frameworkCherrypick, state: pb.CherrypickState.PENDING, ), ], @@ -383,6 +385,7 @@ void main() { candidateBranch: candidateBranch, checkoutPath: engineCheckoutPath, dartRevision: 'cdef0123', + workingBranch: workingBranch, upstream: pb.Remote(name: 'upstream', url: engineUpstreamRemoteUrl), ), currentPhase: ReleasePhase.APPLY_FRAMEWORK_CHERRYPICKS,