Skip to content
Open
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
18 changes: 18 additions & 0 deletions script/tool/lib/src/common/file_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// found in the LICENSE file.

import 'package:file/file.dart';
import 'package:path/path.dart' as p;

/// Returns a [File] created by appending all but the last item in [components]
/// to [base] as subdirectories, then appending the last as a file.
Expand Down Expand Up @@ -31,3 +32,20 @@ Directory childDirectoryWithSubcomponents(
}
return dir;
}

/// Returns the relative path from [from] to [entity] using [platformContext]
/// as the path context, but always formatting the result as a POSIX path
/// (using forward slashes).
///
/// This is useful for generating paths that will be used in configuration
/// files or command lines that expect POSIX paths, even when running on a
/// platform that uses a different path separator, or for display.
String relativePosixPath(
FileSystemEntity entity, {
required Directory from,
required p.Context platformContext,
}) => p.posix.joinAll(
platformContext.split(
platformContext.relative(entity.absolute.path, from: from.path),
),
);
5 changes: 2 additions & 3 deletions script/tool/lib/src/common/package_looping_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
import 'dart:async';

import 'package:file/file.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';

import 'core.dart';
import 'file_utils.dart';
import 'git_version_finder.dart';
import 'output_utils.dart';
import 'package_command.dart';
Expand Down Expand Up @@ -238,8 +238,7 @@ abstract class PackageLoopingCommand extends PackageCommand {
String getRelativePosixPath(
FileSystemEntity entity, {
required Directory from,
}) =>
p.posix.joinAll(path.split(path.relative(entity.path, from: from.path)));
}) => relativePosixPath(entity, from: from, platformContext: path);

/// The suggested indentation for printed output.
String get indentation => hasLongOutput ? '' : ' ';
Expand Down
108 changes: 13 additions & 95 deletions script/tool/lib/src/dependabot_check_command.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,20 @@
// found in the LICENSE file.

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

import 'common/output_utils.dart';
import 'common/package_looping_command.dart';
import 'common/repository_package.dart';
import 'validators/dependabot_validator.dart';

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

static const String _configPathFlag = 'config';
DependabotCheckCommand(super.packagesDir, {super.gitDir});

late Directory _repoRoot;

// The set of directories covered by "gradle" entries in the config.
Set<String> _gradleDirs = const <String>{};
// The set of directories covered by the repo's Dependabot configuration.
late DependabotCoverage _coverage;

@override
final String name = 'dependabot-check';
Expand All @@ -48,99 +39,26 @@ class DependabotCheckCommand extends PackageLoopingCommand {
Future<void> initializeRun() async {
_repoRoot = packagesDir.fileSystem.directory((await gitDir).path);

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

const typeKey = 'package-ecosystem';
const dirKey = 'directory';
const dirsKey = 'directories';
final Iterable<YamlMap> gradleEntries = entries.cast<YamlMap>().where(
(YamlMap entry) => entry[typeKey] == 'gradle',
);
final Iterable<String?> directoryEntries = gradleEntries.map(
(YamlMap entry) => entry[dirKey] as String?,
);
final Iterable<String?> directoriesEntries = gradleEntries
.map((YamlMap entry) => entry[dirsKey] as YamlList?)
.expand((YamlList? list) => list?.nodes ?? <String>[])
.cast<YamlScalar>()
.map((YamlScalar entry) => entry.value as String);
_gradleDirs = directoryEntries
.followedBy(directoriesEntries)
.whereType<String>()
.toSet();
_coverage = DependabotValidator.loadConfig(repoRoot: _repoRoot);
}

@override
Future<PackageResult> runForPackage(RepositoryPackage package) async {
var skipped = true;
final validator = DependabotValidator(
coverage: _coverage,
path: path,
repoRoot: _repoRoot,
indentation: indentation,
);

final errors = <String>[];

final _GradleCoverageResult gradleResult =
_validateDependabotGradleCoverage(package);
skipped = skipped && gradleResult.runState == RunState.skipped;
if (gradleResult.runState == RunState.failed) {
printError('${indentation}Missing Gradle coverage.');
print(
'${indentation}Add a "gradle" entry to '
'${getStringArg(_configPathFlag)} for ${gradleResult.missingPath}',
);
errors.add('Missing Gradle coverage');
}
errors.addAll(validator.validateDependabotCoverage(package));

// 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.
_GradleCoverageResult _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 dependabotPath =
'/${getRelativePosixPath(appDir, from: _repoRoot)}';
return _gradleDirs.contains(dependabotPath)
? _GradleCoverageResult(RunState.succeeded)
: _GradleCoverageResult(RunState.failed, missingPath: dependabotPath);
} else if (androidDir.existsSync()) {
// It's a library, so only check for the android directory to be covered.
final dependabotPath =
'/${getRelativePosixPath(androidDir, from: _repoRoot)}';
return _gradleDirs.contains(dependabotPath)
? _GradleCoverageResult(RunState.succeeded)
: _GradleCoverageResult(RunState.failed, missingPath: dependabotPath);
}
return _GradleCoverageResult(RunState.skipped);
}
}

class _GradleCoverageResult {
_GradleCoverageResult(this.runState, {this.missingPath});

final RunState runState;
final String? missingPath;
}
Loading
Loading