blob: 6e57f909522e6353458bfe6d4db4e5ecca3bae3f [file] [log] [blame]
// ignore_for_file: public_member_api_docs, sort_constructors_first
// Copyright (c) 2023, 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 'dart:convert';
import 'dart:io';
import 'package:http/http.dart' as http;
import 'package:path/path.dart' as path;
import 'repo.dart';
// TODO:(devoncarew): Consider replacing some of this class with package:github.
class Github {
static Map<String, String> get _env => Platform.environment;
/// When true, details of any RPC error are printed to the console.
final bool verbose;
http.Client? _httpClient;
Github({this.verbose = false});
String? get githubAuthToken => _env['GITHUB_TOKEN'];
/// The owner and repository name. For example, `octocat/Hello-World`.
String? get repoSlug => _env['GITHUB_REPOSITORY'];
/// The PR (or issue) number.
String? get issueNumber => _env['ISSUE_NUMBER'];
/// Any labels applied to this PR.
List<String> get prLabels =>
_env.containsKey('PR_LABELS') ? _env['PR_LABELS']!.split(',') : [];
/// The commit SHA that triggered the workflow.
String? get sha => _env['GITHUB_SHA'];
/// The name of the person or app that initiated the workflow.
String? get actor => _env['GITHUB_ACTOR'];
/// Whether we're running withing the context of a GitHub action.
bool get inGithubContext => _env['GITHUB_ACTIONS'] != null;
/// The short ref name of the branch or tag that triggered the workflow run.
/// This value matches the branch or tag name shown on GitHub. For example,
/// `feature-branch-1`.
String? get refName => _env['GITHUB_REF_NAME'];
/// The ref name of the base where the PR branched off of.
String? get baseRef => _env['base_ref'];
http.Client get httpClient => _httpClient ??= http.Client();
/// Write the given [markdownSummary] content to the GitHub
/// `GITHUB_STEP_SUMMARY` file. This will cause the markdown output to be
/// appended to the GitHub job summary for the current PR.
///
/// See also:
/// https://docs.github.com/en/actions/learn-github-actions/variables.
void appendStepSummary(String markdownSummary) {
var summaryPath = _env['GITHUB_STEP_SUMMARY'];
if (summaryPath == null) {
stderr.writeln("'GITHUB_STEP_SUMMARY' doesn't exist.");
return;
}
var file = File(summaryPath);
file.writeAsStringSync('${markdownSummary.trimRight()}\n\n',
mode: FileMode.append);
}
Future<String> callRestApiGet(Uri uri) async {
var token = githubAuthToken!;
return httpClient.get(uri, headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github+json',
}).then((response) {
return response.statusCode != 200
? throw RpcException(response.reasonPhrase!)
: response.body;
});
}
Future<String> callRestApiPost(Uri uri, String body) async {
var token = githubAuthToken!;
return httpClient
.post(uri,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github+json',
},
body: body)
.then((response) {
if (response.statusCode != 201) {
if (verbose) {
stderr.writeln('${response.statusCode} ${response.reasonPhrase}');
for (var entry in response.headers.entries) {
stderr.writeln('${entry.key}: ${entry.value}');
}
stderr.writeln(response.body);
}
throw RpcException(response.reasonPhrase!);
}
return response.body;
});
}
Future<String> callRestApiPatch(Uri uri, String body) async {
var token = githubAuthToken!;
return httpClient
.patch(uri,
headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github+json',
},
body: body)
.then((response) {
return response.statusCode != 200
? throw RpcException(response.reasonPhrase!)
: response.body;
});
}
Future<void> callRestApiDelete(Uri uri) async {
var token = githubAuthToken!;
return httpClient.delete(uri, headers: {
'Authorization': 'Bearer $token',
'Accept': 'application/vnd.github+json',
}).then((response) {
if (response.statusCode != 204) {
throw RpcException(response.reasonPhrase!);
}
});
}
/// Find a comment on the PR matching the given criteria ([user],
/// [searchTerm]). Return the issue ID if a matching comment is found or null
/// if there's no match.
Future<int?> findCommentId(
String repoSlug,
String issueNumber, {
required String user,
String? searchTerm,
}) async {
var result = await callRestApiGet(
Uri.parse('https://api.github.com/repos/$repoSlug/issues/$issueNumber/'
'comments?per_page=100'),
);
var items = jsonDecode(result) as List;
for (var item in items) {
item as Map;
var id = item['id'] as int;
var userLogin = (item['user'] as Map)['login'] as String;
var body = item['body'] as String;
if (userLogin != user) continue;
if (searchTerm != null && !body.contains(searchTerm)) continue;
return id;
}
return null;
}
Future<List<GitFile>> listFilesForPR() async {
var result = await callRestApiGet(
Uri.parse(
'https://api.github.com/repos/$repoSlug/pulls/$issueNumber/files'),
);
var json = jsonDecode(result) as List;
var files = json
.map((e) => e as Map<String, dynamic>)
.map((e) => GitFile(
e['filename'] as String,
FileStatus.values.firstWhere(
(element) => element.name == e['status'] as String,
),
))
.toList();
return files;
}
void close() {
_httpClient?.close();
}
/// Write a notice message to the github log.
void notice({required String message}) {
print('::notice ::$message');
}
}
class RpcException implements Exception {
final String message;
RpcException(this.message);
@override
String toString() => 'RpcException: $message';
}
class GitFile {
final String filename;
final FileStatus status;
bool isInPackage(Package package) {
print('Check if $relativePath is in ${package.directory.path}');
return path.isWithin(package.directory.path, relativePath);
}
GitFile(this.filename, this.status);
String get relativePath =>
path.relative(filename, from: Directory.current.path);
@override
String toString() => '$filename: $status';
}
enum FileStatus {
added,
removed,
modified,
renamed,
copied,
changed,
unchanged;
}