Skip to content
This repository was archived by the owner on Feb 22, 2023. It is now read-only.

Commit 3a1ae79

Browse files
[ci] Ensure complete dependabot coverage (#5976)
1 parent dfd1b6e commit 3a1ae79

File tree

6 files changed

+286
-3
lines changed

6 files changed

+286
-3
lines changed

.cirrus.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ task:
113113
# run with --require-excerpts and no exclusions.
114114
- ./script/tool_runner.sh readme-check --require-excerpts --exclude=script/configs/temp_exclude_excerpt.yaml
115115
license_script: dart $PLUGIN_TOOL license-check
116+
dependabot_script: dart $PLUGIN_TOOL dependabot-check
116117
- name: federated_safety
117118
# This check is only meaningful for PRs, as it validates changes
118119
# rather than state.

.github/dependabot.yml

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
11
version: 2
22
updates:
33
- package-ecosystem: "gradle"
4-
directory: "/packages/camera/camera/android"
4+
directory: "/packages/camera/camera_android/android"
5+
commit-message:
6+
prefix: "[camera]"
7+
schedule:
8+
interval: "weekly"
9+
open-pull-requests-limit: 10
10+
11+
- package-ecosystem: "gradle"
12+
directory: "/packages/camera/camera_android/example/android/app"
513
commit-message:
614
prefix: "[camera]"
715
schedule:
@@ -216,6 +224,14 @@ updates:
216224
interval: "weekly"
217225
open-pull-requests-limit: 10
218226

227+
- package-ecosystem: "gradle"
228+
directory: "/packages/shared_preferences/shared_preferences_android/android"
229+
commit-message:
230+
prefix: "[shared_pref]"
231+
schedule:
232+
interval: "weekly"
233+
open-pull-requests-limit: 10
234+
219235
- package-ecosystem: "gradle"
220236
directory: "/packages/shared_preferences/shared_preferences_android/example/android/app"
221237
commit-message:
@@ -248,6 +264,14 @@ updates:
248264
interval: "weekly"
249265
open-pull-requests-limit: 10
250266

267+
- package-ecosystem: "gradle"
268+
directory: "/packages/video_player/video_player/example/android/app"
269+
commit-message:
270+
prefix: "[video_player]"
271+
schedule:
272+
interval: "weekly"
273+
open-pull-requests-limit: 10
274+
251275
- package-ecosystem: "gradle"
252276
directory: "/packages/video_player/video_player_android/android"
253277
commit-message:
@@ -281,13 +305,13 @@ updates:
281305
open-pull-requests-limit: 10
282306

283307
- package-ecosystem: "gradle"
284-
directory: "/packages/webview_flutter/webview_flutter_android/example/android"
308+
directory: "/packages/webview_flutter/webview_flutter_android/example/android/app"
285309
commit-message:
286310
prefix: "[webview]"
287311
schedule:
288312
interval: "weekly"
289313
open-pull-requests-limit: 10
290-
314+
291315
- package-ecosystem: "github-actions"
292316
directory: "/"
293317
commit-message:

script/tool/CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
- Supports empty custom analysis allow list files.
44
- `drive-examples` now validates files to ensure that they don't accidentally
55
use `test(...)`.
6+
- Adds a new `dependabot-check` command to ensure complete Dependabot coverage.
67

78
## 0.8.6
89

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
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 'dart:async';
6+
7+
import 'package:file/file.dart';
8+
import 'package:git/git.dart';
9+
import 'package:yaml/yaml.dart';
10+
11+
import 'common/core.dart';
12+
import 'common/package_looping_command.dart';
13+
import 'common/repository_package.dart';
14+
15+
/// A command to verify Dependabot configuration coverage of packages.
16+
class DependabotCheckCommand extends PackageLoopingCommand {
17+
/// Creates Dependabot check command instance.
18+
DependabotCheckCommand(Directory packagesDir, {GitDir? gitDir})
19+
: super(packagesDir, gitDir: gitDir) {
20+
argParser.addOption(_configPathFlag,
21+
help: 'Path to the Dependabot configuration file',
22+
defaultsTo: '.github/dependabot.yml');
23+
}
24+
25+
static const String _configPathFlag = 'config';
26+
27+
late Directory _repoRoot;
28+
29+
// The set of directories covered by "gradle" entries in the config.
30+
Set<String> _gradleDirs = const <String>{};
31+
32+
@override
33+
final String name = 'dependabot-check';
34+
35+
@override
36+
final String description =
37+
'Checks that all packages have Dependabot coverage.';
38+
39+
@override
40+
final PackageLoopingType packageLoopingType =
41+
PackageLoopingType.includeAllSubpackages;
42+
43+
@override
44+
final bool hasLongOutput = false;
45+
46+
@override
47+
Future<void> initializeRun() async {
48+
_repoRoot = packagesDir.fileSystem.directory((await gitDir).path);
49+
50+
final YamlMap config = loadYaml(_repoRoot
51+
.childFile(getStringArg(_configPathFlag))
52+
.readAsStringSync()) as YamlMap;
53+
final dynamic entries = config['updates'];
54+
if (entries is! YamlList) {
55+
return;
56+
}
57+
58+
const String typeKey = 'package-ecosystem';
59+
const String dirKey = 'directory';
60+
_gradleDirs = entries
61+
.where((dynamic entry) => entry[typeKey] == 'gradle')
62+
.map((dynamic entry) => (entry as YamlMap)[dirKey] as String)
63+
.toSet();
64+
}
65+
66+
@override
67+
Future<PackageResult> runForPackage(RepositoryPackage package) async {
68+
bool skipped = true;
69+
final List<String> errors = <String>[];
70+
71+
final RunState gradleState = _validateDependabotGradleCoverage(package);
72+
skipped = skipped && gradleState == RunState.skipped;
73+
if (gradleState == RunState.failed) {
74+
printError('${indentation}Missing Gradle coverage.');
75+
errors.add('Missing Gradle coverage');
76+
}
77+
78+
// TODO(stuartmorgan): Add other ecosystem checks here as more are enabled.
79+
80+
if (skipped) {
81+
return PackageResult.skip('No supported package ecosystems');
82+
}
83+
return errors.isEmpty
84+
? PackageResult.success()
85+
: PackageResult.fail(errors);
86+
}
87+
88+
/// Returns the state for the Dependabot coverage of the Gradle ecosystem for
89+
/// [package]:
90+
/// - succeeded if it includes gradle and is covered.
91+
/// - failed if it includes gradle and is not covered.
92+
/// - skipped if it doesn't include gradle.
93+
RunState _validateDependabotGradleCoverage(RepositoryPackage package) {
94+
final Directory androidDir =
95+
package.platformDirectory(FlutterPlatform.android);
96+
final Directory appDir = androidDir.childDirectory('app');
97+
if (appDir.existsSync()) {
98+
// It's an app, so only check for the app directory to be covered.
99+
final String dependabotPath =
100+
'/${getRelativePosixPath(appDir, from: _repoRoot)}';
101+
return _gradleDirs.contains(dependabotPath)
102+
? RunState.succeeded
103+
: RunState.failed;
104+
} else if (androidDir.existsSync()) {
105+
// It's a library, so only check for the android directory to be covered.
106+
final String dependabotPath =
107+
'/${getRelativePosixPath(androidDir, from: _repoRoot)}';
108+
return _gradleDirs.contains(dependabotPath)
109+
? RunState.succeeded
110+
: RunState.failed;
111+
}
112+
return RunState.skipped;
113+
}
114+
}

script/tool/lib/src/main.dart

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import 'dart:io' as io;
77
import 'package:args/command_runner.dart';
88
import 'package:file/file.dart';
99
import 'package:file/local.dart';
10+
import 'package:flutter_plugin_tools/src/dependabot_check_command.dart';
1011

1112
import 'analyze_command.dart';
1213
import 'build_examples_command.dart';
@@ -55,6 +56,7 @@ void main(List<String> args) {
5556
..addCommand(BuildExamplesCommand(packagesDir))
5657
..addCommand(CreateAllPluginsAppCommand(packagesDir))
5758
..addCommand(CustomTestCommand(packagesDir))
59+
..addCommand(DependabotCheckCommand(packagesDir))
5860
..addCommand(DriveExamplesCommand(packagesDir))
5961
..addCommand(FederationSafetyCheckCommand(packagesDir))
6062
..addCommand(FirebaseTestLabCommand(packagesDir))
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2013 The Flutter Authors. All rights reserved.
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:args/command_runner.dart';
6+
import 'package:file/file.dart';
7+
import 'package:file/memory.dart';
8+
import 'package:flutter_plugin_tools/src/common/core.dart';
9+
import 'package:flutter_plugin_tools/src/dependabot_check_command.dart';
10+
import 'package:mockito/mockito.dart';
11+
import 'package:test/test.dart';
12+
13+
import 'common/plugin_command_test.mocks.dart';
14+
import 'util.dart';
15+
16+
void main() {
17+
late CommandRunner<void> runner;
18+
late FileSystem fileSystem;
19+
late Directory root;
20+
late Directory packagesDir;
21+
22+
setUp(() {
23+
fileSystem = MemoryFileSystem();
24+
root = fileSystem.currentDirectory;
25+
packagesDir = root.childDirectory('packages');
26+
27+
final MockGitDir gitDir = MockGitDir();
28+
when(gitDir.path).thenReturn(root.path);
29+
30+
final DependabotCheckCommand command = DependabotCheckCommand(
31+
packagesDir,
32+
gitDir: gitDir,
33+
);
34+
runner = CommandRunner<void>(
35+
'dependabot_test', 'Test for $DependabotCheckCommand');
36+
runner.addCommand(command);
37+
});
38+
39+
void _setDependabotCoverage({
40+
Iterable<String> gradleDirs = const <String>[],
41+
}) {
42+
final Iterable<String> gradleEntries =
43+
gradleDirs.map((String directory) => '''
44+
- package-ecosystem: "gradle"
45+
directory: "/$directory"
46+
schedule:
47+
interval: "daily"
48+
''');
49+
final File configFile =
50+
root.childDirectory('.github').childFile('dependabot.yml');
51+
configFile.createSync(recursive: true);
52+
configFile.writeAsStringSync('''
53+
version: 2
54+
updates:
55+
${gradleEntries.join('\n')}
56+
''');
57+
}
58+
59+
test('skips with no supported ecosystems', () async {
60+
_setDependabotCoverage();
61+
createFakePackage('a_package', packagesDir);
62+
63+
final List<String> output =
64+
await runCapturingPrint(runner, <String>['dependabot-check']);
65+
66+
expect(
67+
output,
68+
containsAllInOrder(<Matcher>[
69+
contains('SKIPPING: No supported package ecosystems'),
70+
]));
71+
});
72+
73+
test('fails for app missing Gradle coverage', () async {
74+
_setDependabotCoverage();
75+
final RepositoryPackage package =
76+
createFakePackage('a_package', packagesDir);
77+
package.directory
78+
.childDirectory('example')
79+
.childDirectory('android')
80+
.childDirectory('app')
81+
.createSync(recursive: true);
82+
83+
Error? commandError;
84+
final List<String> output = await runCapturingPrint(
85+
runner, <String>['dependabot-check'], errorHandler: (Error e) {
86+
commandError = e;
87+
});
88+
89+
expect(commandError, isA<ToolExit>());
90+
expect(
91+
output,
92+
containsAllInOrder(<Matcher>[
93+
contains('Missing Gradle coverage.'),
94+
contains('a_package/example:\n'
95+
' Missing Gradle coverage')
96+
]));
97+
});
98+
99+
test('fails for plugin missing Gradle coverage', () async {
100+
_setDependabotCoverage();
101+
final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir);
102+
plugin.directory.childDirectory('android').createSync(recursive: true);
103+
104+
Error? commandError;
105+
final List<String> output = await runCapturingPrint(
106+
runner, <String>['dependabot-check'], errorHandler: (Error e) {
107+
commandError = e;
108+
});
109+
110+
expect(commandError, isA<ToolExit>());
111+
expect(
112+
output,
113+
containsAllInOrder(<Matcher>[
114+
contains('Missing Gradle coverage.'),
115+
contains('a_plugin:\n'
116+
' Missing Gradle coverage')
117+
]));
118+
});
119+
120+
test('passes for correct Gradle coverage', () async {
121+
_setDependabotCoverage(gradleDirs: <String>[
122+
'packages/a_plugin/android',
123+
'packages/a_plugin/example/android/app',
124+
]);
125+
final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir);
126+
// Test the plugin.
127+
plugin.directory.childDirectory('android').createSync(recursive: true);
128+
// And its example app.
129+
plugin.directory
130+
.childDirectory('example')
131+
.childDirectory('android')
132+
.childDirectory('app')
133+
.createSync(recursive: true);
134+
135+
final List<String> output =
136+
await runCapturingPrint(runner, <String>['dependabot-check']);
137+
138+
expect(output,
139+
containsAllInOrder(<Matcher>[contains('Ran for 2 package(s)')]));
140+
});
141+
}

0 commit comments

Comments
 (0)