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

[ci] Ensure complete dependabot coverage #5976

Merged
merged 5 commits into from
Jun 22, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .cirrus.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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: "daily"
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: "daily"
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: "daily"
open-pull-requests-limit: 10

- package-ecosystem: "github-actions"
directory: "/"
commit-message:
Expand Down
1 change: 1 addition & 0 deletions script/tool/CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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)')]));
});
}