blob: 56f59e00a623f174cc9268494b68c7e2365d8963 [file] [log] [blame] [edit]
// Copyright 2023 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/repository_configuration.dart';
import 'package:auto_submit/service/config.dart';
import 'package:auto_submit/service/github_service.dart';
import 'package:auto_submit/service/log.dart';
import 'package:github/github.dart';
import 'package:mutex/mutex.dart';
import 'package:neat_cache/neat_cache.dart';
/// The [RepositoryConfigurationManager] is responsible for fetching and merging
/// the autosubmit configuration from the Org level repository and if needed
/// fetching the override configuration from the pull request repository.
///
/// It will attempt to access the cache first before repulling the configuraiton
/// from the repositories. This is currently set at a 10 minute TTL.
class RepositoryConfigurationManager {
RepositoryConfigurationManager(this.config, this.cache);
// Mutex protects the calls to cache while the [RepositoryConfiguration] is
// collected from github.
final Mutex _mutex = Mutex();
static const String fileSeparator = '/';
// This is the well named organization level repository and configuration file
// we will read before looking to see if there is a local file with
// overwrites.
static const String orgRepository = '.github';
static const String dirName = 'autosubmit';
static const String fileName = 'autosubmit.yml';
final Config config;
final Cache cache;
/// Read the configuration from the cache given the slug, if the config is not
/// in the cache then go and get it from the repository and store it in the
/// cache.
Future<RepositoryConfiguration> readRepositoryConfiguration(
RepositorySlug slug,
) async {
await _mutex.acquire();
try {
// Get the contents from the cache or go to github.
final cacheValue = await cache['${slug.fullName}$fileSeparator$fileName'].get(
() async => _getConfiguration(slug),
config.repositoryConfigurationTtl,
);
final String cacheYaml = String.fromCharCodes(cacheValue);
log.info('Converting yaml to RepositoryConfiguration: $cacheYaml');
return RepositoryConfiguration.fromYaml(cacheYaml);
} finally {
_mutex.release();
}
}
/// Collect the configuration from github and handle the cache conversion to
/// bytes.
Future<List<int>> _getConfiguration(
RepositorySlug slug,
) async {
// Read the org level configuraiton file first.
log.info('Getting org level configuration.');
// Get the Org level configuration.
final RepositorySlug orgSlug = RepositorySlug(slug.owner, orgRepository);
GithubService githubService = await config.createGithubService(orgSlug);
final String orgLevelConfig = await githubService.getFileContents(orgSlug, '$dirName$fileSeparator$fileName');
final RepositoryConfiguration globalRepositoryConfiguration = RepositoryConfiguration.fromYaml(orgLevelConfig);
// Collect the default branch if it was not supplied.
if (globalRepositoryConfiguration.defaultBranch == RepositoryConfiguration.defaultBranchStr) {
globalRepositoryConfiguration.defaultBranch = await githubService.getDefaultBranch(slug);
}
log.info('Default branch was found to be ${globalRepositoryConfiguration.defaultBranch} for ${slug.fullName}.');
// If the override flag is set to true we check the pull request's
// repository to collect any values that will override the global config.
if (globalRepositoryConfiguration.allowConfigOverride) {
log.info('Override is set, collecting and merging local repository configuration.');
githubService = await config.createGithubService(slug);
String? localRepositoryConfigurationYaml;
try {
localRepositoryConfigurationYaml = await githubService.getFileContents(slug, '$dirName$fileSeparator$fileName');
final RepositoryConfiguration localRepositoryConfiguration =
RepositoryConfiguration.fromYaml(localRepositoryConfigurationYaml);
final RepositoryConfiguration mergedRepositoryConfiguration = mergeConfigurations(
globalRepositoryConfiguration,
localRepositoryConfiguration,
);
return mergedRepositoryConfiguration.toString().codeUnits;
} on GitHubError {
log.warning(
'Configuration override was set but no local repository configuration file was found in ${slug.fullName}, using global configuration.',
);
}
}
return globalRepositoryConfiguration.toString().codeUnits;
}
/// Merge the local [RepositoryConfiguration] with the global
/// [RepositoryConfiguration].
///
/// Values that are lists are additive. Values that are not lists overwrite
/// the value in the global configuration.
///
/// The number of approving reviews in the local configuration cannot override
/// the global configuration if it is a lower value.
///
/// We also do not need to allow the default branch override as it is
/// collected from the repository directly.
RepositoryConfiguration mergeConfigurations(
RepositoryConfiguration globalConfiguration,
RepositoryConfiguration localConfiguration,
) {
final RepositoryConfiguration mergedRepositoryConfiguration = RepositoryConfiguration(
allowConfigOverride: globalConfiguration.allowConfigOverride,
defaultBranch: globalConfiguration.defaultBranch,
autoApprovalAccounts: globalConfiguration.autoApprovalAccounts,
approvingReviews: globalConfiguration.approvingReviews,
approvalGroup: globalConfiguration.approvalGroup,
runCi: globalConfiguration.runCi,
supportNoReviewReverts: globalConfiguration.supportNoReviewReverts,
requiredCheckRunsOnRevert: globalConfiguration.requiredCheckRunsOnRevert,
);
// auto approval accounts, they should be empty if nothing was defined
if (localConfiguration.autoApprovalAccounts.isNotEmpty) {
mergedRepositoryConfiguration.autoApprovalAccounts.addAll(localConfiguration.autoApprovalAccounts);
}
// approving reviews
// this may not be set lower than the global configuration value
final int localApprovingReviews = localConfiguration.approvingReviews;
if (localApprovingReviews > globalConfiguration.approvingReviews) {
mergedRepositoryConfiguration.approvingReviews = localApprovingReviews;
}
// approval group
final String localApprovalGroup = localConfiguration.approvalGroup;
if (localApprovalGroup.isNotEmpty) {
mergedRepositoryConfiguration.approvalGroup = localApprovalGroup;
}
// run ci
// validates the checks runs
final bool localRunCi = localConfiguration.runCi;
if (globalConfiguration.runCi != localRunCi) {
mergedRepositoryConfiguration.runCi = localRunCi;
}
// support no revert reviews - this will be a moot point after revert is updated
final bool localSupportNoReviewReverts = localConfiguration.supportNoReviewReverts;
if (localSupportNoReviewReverts != globalConfiguration.supportNoReviewReverts) {
mergedRepositoryConfiguration.supportNoReviewReverts = localSupportNoReviewReverts;
}
// required checkruns on revert, they should be empty if nothing was defined
if (localConfiguration.requiredCheckRunsOnRevert.isNotEmpty) {
mergedRepositoryConfiguration.requiredCheckRunsOnRevert.addAll(localConfiguration.requiredCheckRunsOnRevert);
}
return mergedRepositoryConfiguration;
}
}