diff --git a/packages/go_router/ci_config.yaml b/packages/go_router/ci_config.yaml new file mode 100644 index 00000000000..f7160fb4571 --- /dev/null +++ b/packages/go_router/ci_config.yaml @@ -0,0 +1,3 @@ +release: + # TODO(chunhtai): Opt in when ready. + batch: false diff --git a/packages/go_router_builder/ci_config.yaml b/packages/go_router_builder/ci_config.yaml new file mode 100644 index 00000000000..f7160fb4571 --- /dev/null +++ b/packages/go_router_builder/ci_config.yaml @@ -0,0 +1,3 @@ +release: + # TODO(chunhtai): Opt in when ready. + batch: false diff --git a/script/tool/lib/src/common/repository_package.dart b/script/tool/lib/src/common/repository_package.dart index 85a642e0668..1b3e093a124 100644 --- a/script/tool/lib/src/common/repository_package.dart +++ b/script/tool/lib/src/common/repository_package.dart @@ -60,6 +60,9 @@ class RepositoryPackage { /// The package's top-level README. File get authorsFile => directory.childFile('AUTHORS'); + /// The package's top-level ci_config.yaml. + File get ciConfigFile => directory.childFile('ci_config.yaml'); + /// The lib directory containing the package's code. Directory get libDirectory => directory.childDirectory('lib'); diff --git a/script/tool/lib/src/repo_package_info_check_command.dart b/script/tool/lib/src/repo_package_info_check_command.dart index 808f3a88ccd..cebb74f9988 100644 --- a/script/tool/lib/src/repo_package_info_check_command.dart +++ b/script/tool/lib/src/repo_package_info_check_command.dart @@ -3,6 +3,7 @@ // found in the LICENSE file. import 'package:file/file.dart'; +import 'package:yaml/yaml.dart'; import 'common/core.dart'; import 'common/output_utils.dart'; @@ -12,6 +13,12 @@ import 'common/repository_package.dart'; const int _exitBadTableEntry = 3; const int _exitUnknownPackageEntry = 4; +const Map _validCiConfigSyntax = { + 'release': { + 'batch': {true, false} + }, +}; + /// A command to verify repository-level metadata about packages, such as /// repo README and CODEOWNERS entries. class RepoPackageInfoCheckCommand extends PackageLoopingCommand { @@ -108,6 +115,12 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { errors.add('Missing CODEOWNERS entry'); } + // The content of ci_config.yaml must be valid if there is one. + if (package.ciConfigFile.existsSync()) { + errors.addAll( + _validateCiConfig(package.ciConfigFile, mainPackage: package)); + } + // Any published package should be in the README table. // For federated plugins, only the app-facing package is listed. if (package.isPublishable() && @@ -190,6 +203,67 @@ class RepoPackageInfoCheckCommand extends PackageLoopingCommand { : PackageResult.fail(errors); } + List _validateCiConfig(File ciConfig, + {required RepositoryPackage mainPackage}) { + print('${indentation}Checking ' + '${getRelativePosixPath(ciConfig, from: mainPackage.directory)}...'); + final YamlMap config; + try { + final Object? yaml = loadYaml(ciConfig.readAsStringSync()); + if (yaml is! YamlMap) { + printError('${indentation}The ci_config.yaml file must be a map.'); + return ['Root of config is not a map.']; + } + config = yaml; + } on YamlException catch (e) { + printError( + '${indentation}Invalid YAML in ${getRelativePosixPath(ciConfig, from: mainPackage.directory)}:'); + printError(e.toString()); + return ['Invalid YAML']; + } + + return _checkCiConfigEntries(config, syntax: _validCiConfigSyntax); + } + + List _checkCiConfigEntries(YamlMap config, + {required Map syntax, String configPrefix = ''}) { + final List errors = []; + for (final MapEntry entry in config.entries) { + if (!syntax.containsKey(entry.key)) { + printError( + '${indentation}Unknown key `${entry.key}` in config${_formatConfigPrefix(configPrefix)}, the possible keys are ${syntax.keys.toList()}'); + errors.add( + 'Unknown key `${entry.key}` in config${_formatConfigPrefix(configPrefix)}'); + } else { + final Object syntaxValue = syntax[entry.key]!; + configPrefix = configPrefix.isEmpty + ? entry.key! as String + : '$configPrefix.${entry.key}'; + if (syntaxValue is Set) { + if (!syntaxValue.contains(entry.value)) { + printError( + '${indentation}Invalid value `${entry.value}` for key${_formatConfigPrefix(configPrefix)}, the possible values are ${syntaxValue.toList()}'); + errors.add( + 'Invalid value `${entry.value}` for key${_formatConfigPrefix(configPrefix)}'); + } + } else if (entry.value is! YamlMap) { + printError( + '${indentation}Invalid value `${entry.value}` for key${_formatConfigPrefix(configPrefix)}, the value must be a map'); + errors.add( + 'Invalid value `${entry.value}` for key${_formatConfigPrefix(configPrefix)}'); + } else { + errors.addAll(_checkCiConfigEntries(entry.value! as YamlMap, + syntax: syntaxValue as Map, + configPrefix: configPrefix)); + } + } + } + return errors; + } + + String _formatConfigPrefix(String configPrefix) => + configPrefix.isEmpty ? '' : ' `$configPrefix`'; + String _prTagForPackage(String packageName) => 'p: $packageName'; String _issueTagForPackage(String packageName) { diff --git a/script/tool/test/repo_package_info_check_command_test.dart b/script/tool/test/repo_package_info_check_command_test.dart index a5db19f18af..4de818f186b 100644 --- a/script/tool/test/repo_package_info_check_command_test.dart +++ b/script/tool/test/repo_package_info_check_command_test.dart @@ -408,4 +408,113 @@ ${readmeTableEntry(packageName)} ' Missing CODEOWNERS entry') ])); }); + + group('ci_config check', () { + test('control test', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeCodeOwners([package]); + + package.ciConfigFile.writeAsStringSync(''' +release: + batch: false + '''); + + final List output = + await runCapturingPrint(runner, ['repo-package-info-check']); + + expect( + output, + containsAll([ + contains(' Checking ci_config.yaml...'), + contains('No issues found!'), + ]), + ); + }); + + test('missing ci_config file is ok', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeCodeOwners([package]); + + final List output = + await runCapturingPrint(runner, ['repo-package-info-check']); + + expect( + output, + containsAllInOrder([ + contains('No issues found!'), + ]), + ); + }); + + test('fails for unknown key', () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeCodeOwners([package]); + package.ciConfigFile.writeAsStringSync(''' +something: true + '''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['repo-package-info-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains('Unknown key `something` in config, the possible keys are'), + ]), + ); + }); + + test('fails for invalid value type for batch property in release', + () async { + final RepositoryPackage package = + createFakePackage('a_package', packagesDir); + + root.childFile('README.md').writeAsStringSync(''' +${readmeTableHeader()} +${readmeTableEntry('a_package')} +'''); + writeCodeOwners([package]); + package.ciConfigFile.writeAsStringSync(''' +release: + batch: 1 + '''); + + Error? commandError; + final List output = await runCapturingPrint( + runner, ['repo-package-info-check'], errorHandler: (Error e) { + commandError = e; + }); + + expect(commandError, isA()); + expect( + output, + containsAllInOrder([ + contains( + 'Invalid value `1` for key `release.batch`, the possible values are [true, false]'), + ]), + ); + }); + }); }