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

Commit

Permalink
[ci] Ensure complete dependabot coverage (#5976)
Browse files Browse the repository at this point in the history
  • Loading branch information
stuartmorgan committed Jun 22, 2022
1 parent dfd1b6e commit 3a1ae79
Show file tree
Hide file tree
Showing 6 changed files with 286 additions and 3 deletions.
1 change: 1 addition & 0 deletions .cirrus.yml
Expand Up @@ -113,6 +113,7 @@ task:
# run with --require-excerpts and no exclusions.
- ./script/tool_runner.sh readme-check --require-excerpts --exclude=script/configs/temp_exclude_excerpt.yaml
license_script: dart $PLUGIN_TOOL license-check
dependabot_script: dart $PLUGIN_TOOL dependabot-check
- name: federated_safety
# This check is only meaningful for PRs, as it validates changes
# rather than state.
Expand Down
30 changes: 27 additions & 3 deletions .github/dependabot.yml
@@ -1,7 +1,15 @@
version: 2
updates:
- package-ecosystem: "gradle"
directory: "/packages/camera/camera/android"
directory: "/packages/camera/camera_android/android"
commit-message:
prefix: "[camera]"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/camera/camera_android/example/android/app"
commit-message:
prefix: "[camera]"
schedule:
Expand Down Expand Up @@ -216,6 +224,14 @@ updates:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/shared_preferences/shared_preferences_android/android"
commit-message:
prefix: "[shared_pref]"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/shared_preferences/shared_preferences_android/example/android/app"
commit-message:
Expand Down Expand Up @@ -248,6 +264,14 @@ updates:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/video_player/video_player/example/android/app"
commit-message:
prefix: "[video_player]"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/video_player/video_player_android/android"
commit-message:
Expand Down Expand Up @@ -281,13 +305,13 @@ updates:
open-pull-requests-limit: 10

- package-ecosystem: "gradle"
directory: "/packages/webview_flutter/webview_flutter_android/example/android"
directory: "/packages/webview_flutter/webview_flutter_android/example/android/app"
commit-message:
prefix: "[webview]"
schedule:
interval: "weekly"
open-pull-requests-limit: 10

- package-ecosystem: "github-actions"
directory: "/"
commit-message:
Expand Down
1 change: 1 addition & 0 deletions script/tool/CHANGELOG.md
Expand Up @@ -3,6 +3,7 @@
- Supports empty custom analysis allow list files.
- `drive-examples` now validates files to ensure that they don't accidentally
use `test(...)`.
- Adds a new `dependabot-check` command to ensure complete Dependabot coverage.

## 0.8.6

Expand Down
114 changes: 114 additions & 0 deletions script/tool/lib/src/dependabot_check_command.dart
@@ -0,0 +1,114 @@
// Copyright 2013 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:async';

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

import 'common/core.dart';
import 'common/package_looping_command.dart';
import 'common/repository_package.dart';

/// A command to verify Dependabot configuration coverage of packages.
class DependabotCheckCommand extends PackageLoopingCommand {
/// Creates Dependabot check command instance.
DependabotCheckCommand(Directory packagesDir, {GitDir? gitDir})
: super(packagesDir, gitDir: gitDir) {
argParser.addOption(_configPathFlag,
help: 'Path to the Dependabot configuration file',
defaultsTo: '.github/dependabot.yml');
}

static const String _configPathFlag = 'config';

late Directory _repoRoot;

// The set of directories covered by "gradle" entries in the config.
Set<String> _gradleDirs = const <String>{};

@override
final String name = 'dependabot-check';

@override
final String description =
'Checks that all packages have Dependabot coverage.';

@override
final PackageLoopingType packageLoopingType =
PackageLoopingType.includeAllSubpackages;

@override
final bool hasLongOutput = false;

@override
Future<void> initializeRun() async {
_repoRoot = packagesDir.fileSystem.directory((await gitDir).path);

final YamlMap config = loadYaml(_repoRoot
.childFile(getStringArg(_configPathFlag))
.readAsStringSync()) as YamlMap;
final dynamic entries = config['updates'];
if (entries is! YamlList) {
return;
}

const String typeKey = 'package-ecosystem';
const String dirKey = 'directory';
_gradleDirs = entries
.where((dynamic entry) => entry[typeKey] == 'gradle')
.map((dynamic entry) => (entry as YamlMap)[dirKey] as String)
.toSet();
}

@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
bool skipped = true;
final List<String> errors = <String>[];

final RunState gradleState = _validateDependabotGradleCoverage(package);
skipped = skipped && gradleState == RunState.skipped;
if (gradleState == RunState.failed) {
printError('${indentation}Missing Gradle coverage.');
errors.add('Missing Gradle coverage');
}

// TODO(stuartmorgan): Add other ecosystem checks here as more are enabled.

if (skipped) {
return PackageResult.skip('No supported package ecosystems');
}
return errors.isEmpty
? PackageResult.success()
: PackageResult.fail(errors);
}

/// Returns the state for the Dependabot coverage of the Gradle ecosystem for
/// [package]:
/// - succeeded if it includes gradle and is covered.
/// - failed if it includes gradle and is not covered.
/// - skipped if it doesn't include gradle.
RunState _validateDependabotGradleCoverage(RepositoryPackage package) {
final Directory androidDir =
package.platformDirectory(FlutterPlatform.android);
final Directory appDir = androidDir.childDirectory('app');
if (appDir.existsSync()) {
// It's an app, so only check for the app directory to be covered.
final String dependabotPath =
'/${getRelativePosixPath(appDir, from: _repoRoot)}';
return _gradleDirs.contains(dependabotPath)
? RunState.succeeded
: RunState.failed;
} else if (androidDir.existsSync()) {
// It's a library, so only check for the android directory to be covered.
final String dependabotPath =
'/${getRelativePosixPath(androidDir, from: _repoRoot)}';
return _gradleDirs.contains(dependabotPath)
? RunState.succeeded
: RunState.failed;
}
return RunState.skipped;
}
}
2 changes: 2 additions & 0 deletions script/tool/lib/src/main.dart
Expand Up @@ -7,6 +7,7 @@ import 'dart:io' as io;
import 'package:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/local.dart';
import 'package:flutter_plugin_tools/src/dependabot_check_command.dart';

import 'analyze_command.dart';
import 'build_examples_command.dart';
Expand Down Expand Up @@ -55,6 +56,7 @@ void main(List<String> args) {
..addCommand(BuildExamplesCommand(packagesDir))
..addCommand(CreateAllPluginsAppCommand(packagesDir))
..addCommand(CustomTestCommand(packagesDir))
..addCommand(DependabotCheckCommand(packagesDir))
..addCommand(DriveExamplesCommand(packagesDir))
..addCommand(FederationSafetyCheckCommand(packagesDir))
..addCommand(FirebaseTestLabCommand(packagesDir))
Expand Down
141 changes: 141 additions & 0 deletions script/tool/test/dependabot_check_command_test.dart
@@ -0,0 +1,141 @@
// Copyright 2013 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:args/command_runner.dart';
import 'package:file/file.dart';
import 'package:file/memory.dart';
import 'package:flutter_plugin_tools/src/common/core.dart';
import 'package:flutter_plugin_tools/src/dependabot_check_command.dart';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';

import 'common/plugin_command_test.mocks.dart';
import 'util.dart';

void main() {
late CommandRunner<void> runner;
late FileSystem fileSystem;
late Directory root;
late Directory packagesDir;

setUp(() {
fileSystem = MemoryFileSystem();
root = fileSystem.currentDirectory;
packagesDir = root.childDirectory('packages');

final MockGitDir gitDir = MockGitDir();
when(gitDir.path).thenReturn(root.path);

final DependabotCheckCommand command = DependabotCheckCommand(
packagesDir,
gitDir: gitDir,
);
runner = CommandRunner<void>(
'dependabot_test', 'Test for $DependabotCheckCommand');
runner.addCommand(command);
});

void _setDependabotCoverage({
Iterable<String> gradleDirs = const <String>[],
}) {
final Iterable<String> gradleEntries =
gradleDirs.map((String directory) => '''
- package-ecosystem: "gradle"
directory: "/$directory"
schedule:
interval: "daily"
''');
final File configFile =
root.childDirectory('.github').childFile('dependabot.yml');
configFile.createSync(recursive: true);
configFile.writeAsStringSync('''
version: 2
updates:
${gradleEntries.join('\n')}
''');
}

test('skips with no supported ecosystems', () async {
_setDependabotCoverage();
createFakePackage('a_package', packagesDir);

final List<String> output =
await runCapturingPrint(runner, <String>['dependabot-check']);

expect(
output,
containsAllInOrder(<Matcher>[
contains('SKIPPING: No supported package ecosystems'),
]));
});

test('fails for app missing Gradle coverage', () async {
_setDependabotCoverage();
final RepositoryPackage package =
createFakePackage('a_package', packagesDir);
package.directory
.childDirectory('example')
.childDirectory('android')
.childDirectory('app')
.createSync(recursive: true);

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['dependabot-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Missing Gradle coverage.'),
contains('a_package/example:\n'
' Missing Gradle coverage')
]));
});

test('fails for plugin missing Gradle coverage', () async {
_setDependabotCoverage();
final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir);
plugin.directory.childDirectory('android').createSync(recursive: true);

Error? commandError;
final List<String> output = await runCapturingPrint(
runner, <String>['dependabot-check'], errorHandler: (Error e) {
commandError = e;
});

expect(commandError, isA<ToolExit>());
expect(
output,
containsAllInOrder(<Matcher>[
contains('Missing Gradle coverage.'),
contains('a_plugin:\n'
' Missing Gradle coverage')
]));
});

test('passes for correct Gradle coverage', () async {
_setDependabotCoverage(gradleDirs: <String>[
'packages/a_plugin/android',
'packages/a_plugin/example/android/app',
]);
final RepositoryPackage plugin = createFakePlugin('a_plugin', packagesDir);
// Test the plugin.
plugin.directory.childDirectory('android').createSync(recursive: true);
// And its example app.
plugin.directory
.childDirectory('example')
.childDirectory('android')
.childDirectory('app')
.createSync(recursive: true);

final List<String> output =
await runCapturingPrint(runner, <String>['dependabot-check']);

expect(output,
containsAllInOrder(<Matcher>[contains('Ran for 2 package(s)')]));
});
}

0 comments on commit 3a1ae79

Please sign in to comment.