Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 3 additions & 4 deletions .github/workflows/go_router_batch.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@ name: "Creates Batch Release for go_router"

on:
workflow_dispatch:
# Uncomment the cron schedule when ready.
# schedule:
# # Run every Monday at 8:00 AM
# - cron: "0 8 * * 1"
schedule:
# Run every Monday at 8:00 AM
- cron: "0 8 * * 1"

jobs:
dispatch_release_pr:
Expand Down
3 changes: 1 addition & 2 deletions packages/go_router/ci_config.yaml
Original file line number Diff line number Diff line change
@@ -1,3 +1,2 @@
release:
# TODO(chunhtai): Opt in when ready.
batch: false
batch: true
3 changes: 3 additions & 0 deletions packages/go_router/pending_changelogs/convert_batch.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
changelog: |
- Converts go_router to use batch release.
version: skip
6 changes: 6 additions & 0 deletions packages/go_router/pending_changelogs/template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Use this file as template to draft a unreleased changelog file.
# Make a copy of this file in the same directory, rename it, and fill in the details.
changelog: |
- Can include a list of changes.
- with markdown supported.
version: <major|minor|patch|skip>
207 changes: 207 additions & 0 deletions script/tool/lib/src/repo_package_info_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

import 'dart:io' as io;

import 'package:file/file.dart';
import 'package:yaml/yaml.dart';

import 'common/core.dart';
import 'common/output_utils.dart';
Expand Down Expand Up @@ -149,6 +152,8 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand {
errors.add(e.message);
}

errors.addAll(await _validateFilesBasedOnReleaseStrategy(package));

// All published packages should have a README.md entry.
if (package.isPublishable()) {
errors.addAll(_validateRootReadme(package));
Expand Down Expand Up @@ -264,4 +269,206 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand {
return 'p: $packageName';
}
}

Future<List<String>> _validateFilesBasedOnReleaseStrategy(
RepositoryPackage package,
) async {
final errors = <String>[];
final bool isBatchRelease =
package.parseCIConfig()?.isBatchRelease ?? false;
final String packageName = package.directory.basename;
final Directory workflowDir = _repoRoot
.childDirectory('.github')
.childDirectory('workflows');

errors.addAll(
_validateSpecificBatchWorkflow(
packageName,
workflowDir: workflowDir,
isBatchRelease: isBatchRelease,
),
);

errors.addAll(
_validateGlobalWorkflowTrigger(
'release_from_branches.yml',
workflowDir: workflowDir,
isBatchRelease: isBatchRelease,
packageName: packageName,
),
);
errors.addAll(
_validateGlobalWorkflowTrigger(
'sync_release_pr.yml',
workflowDir: workflowDir,
isBatchRelease: isBatchRelease,
packageName: packageName,
),
);

errors.addAll(
await _validateRemoteReleaseBranch(
packageName,
isBatchRelease: isBatchRelease,
),
);

return errors;
}

Future<List<String>> _validateRemoteReleaseBranch(
String packageName, {
required bool isBatchRelease,
}) async {
final errors = <String>[];
// Verify release branch exists on remote flutter/packages if it is a batch release package.
final io.ProcessResult result = await (await gitDir).runCommand(<String>[
'ls-remote',
'--exit-code',
'--heads',
'origin',
'release-$packageName',
], throwOnError: false);
final branchExists = result.exitCode == 0;
if (isBatchRelease && !branchExists) {
errors.add(
'Branch release-$packageName does not exist on remote flutter/packages\n'
'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release',
);
}
// Allow branch to exist on remote flutter/packages for non-batch release packages.
// Otherwise, it will be hard to opt package out of batch release.
//
// Enforcing this will run into a deadlock where the ci in the PR to opts out of batch release
// will require removal of the release branch. but removing the release branch will immediately break
// the latest main.
return errors;
}

List<String> _validateSpecificBatchWorkflow(
String packageName, {
required Directory workflowDir,
required bool isBatchRelease,
}) {
final errors = <String>[];
final File batchWorkflowFile = workflowDir.childFile(
'${packageName}_batch.yml',
);
if (isBatchRelease) {
if (!batchWorkflowFile.existsSync()) {
errors.add(
'Missing batch workflow file: .github/workflows/${packageName}_batch.yml\n'
'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release',
);
} else {
// Validate content.
final String content = batchWorkflowFile.readAsStringSync();
YamlMap? yaml;
try {
yaml = loadYaml(content) as YamlMap?;
} catch (e) {
errors.add('Invalid YAML in ${packageName}_batch.yml: $e');
}

if (yaml != null) {
var foundDispatch = false;
final jobs = yaml['jobs'] as YamlMap?;
if (jobs != null) {
for (final Object? job in jobs.values) {
if (job is YamlMap && job['steps'] is YamlList) {
final steps = job['steps'] as YamlList;
for (final Object? step in steps) {
if (step is YamlMap &&
step['uses'] is String &&
(step['uses'] as String).startsWith(
'peter-evans/repository-dispatch',
)) {
final withArgs = step['with'] as YamlMap?;
if (withArgs != null &&
withArgs['event-type'] == 'batch-release-pr' &&
withArgs['client-payload'] ==
'{"package": "$packageName"}') {
foundDispatch = true;
}
}
}
}
}
}

if (!foundDispatch) {
errors.add(
'Invalid batch workflow content in ${packageName}_batch.yml. '
'Must contain a step using peter-evans/repository-dispatch with:\n'
' event-type: batch-release-pr\n'
' client-payload: \'{"package": "$packageName"}\'\n'
'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release',
);
}
}
}
} else {
if (batchWorkflowFile.existsSync()) {
errors.add(
'Unexpected batch workflow file: .github/workflows/${packageName}_batch.yml\n',
);
}
}
return errors;
}

List<String> _validateGlobalWorkflowTrigger(
String workflowName, {
required Directory workflowDir,
required bool isBatchRelease,
required String packageName,
}) {
final errors = <String>[];
final File workflowFile = workflowDir.childFile(workflowName);
if (!workflowFile.existsSync()) {
if (isBatchRelease) {
errors.add(
'Missing global workflow file: .github/workflows/$workflowName\n'
'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release',
);
}
return errors;
}

final String content = workflowFile.readAsStringSync();
YamlMap? yaml;
try {
yaml = loadYaml(content) as YamlMap?;
} catch (e) {
errors.add('Invalid YAML in $workflowName: $e');
}

var hasTrigger = false;
if (yaml != null) {
final on = yaml['on'] as YamlMap?;
if (on is YamlMap) {
final push = on['push'] as YamlMap?;
if (push is YamlMap) {
final branches = push['branches'] as YamlList?;
if (branches is YamlList) {
if (branches.contains('release-$packageName')) {
hasTrigger = true;
}
}
}
}
}

if (isBatchRelease && !hasTrigger) {
errors.add(
'Missing trigger for release-$packageName in .github/workflows/$workflowName\n'
'See https://github.com/flutter/flutter/blob/master/docs/ecosystem/release/README.md#batch-release',
);
} else if (!isBatchRelease && hasTrigger) {
errors.add(
'Unexpected trigger for release-$packageName in .github/workflows/$workflowName\n',
);
}
return errors;
}
}
Loading
Loading