Skip to content

Commit d3f860d

Browse files
authored
[ci] Adds repo checks in main branch for batch release (#10485)
This fixes main branch part of flutter/flutter#176433 The work left is adjusting cocoon to also run this check in release branch The high level view of this pr. 1. Move ci_config.yaml and pending changelog parsing into thir own files and the parsed results are exposed as a getter method in RepositoryPackage so that it can be shared between branch_for_release, repo_package_info_check, and version_check 2. The format error of ci_config.yaml and changelog files are reported in repo_package_info_check. The other 2 assume they are correct formatted and throw directly if they are not. 3. update the version_check to be able to handle batch release package ## Pre-Review Checklist **Note**: The Flutter team is currently trialing the use of [Gemini Code Assist for GitHub](https://developers.google.com/gemini-code-assist/docs/review-github-code). Comments from the `gemini-code-assist` bot should not be taken as authoritative feedback from the Flutter team. If you find its comments useful you can update your code accordingly, but if you are unsure or disagree with the feedback, please feel free to wait for a Flutter team member's review for guidance on which automated comments should be addressed. [^1]: Regular contributors who have demonstrated familiarity with the repository guidelines only need to comment if the PR is not auto-exempted by repo tooling.
1 parent 03957f4 commit d3f860d

16 files changed

+1215
-288
lines changed

packages/go_router/pending_changelogs/template.yaml

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/go_router/pending_changelogs/test_only_1.yaml

Lines changed: 0 additions & 6 deletions
This file was deleted.

packages/go_router/pending_changelogs/test_only_2.yaml

Lines changed: 0 additions & 5 deletions
This file was deleted.

script/tool/lib/src/branch_for_batch_release_command.dart

Lines changed: 21 additions & 135 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ import 'dart:math' as math;
88
import 'package:file/file.dart';
99
import 'package:git/git.dart';
1010
import 'package:pub_semver/pub_semver.dart';
11-
import 'package:yaml/yaml.dart';
1211
import 'package:yaml_edit/yaml_edit.dart';
1312

1413
import 'common/core.dart';
@@ -19,10 +18,6 @@ import 'common/repository_package.dart';
1918
const int _kExitPackageMalformed = 3;
2019
const int _kGitFailedToPush = 4;
2120

22-
// The template file name used to draft a pending changelog file.
23-
// This file will not be picked up by the batch release process.
24-
const String _kTemplateFileName = 'template.yaml';
25-
2621
/// A command to create a remote branch with release changes for a single package.
2722
class BranchForBatchReleaseCommand extends PackageCommand {
2823
/// Creates a new `branch-for-batch-release` command.
@@ -69,10 +64,15 @@ class BranchForBatchReleaseCommand extends PackageCommand {
6964
final GitDir repository = await gitDir;
7065

7166
print('Parsing package "${package.displayName}"...');
72-
final _PendingChangelogs pendingChangelogs = await _getPendingChangelogs(
73-
package,
74-
);
75-
if (pendingChangelogs.entries.isEmpty) {
67+
final List<PendingChangelogEntry> pendingChangelogs;
68+
try {
69+
pendingChangelogs = package.getPendingChangelogs();
70+
} on FormatException catch (e) {
71+
printError('Failed to parse pending changelogs: ${e.message}');
72+
throw ToolExit(_kExitPackageMalformed);
73+
}
74+
75+
if (pendingChangelogs.isEmpty) {
7676
print('No pending changelogs found for ${package.displayName}.');
7777
return;
7878
}
@@ -85,7 +85,7 @@ class BranchForBatchReleaseCommand extends PackageCommand {
8585
throw ToolExit(_kExitPackageMalformed);
8686
}
8787
final _ReleaseInfo releaseInfo = _getReleaseInfo(
88-
pendingChangelogs.entries,
88+
pendingChangelogs,
8989
pubspec.version!,
9090
);
9191

@@ -101,77 +101,36 @@ class BranchForBatchReleaseCommand extends PackageCommand {
101101
git: repository,
102102
package: package,
103103
branchName: branchName,
104-
pendingChangelogFiles: pendingChangelogs.files,
104+
pendingChangelogFiles: pendingChangelogs
105+
.map<File>((PendingChangelogEntry e) => e.file)
106+
.toList(),
105107
releaseInfo: releaseInfo,
106108
remoteName: remoteName,
107109
);
108110
}
109111

110-
/// Returns the parsed changelog entries for the given package.
111-
///
112-
/// This method read through the files in the pending_changelogs folder
113-
/// and parsed each file as a changelog entry.
114-
///
115-
/// Throws a [ToolExit] if the package does not have a pending_changelogs folder.
116-
Future<_PendingChangelogs> _getPendingChangelogs(
117-
RepositoryPackage package,
118-
) async {
119-
final Directory pendingChangelogsDir = package.directory.childDirectory(
120-
'pending_changelogs',
121-
);
122-
if (!pendingChangelogsDir.existsSync()) {
123-
printError(
124-
'No pending_changelogs folder found for ${package.displayName}.',
125-
);
126-
throw ToolExit(_kExitPackageMalformed);
127-
}
128-
final List<File> pendingChangelogFiles = pendingChangelogsDir
129-
.listSync()
130-
.whereType<File>()
131-
.where(
132-
(File f) =>
133-
f.basename.endsWith('.yaml') && f.basename != _kTemplateFileName,
134-
)
135-
.toList();
136-
try {
137-
final List<_PendingChangelogEntry> entries = pendingChangelogFiles
138-
.map<_PendingChangelogEntry>(
139-
(File f) => _PendingChangelogEntry.parse(f.readAsStringSync()),
140-
)
141-
.toList();
142-
return _PendingChangelogs(entries, pendingChangelogFiles);
143-
} on FormatException catch (e) {
144-
printError('Malformed pending changelog file: $e');
145-
throw ToolExit(_kExitPackageMalformed);
146-
}
147-
}
148-
149112
/// Returns the release info for the given package.
150113
///
151114
/// This method read through the parsed changelog entries decide the new version
152115
/// by following the version change rules. See [_VersionChange] for more details.
153116
_ReleaseInfo _getReleaseInfo(
154-
List<_PendingChangelogEntry> pendingChangelogEntries,
117+
List<PendingChangelogEntry> pendingChangelogEntries,
155118
Version oldVersion,
156119
) {
157120
final changelogs = <String>[];
158-
int versionIndex = _VersionChange.skip.index;
121+
int versionIndex = VersionChange.skip.index;
159122
for (final entry in pendingChangelogEntries) {
160123
changelogs.add(entry.changelog);
161124
versionIndex = math.min(versionIndex, entry.version.index);
162125
}
163-
final _VersionChange effectiveVersionChange =
164-
_VersionChange.values[versionIndex];
126+
final VersionChange effectiveVersionChange =
127+
VersionChange.values[versionIndex];
165128

166129
final Version? newVersion = switch (effectiveVersionChange) {
167-
_VersionChange.skip => null,
168-
_VersionChange.major => Version(oldVersion.major + 1, 0, 0),
169-
_VersionChange.minor => Version(
170-
oldVersion.major,
171-
oldVersion.minor + 1,
172-
0,
173-
),
174-
_VersionChange.patch => Version(
130+
VersionChange.skip => null,
131+
VersionChange.major => Version(oldVersion.major + 1, 0, 0),
132+
VersionChange.minor => Version(oldVersion.major, oldVersion.minor + 1, 0),
133+
VersionChange.patch => Version(
175134
oldVersion.major,
176135
oldVersion.minor,
177136
oldVersion.patch + 1,
@@ -305,18 +264,6 @@ class BranchForBatchReleaseCommand extends PackageCommand {
305264
}
306265
}
307266

308-
/// A data class for pending changelogs.
309-
class _PendingChangelogs {
310-
/// Creates a new instance.
311-
_PendingChangelogs(this.entries, this.files);
312-
313-
/// The parsed pending changelog entries.
314-
final List<_PendingChangelogEntry> entries;
315-
316-
/// The files that the pending changelog entries were parsed from.
317-
final List<File> files;
318-
}
319-
320267
/// A data class for processed release information.
321268
class _ReleaseInfo {
322269
/// Creates a new instance.
@@ -328,64 +275,3 @@ class _ReleaseInfo {
328275
/// The combined changelog entries.
329276
final List<String> changelogs;
330277
}
331-
332-
/// The type of version change for a release.
333-
///
334-
/// The order of the enum values is important as it is used to determine which version
335-
/// take priority when multiple version changes are specified. The top most value
336-
/// (the samller the index) has the highest priority.
337-
enum _VersionChange {
338-
/// A major version change (e.g., 1.2.3 -> 2.0.0).
339-
major,
340-
341-
/// A minor version change (e.g., 1.2.3 -> 1.3.0).
342-
minor,
343-
344-
/// A patch version change (e.g., 1.2.3 -> 1.2.4).
345-
patch,
346-
347-
/// No version change.
348-
skip,
349-
}
350-
351-
/// Represents a single entry in the pending changelog.
352-
class _PendingChangelogEntry {
353-
/// Creates a new pending changelog entry.
354-
_PendingChangelogEntry({required this.changelog, required this.version});
355-
356-
/// Creates a PendingChangelogEntry from a YAML string.
357-
factory _PendingChangelogEntry.parse(String yamlContent) {
358-
final dynamic yaml = loadYaml(yamlContent);
359-
if (yaml is! YamlMap) {
360-
throw FormatException(
361-
'Expected a YAML map, but found ${yaml.runtimeType}.',
362-
);
363-
}
364-
365-
final dynamic changelogYaml = yaml['changelog'];
366-
if (changelogYaml is! String) {
367-
throw FormatException(
368-
'Expected "changelog" to be a string, but found ${changelogYaml.runtimeType}.',
369-
);
370-
}
371-
final String changelog = changelogYaml.trim();
372-
373-
final versionString = yaml['version'] as String?;
374-
if (versionString == null) {
375-
throw const FormatException('Missing "version" key.');
376-
}
377-
final _VersionChange version = _VersionChange.values.firstWhere(
378-
(_VersionChange e) => e.name == versionString,
379-
orElse: () =>
380-
throw FormatException('Invalid version type: $versionString'),
381-
);
382-
383-
return _PendingChangelogEntry(changelog: changelog, version: version);
384-
}
385-
386-
/// The changelog messages for this entry.
387-
final String changelog;
388-
389-
/// The type of version change for this entry.
390-
final _VersionChange version;
391-
}
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
// Copyright 2013 The Flutter Authors
2+
// Use of this source code is governed by a BSD-style license that can be
3+
// found in the LICENSE file.
4+
5+
import 'package:yaml/yaml.dart';
6+
7+
/// A class representing the parsed content of a `ci_config.yaml` file.
8+
class CIConfig {
9+
/// Creates a [CIConfig] from a parsed YAML map.
10+
CIConfig._(this.isBatchRelease);
11+
12+
/// Parses a [CIConfig] from a YAML string.
13+
///
14+
/// Throws if the YAML is not a valid ci_config.yaml.
15+
factory CIConfig.parse(String yaml) {
16+
final Object? loaded = loadYaml(yaml);
17+
if (loaded is! YamlMap) {
18+
throw const FormatException('Root of ci_config.yaml must be a map.');
19+
}
20+
21+
_checkCIConfigEntries(loaded, syntax: _validCIConfigSyntax);
22+
23+
var isBatchRelease = false;
24+
final Object? release = loaded['release'];
25+
if (release is Map) {
26+
isBatchRelease = release['batch'] == true;
27+
}
28+
29+
return CIConfig._(isBatchRelease);
30+
}
31+
32+
static const Map<String, Object?> _validCIConfigSyntax = <String, Object?>{
33+
'release': <String, Object?>{
34+
'batch': <bool>{true, false},
35+
},
36+
};
37+
38+
/// Returns true if the package is configured for batch release.
39+
final bool isBatchRelease;
40+
41+
static void _checkCIConfigEntries(
42+
YamlMap config, {
43+
required Map<String, Object?> syntax,
44+
String configPrefix = '',
45+
}) {
46+
for (final MapEntry<Object?, Object?> entry in config.entries) {
47+
if (!syntax.containsKey(entry.key)) {
48+
throw FormatException(
49+
'Unknown key `${entry.key}` in config${_formatConfigPrefix(configPrefix)}, the possible keys are ${syntax.keys.toList()}',
50+
);
51+
} else {
52+
final Object syntaxValue = syntax[entry.key]!;
53+
final newConfigPrefix = configPrefix.isEmpty
54+
? entry.key! as String
55+
: '$configPrefix.${entry.key}';
56+
if (syntaxValue is Set) {
57+
if (!syntaxValue.contains(entry.value)) {
58+
throw FormatException(
59+
'Invalid value `${entry.value}` for key${_formatConfigPrefix(newConfigPrefix)}, the possible values are ${syntaxValue.toList()}',
60+
);
61+
}
62+
} else if (entry.value is! YamlMap) {
63+
throw FormatException(
64+
'Invalid value `${entry.value}` for key${_formatConfigPrefix(newConfigPrefix)}, the value must be a map',
65+
);
66+
} else {
67+
_checkCIConfigEntries(
68+
entry.value! as YamlMap,
69+
syntax: syntaxValue as Map<String, Object?>,
70+
configPrefix: newConfigPrefix,
71+
);
72+
}
73+
}
74+
}
75+
}
76+
77+
static String _formatConfigPrefix(String configPrefix) =>
78+
configPrefix.isEmpty ? '' : ' `$configPrefix`';
79+
}

script/tool/lib/src/common/package_state_utils.dart

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,13 @@ Future<PackageChangeState> checkPackageChangeState(
8080
continue;
8181
}
8282

83+
if (package.parseCIConfig()?.isBatchRelease ?? false) {
84+
if (components.first == 'pending_changelogs') {
85+
hasChangelogChange = true;
86+
continue;
87+
}
88+
}
89+
8390
if (!needsVersionChange) {
8491
// Developer-only changes don't need version changes or changelog changes.
8592
if (await _isDevChange(components, git: git, repoPath: path)) {
@@ -206,6 +213,9 @@ Future<bool> _isDevChange(
206213
pathComponents.first == 'run_tests.sh' ||
207214
// CONTRIBUTING.md is dev-facing.
208215
pathComponents.last == 'CONTRIBUTING.md' ||
216+
// The top-level "pending_changelogs" directory is the repo convention for storing
217+
// pending changelog files.
218+
pathComponents.first == 'pending_changelogs' ||
209219
// Lints don't affect clients.
210220
pathComponents.contains('analysis_options.yaml') ||
211221
pathComponents.contains('lint-baseline.xml') ||

0 commit comments

Comments
 (0)