-
Notifications
You must be signed in to change notification settings - Fork 101
WIP - Implement code freeze mechanism at autosubmit #4998
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| // Copyright 2026 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 'dart:io'; | ||
|
|
||
| import 'package:native_assets_cli/native_assets_cli.dart'; | ||
|
|
||
| void main(List<String> args) async { | ||
| await build(args, (config, output) async { | ||
| // 1. Read the source file (the code freeze config) | ||
| final configFile = File('lib/configuration/code_freeze.yaml'); | ||
| final content = await configFile.readAsString(); | ||
|
|
||
| // 2. Define the path for the generated code | ||
| final outputFile = File('lib/src/generated_config.dart'); | ||
|
|
||
| // 3. Write the file as a raw string constant | ||
| await outputFile.writeAsString(""" | ||
| // GENERATED CODE - DO NOT MODIFY BY HAND | ||
| // Generated by hook/build.dart | ||
|
|
||
| const String codeFreezeConfigContent = | ||
| r'''$content'''; | ||
| """); | ||
|
|
||
| // 4. Tell Dart that this hook depends on the config file | ||
| output.addDependency(configFile.uri); | ||
| }); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| flutter/flutter: | ||
| frozen_labels: | ||
| - "f: material design" | ||
| - "f: cupertino" | ||
| frozen_paths: | ||
| - "packages/flutter/lib/src/material/" | ||
| - "packages/flutter/lib/src/cupertino/" |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,90 @@ | ||
| // Copyright 2026 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:github/github.dart'; | ||
| import 'package:json_annotation/json_annotation.dart'; | ||
| import 'package:meta/meta.dart'; | ||
| import 'package:yaml/yaml.dart'; | ||
|
|
||
| part 'code_freeze_configuration.g.dart'; | ||
|
|
||
| /// Configuration for repository-specific code freezes. | ||
| @JsonSerializable(explicitToJson: true) | ||
| @immutable | ||
| final class CodeFreezeConfiguration { | ||
| const CodeFreezeConfiguration([this.repoFreezeCriteria = const <String, FreezeCriteria>{}]); | ||
|
|
||
| /// A mapping of repository slugs to their freeze criteria. | ||
| @JsonKey(name: 'repoFreezeCriteria') | ||
| final Map<String, FreezeCriteria> repoFreezeCriteria; | ||
|
|
||
| /// Parses the configuration from a YAML string. | ||
| factory CodeFreezeConfiguration.fromYaml(String yaml) { | ||
| final yamlDoc = loadYaml(yaml) as YamlMap; | ||
| final map = <String, dynamic>{ | ||
| 'repoFreezeCriteria': yamlDoc.asMap, | ||
| }; | ||
| return CodeFreezeConfiguration.fromJson(map); | ||
| } | ||
|
|
||
| /// Creates [CodeFreezeConfiguration] from a [json] object. | ||
| factory CodeFreezeConfiguration.fromJson(Map<String, dynamic> json) => _$CodeFreezeConfigurationFromJson(json); | ||
|
|
||
| /// Converts [CodeFreezeConfiguration] to a [json] object. | ||
| Map<String, dynamic> toJson() => _$CodeFreezeConfigurationToJson(this); | ||
|
|
||
| /// Returns the freeze criteria for the given [slug]. | ||
| FreezeCriteria getFreezeCriteria(RepositorySlug slug) { | ||
| return repoFreezeCriteria[slug.fullName] ?? const FreezeCriteria(); | ||
| } | ||
| } | ||
|
|
||
| /// Criteria used to determine if a PR is affected by a code freeze. | ||
| @JsonSerializable() | ||
| @immutable | ||
| final class FreezeCriteria { | ||
| const FreezeCriteria({ | ||
| this.frozenLabels = const <String>{}, | ||
| this.frozenPaths = const <String>{}, | ||
| }); | ||
|
|
||
| final Set<String> frozenLabels; | ||
| final Set<String> frozenPaths; | ||
|
|
||
| /// Creates [FreezeCriteria] from a [json] object. | ||
| factory FreezeCriteria.fromJson(Map<String, dynamic> json) => _$FreezeCriteriaFromJson(json); | ||
|
|
||
| /// Converts [FreezeCriteria] to a [json] object. | ||
| Map<String, dynamic> toJson() => _$FreezeCriteriaToJson(this); | ||
|
|
||
| bool get isEmpty => frozenLabels.isEmpty && frozenPaths.isEmpty; | ||
| } | ||
|
|
||
| extension _YamlMapToMap on YamlMap { | ||
| Map<String, dynamic> get asMap => <String, dynamic>{ | ||
| for (final MapEntry(:key, :value) in entries) | ||
| if (value is YamlMap) | ||
| '$key': value.asMap | ||
| else if (value is YamlList) | ||
| '$key': value.asList | ||
| else if (value is YamlScalar) | ||
| '$key': value.value | ||
| else | ||
| '$key': value, | ||
| }; | ||
| } | ||
|
|
||
| extension _YamlListToList on YamlList { | ||
| List<dynamic> get asList => <dynamic>[ | ||
| for (final node in nodes) | ||
| if (node is YamlMap) | ||
| node.asMap | ||
| else if (node is YamlList) | ||
| node.asList | ||
| else if (node is YamlScalar) | ||
| node.value | ||
| else | ||
| node, | ||
| ]; | ||
| } | ||
|
Comment on lines
+64
to
+90
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was older code that I think can be deleted... but I could be wrong if |
||
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,64 @@ | ||
| // Copyright 2026 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:github/github.dart' as github; | ||
|
|
||
| import '../model/auto_submit_query_result.dart'; | ||
| import 'validation.dart'; | ||
|
|
||
| /// Validates that a pull request is not affected by an active code freeze. | ||
| class CodeFreeze extends Validation { | ||
| CodeFreeze({required super.config}); | ||
|
|
||
| @override | ||
| Future<ValidationResult> validate( | ||
| QueryResult result, | ||
| github.PullRequest pr, | ||
| ) async { | ||
| final slug = pr.base!.repo!.slug(); | ||
| final criteria = config.codeFreezeConfiguration.getFreezeCriteria(slug); | ||
|
|
||
| if (criteria.isEmpty) { | ||
| return ValidationResult(true, Action.IGNORE_FAILURE, ''); | ||
| } | ||
|
|
||
| // Check labels first as it is cheaper. | ||
| final prLabels = | ||
| pr.labels?.map((label) => label.name).toSet() ?? <String>{}; | ||
| final matchedLabels = criteria.frozenLabels.intersection(prLabels); | ||
| if (matchedLabels.isNotEmpty) { | ||
| final message = | ||
| 'This pull request is blocked due to an active code freeze for the following labels: ${matchedLabels.join(", ")}.'; | ||
| return ValidationResult(false, Action.REMOVE_LABEL, message); | ||
| } | ||
|
|
||
| // Check paths if frozen paths are defined. | ||
| if (criteria.frozenPaths.isNotEmpty) { | ||
| final githubService = await config.createGithubService(slug); | ||
| final files = await githubService.getPullRequestFiles(slug, pr); | ||
| final matchedPaths = <String>{}; | ||
|
|
||
| for (final file in files) { | ||
| final filename = file.filename; | ||
| if (filename == null) continue; | ||
| for (final frozenPath in criteria.frozenPaths) { | ||
| if (filename.startsWith(frozenPath)) { | ||
| matchedPaths.add(frozenPath); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| if (matchedPaths.isNotEmpty) { | ||
| final message = | ||
| 'This pull request is blocked due to an active code freeze for the following paths: ${matchedPaths.join(", ")}.'; | ||
| return ValidationResult(false, Action.REMOVE_LABEL, message); | ||
| } | ||
| } | ||
|
|
||
| return ValidationResult(true, Action.IGNORE_FAILURE, ''); | ||
| } | ||
|
|
||
| @override | ||
| String get name => 'CodeFreeze'; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,56 @@ | ||
| // Copyright 2026 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:auto_submit/configuration/code_freeze_configuration.dart'; | ||
| import 'package:github/github.dart'; | ||
| import 'package:test/test.dart'; | ||
|
|
||
| void main() { | ||
| group('CodeFreezeConfiguration', () { | ||
| test('parses YAML correctly', () { | ||
| const yaml = ''' | ||
| flutter/flutter: | ||
| frozen_labels: | ||
| - "f: material design" | ||
| - "f: cupertino" | ||
| frozen_paths: | ||
| - "packages/flutter/lib/src/material/" | ||
| - "packages/flutter/lib/src/cupertino/" | ||
| flutter/packages: | ||
| frozen_labels: | ||
| - "blocked" | ||
| '''; | ||
| final config = CodeFreezeConfiguration.fromYaml(yaml); | ||
|
|
||
| final flutterCriteria = config.getFreezeCriteria( | ||
| RepositorySlug('flutter', 'flutter'), | ||
| ); | ||
| expect( | ||
| flutterCriteria.frozenLabels, | ||
| containsAll(['f: material design', 'f: cupertino']), | ||
| ); | ||
| expect( | ||
| flutterCriteria.frozenPaths, | ||
| containsAll([ | ||
| 'packages/flutter/lib/src/material/', | ||
| 'packages/flutter/lib/src/cupertino/', | ||
| ]), | ||
| ); | ||
|
|
||
| final packagesCriteria = config.getFreezeCriteria( | ||
| RepositorySlug('flutter', 'packages'), | ||
| ); | ||
| expect(packagesCriteria.frozenLabels, contains('blocked')); | ||
| expect(packagesCriteria.frozenPaths, isEmpty); | ||
| }); | ||
|
|
||
| test('returns empty criteria for unknown slug', () { | ||
| final config = CodeFreezeConfiguration.fromYaml('{}'); | ||
| final criteria = config.getFreezeCriteria( | ||
| RepositorySlug('unknown', 'unknown'), | ||
| ); | ||
| expect(criteria.isEmpty, isTrue); | ||
| }); | ||
| }); | ||
| } |
Uh oh!
There was an error while loading. Please reload this page.