blob: b70152f5aa8410cf694bf7d099bb4cb355ccadeb [file] [log] [blame]
// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
// for details. 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:collection/collection.dart';
import 'package:github/github.dart';
import 'package:pub_semver/pub_semver.dart';
import 'action_version.dart';
import 'utils.dart';
class GitHubActionResolver {
GitHubActionResolver({required GitHub github}) : _github = github;
final GitHub _github;
final _tagCache = <String, List<Tag>>{};
final _branchCache = <String, List<Branch>>{};
Future<ActionVersionResolution> resolve(ActionVersion version) async {
if (version.path != null) {
throw ArgumentError.value(
version,
'version',
'path is not supported - yet - $version',
);
}
final slug = RepositorySlug(version.org, version.repo);
final result = await _getTags(slug);
final matches = result
.where(
(element) =>
element.name == version.version ||
element.commit.sha == version.version,
)
.toList();
if (matches.isEmpty) {
// Look for branches?
final branches = _branchCache[slug.fullName] ??=
await _github.repositories.listBranches(slug).toList();
final matchingBranches = branches
.where(
(element) =>
element.name == version.version && element.commit?.sha != null,
)
.toList();
if (matchingBranches.length == 1) {
return ActionVersionResolution._fromBranch(matchingBranches.single);
}
// TODO: if it was a SHA thing coming in, return null?
throw Exception('No matches found for $version');
}
if (matches.length > 1) {
final mismatches = matches
.where((element) => element.commit.sha != matches.first.commit.sha)
.toList();
if (mismatches.isNotEmpty) {
throw Exception('This should never happen!');
}
}
final sha = matches.first.commit.sha;
final shaMatches =
result.where((element) => element.commit.sha == sha).toList();
// We want to return the "best" tag. If there is only one match, then fine!
if (shaMatches.length == 1) {
return ActionVersionResolution._fromTag(shaMatches.single);
}
if (shaMatches.length > 1) {
final validVersions = shaMatches
.where((element) => _semVerExpando[element] != null)
.toList();
if (validVersions.length == 1) {
return ActionVersionResolution._fromTag(validVersions.single);
}
}
throw Exception('Too many matches for $version - $shaMatches');
}
Future<ActionVersionResolution> latestStable(String repoOrg) async {
final slug = RepositorySlug.full(repoOrg);
final tags = await _getTags(slug);
final versionTags =
tags.where((element) => _semVerExpando[element] != null).toList();
final grouped = groupBy(versionTags, (p0) => _semVerExpando[p0]!);
final allVersions = grouped.keys.toList();
final latestVersion = latestStableVersion(allVersions);
if (latestVersion == null) {
throw Exception('Could not figure out "latest" for $repoOrg');
}
final latestTags = grouped[latestVersion]!;
if (latestTags.length == 1) {
return ActionVersionResolution._fromTag(latestTags.single);
}
final prunedTags = latestTags
.where((element) => element.name == 'v${_semVerExpando[element]}')
.toList();
if (prunedTags.length == 1) {
return ActionVersionResolution._fromTag(prunedTags.single);
}
throw UnimplementedError(
'We do not have one tag for $repoOrg: $latestTags',
);
}
Future<List<Tag>> _getTags(RepositorySlug slug) async =>
_tagCache[slug.fullName] ??=
_process(await _github.repositories.listTags(slug).toList());
void close() {
_github.client.close();
}
List<Tag> _process(List<Tag> tags) {
for (var tag in tags) {
var name = tag.name;
if (name.startsWith('v')) {
// Handle the case (with Dart and melos)
// where we only do vX.Y or vX instead of vX.Y.Z
// TODO(kevmoo): remove this silly once Dart fixes it's tags
while ('.'.allMatches(name).length < 2) {
name = '$name.0';
}
try {
final ver = Version.parse(name.substring(1));
_semVerExpando[tag] = ver;
} on FormatException {
// not a version - skipping
}
}
}
return tags;
}
}
class ActionVersionResolution {
ActionVersionResolution.version({required this.sha, required this.version})
: branch = null;
ActionVersionResolution.branch({required this.sha, required this.branch})
: version = null;
factory ActionVersionResolution._fromTag(Tag tag) =>
ActionVersionResolution.version(
sha: tag.commit.sha!,
version: _semVerExpando[tag]!,
);
factory ActionVersionResolution._fromBranch(Branch branch) =>
ActionVersionResolution.branch(
sha: branch.commit!.sha!,
branch: branch.name!,
);
final String sha;
final Version? version;
final String? branch;
@override
String toString() => '${version ?? branch}::$sha';
}
final _semVerExpando = Expando<Version>();