blob: c227635bfbee171bc2aceefa7c0da97bcdaf3477 [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:cocoon_server/logging.dart';
import 'package:github/github.dart';
import 'package:mutex/mutex.dart';
import 'package:neat_cache/neat_cache.dart';
import '../service/config.dart';
import 'repository_configuration.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 cacheYaml = String.fromCharCodes(cacheValue as Iterable<int>);
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 orgSlug = RepositorySlug(slug.owner, orgRepository);
var githubService = await config.createGithubService(orgSlug);
final orgLevelConfig = await githubService.getFileContents(
orgSlug,
'$dirName$fileSeparator$fileName',
);
final 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 localRepositoryConfiguration = RepositoryConfiguration.fromYaml(
localRepositoryConfigurationYaml,
);
final mergedRepositoryConfiguration = mergeConfigurations(
globalRepositoryConfiguration,
localRepositoryConfiguration,
);
return mergedRepositoryConfiguration.toString().codeUnits;
} on GitHubError catch (e) {
log.warn(
'Configuration override was set but no local repository '
'configuration file was found in ${slug.fullName}, using global '
'configuration.',
e,
);
}
}
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 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 localApprovingReviews = localConfiguration.approvingReviews;
if (localApprovingReviews > globalConfiguration.approvingReviews) {
mergedRepositoryConfiguration.approvingReviews = localApprovingReviews;
}
// approval group
final localApprovalGroup = localConfiguration.approvalGroup;
if (localApprovalGroup.isNotEmpty) {
mergedRepositoryConfiguration.approvalGroup = localApprovalGroup;
}
// run ci
// validates the checks runs
final 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 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;
}
}