Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First working implementation #9

Merged
merged 8 commits into from Sep 21, 2022
Merged
6 changes: 6 additions & 0 deletions .github/workflows/build.yml
Expand Up @@ -49,6 +49,12 @@ jobs:
# refresh cache every month to avoid unlimited growth
key: maven-repo-pr-${{ runner.os }}-${{ steps.get-date.outputs.date }}

- name: Check out yrodiere:github-api i1082-listRepositories
run: git clone -b i1082-listRepositories https://github.com/yrodiere/github-api.git

- name: Build github-api SNAPSHOT
run: cd github-api && mvn -B clean install -DskipTests && cd -

- name: Validate formatting
run: mvn -B clean formatter:validate

Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Expand Up @@ -51,6 +51,11 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.kohsuke</groupId>
<artifactId>github-api</artifactId>
<version>1.309-SNAPSHOT</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
Expand All @@ -71,10 +76,16 @@
<artifactId>quarkus-github-app</artifactId>
<version>${quarkus-github-app.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.githubapp</groupId>
<artifactId>quarkus-github-app-command-airline</artifactId>
<version>${quarkus-github-app.version}</version>
</dependency>
<dependency>
<groupId>io.quarkiverse.githubapp</groupId>
<artifactId>quarkus-github-app-testing</artifactId>
<version>${quarkus-github-app.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<!-- To deserialize Optional -->
Expand Down
30 changes: 30 additions & 0 deletions src/main/java/io/quarkus/github/lottery/LotteryCli.java
@@ -0,0 +1,30 @@
package io.quarkus.github.lottery;

import java.io.IOException;

import org.kohsuke.github.GHPermissionType;

import com.github.rvesse.airline.annotations.Cli;
import com.github.rvesse.airline.annotations.Command;

import io.quarkiverse.githubapp.command.airline.Permission;
import io.quarkus.arc.Arc;

@Cli(name = "/lottery", commands = { LotteryCli.DrawCommand.class })
public class LotteryCli {

interface Commands {
void run() throws IOException;
}

@Command(name = "draw")
@Permission(GHPermissionType.ADMIN)
static class DrawCommand implements Commands {
@Override
public void run() throws IOException {
// Cannot inject the service for some reason,
// as Airline uses reflection and performs calls to setAccessible recursively.
Arc.container().instance(LotteryService.class).get().draw();
}
}
}
4 changes: 2 additions & 2 deletions src/main/java/io/quarkus/github/lottery/LotteryService.java
Expand Up @@ -47,8 +47,8 @@ public class LotteryService {
/**
* Draws the lottery and sends lists of tickets to participants as necessary.
*/
@Scheduled(cron = "0 0 * ? * *") // Every hour
public void draw() throws IOException {
@Scheduled(every = "1H", concurrentExecution = Scheduled.ConcurrentExecution.SKIP) // Every hour
public synchronized void draw() throws IOException {
Log.info("Starting draw...");
List<GitHubRepositoryRef> refs = gitHubService.listRepositories();
Log.infof("Will draw for the following repositories: %s", refs);
Expand Down
Expand Up @@ -16,7 +16,7 @@ public record LotteryConfig(
@JsonProperty(required = true) BucketsConfig buckets,
List<ParticipantConfig> participants) {

public static final String FILE_NAME = "quarkus-github-lottery.yaml";
public static final String FILE_NAME = "quarkus-github-lottery.yml";

public record BucketsConfig(
@JsonProperty(required = true) TriageBucketConfig triage) {
Expand Down
@@ -0,0 +1,10 @@
package io.quarkus.github.lottery.github;

/**
* A reference to a GitHub application installation.
*
* @param appName The application name.
* @param installationId The installation ID.
*/
public record GitHubInstallationRef(String appName, long installationId) {
}
Expand Up @@ -61,13 +61,13 @@ public GitHubRepositoryRef ref() {
return ref;
}

public String selfUsername() throws IOException {
return client().getMyself().getLogin();
public String selfLogin() {
return ref.installationRef().appName() + "[bot]";
}

private GitHub client() {
if (client == null) {
client = clientProvider.getInstallationClient(ref.installationId());
client = clientProvider.getInstallationClient(ref.installationRef().installationId());
}
return client;
}
Expand All @@ -81,7 +81,7 @@ private GHRepository repository() throws IOException {

private DynamicGraphQLClient graphQLClient() {
if (graphQLClient == null) {
graphQLClient = clientProvider.getInstallationGraphQLClient(ref.installationId());
graphQLClient = clientProvider.getInstallationGraphQLClient(ref.installationRef().installationId());
}
return graphQLClient;
}
Expand Down Expand Up @@ -120,9 +120,9 @@ public void commentOnDedicatedIssue(String username, String topic, String markdo
issue.comment(markdownBody);
}

public Stream<String> extractCommentsFromDedicatedIssue(String username, String topic, Instant since)
public Stream<String> extractCommentsFromDedicatedIssue(String login, String topic, Instant since)
throws IOException {
return getDedicatedIssue(username, topic)
return getDedicatedIssue(login, topic)
.map(uncheckedIO(issue -> getAppCommentsSince(issue, since)))
.orElse(Stream.of())
.map(GHIssueComment::getBody);
Expand All @@ -145,27 +145,27 @@ private GHIssue createDedicatedIssue(String username, String topic) throws IOExc
}

private Optional<GHIssueComment> getLastAppComment(GHIssue issue) throws IOException {
long selfId = client().getMyself().getId();
String selfLogin = selfLogin();
// TODO ideally we'd use the "since" API parameter to ignore
// older comments (e.g. 1+ year old) that are unlikely to be relevant
// (see 'since' in https://docs.github.com/en/rest/issues/comments#list-issue-comments)
// but that's not supported yet in the library we're using...
GHIssueComment lastNotificationComment = null;
for (GHIssueComment comment : issue.listComments()) {
if (selfId == comment.getUser().getId()) {
if (selfLogin.equals(comment.getUser().getLogin())) {
lastNotificationComment = comment;
}
}
return Optional.ofNullable(lastNotificationComment);
}

private Stream<GHIssueComment> getAppCommentsSince(GHIssue issue, Instant since) throws IOException {
long selfId = client().getMyself().getId();
String selfLogin = selfLogin();
// TODO ideally we'd use the "since" API parameter to ignore older comments
// (see 'since' in https://docs.github.com/en/rest/issues/comments#list-issue-comments)
// but that's not supported yet in the library we're using...
return toStream(issue.listComments())
.filter(uncheckedIO((GHIssueComment comment) -> selfId == comment.getUser().getId()
.filter(uncheckedIO((GHIssueComment comment) -> selfLogin.equals(comment.getUser().getLogin())
&& !comment.getCreatedAt().toInstant().isBefore(since))::apply);
}

Expand Down
Expand Up @@ -3,13 +3,13 @@
/**
* A reference to a GitHub repository as viewed from a GitHub App installation.
*
* @param installationId The installation ID.
* @param installationRef A reference to the GitHub installation.
* @param repositoryName The full name of the GitHub repository.
*/
public record GitHubRepositoryRef(long installationId, String repositoryName) {
public record GitHubRepositoryRef(GitHubInstallationRef installationRef, String repositoryName) {

@Override
public String toString() {
return repositoryName + "(through installation " + installationId + ")";
return repositoryName + "(through " + installationRef + ")";
}
}
Expand Up @@ -8,6 +8,7 @@

import io.quarkiverse.githubapp.GitHubClientProvider;
import io.quarkiverse.githubapp.GitHubConfigFileProvider;
import org.kohsuke.github.GHApp;
import org.kohsuke.github.GHAppInstallation;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;
Expand All @@ -23,9 +24,14 @@ public class GitHubService {
public List<GitHubRepositoryRef> listRepositories() throws IOException {
List<GitHubRepositoryRef> result = new ArrayList<>();
GitHub client = clientProvider.getApplicationClient();
for (GHAppInstallation installation : client.getApp().listInstallations()) {
for (GHRepository repository : installation.listRepositories()) {
result.add(new GitHubRepositoryRef(installation.getId(), repository.getFullName()));
GHApp app = client.getApp();
String appName = app.getName();
for (GHAppInstallation installation : app.listInstallations()) {
long installationId = installation.getId();
var installationRef = new GitHubInstallationRef(appName, installationId);
for (GHRepository repository : clientProvider.getInstallationClient(installationId)
.getInstallation().listRepositories()) {
result.add(new GitHubRepositoryRef(installationRef, repository.getFullName()));
}
}
return result;
Expand Down
Expand Up @@ -28,7 +28,7 @@ public LotteryHistory fetch(DrawRef drawRef, LotteryConfig config) throws IOExce
var persistenceRepo = persistenceRepo(drawRef, config);
var history = new LotteryHistory(drawRef.instant(), config.buckets());
String historyTopic = messageFormatter.formatHistoryTopicText(drawRef);
persistenceRepo.extractCommentsFromDedicatedIssue(persistenceRepo.selfUsername(), historyTopic, history.since())
persistenceRepo.extractCommentsFromDedicatedIssue(persistenceRepo.selfLogin(), historyTopic, history.since())
.flatMap(uncheckedIO(message -> messageFormatter.extractPayloadFromHistoryBodyMarkdown(message).stream()))
.forEach(history::add);
return history;
Expand All @@ -38,7 +38,7 @@ public void append(DrawRef drawRef, LotteryConfig config, List<LotteryReport.Ser
var persistenceRepo = persistenceRepo(drawRef, config);
String historyTopic = messageFormatter.formatHistoryTopicText(drawRef);
String commentBody = messageFormatter.formatHistoryBodyMarkdown(drawRef, reports);
persistenceRepo.commentOnDedicatedIssue(persistenceRepo.selfUsername(), historyTopic, commentBody);
persistenceRepo.commentOnDedicatedIssue(persistenceRepo.selfLogin(), historyTopic, commentBody);
}

GitHubRepository persistenceRepo(DrawRef drawRef, LotteryConfig config) {
Expand Down
Expand Up @@ -60,23 +60,24 @@ public String formatHistoryBodyMarkdown(DrawRef drawRef, List<LotteryReport.Seri
throws JsonProcessingException {
// TODO produce better output, maybe with Qute templates?
return "Here are the reports for " + drawRef.repositoryRef().repositoryName() + " on " + drawRef.instant() + ".\n\n"
+ reports.stream().map(this::formatHistoryBodyReport)
+ reports.stream().map(report -> this.formatHistoryBodyReport(drawRef, report))
.collect(Collectors.joining("\n"))
+ "\n" + PAYLOAD_BEGIN + jsonObjectMapper.writeValueAsString(reports) + PAYLOAD_END;
}

private String formatHistoryBodyReport(LotteryReport.Serialized report) {
private String formatHistoryBodyReport(DrawRef drawRef, LotteryReport.Serialized report) {
StringBuilder builder = new StringBuilder("# ").append(report.username()).append('\n');
builder.append(formatHistoryBodyBucket("Triage", report.triage()));
builder.append(formatHistoryBodyBucket(drawRef, "Triage", report.triage()));
return builder.toString();
}

private String formatHistoryBodyBucket(String title, LotteryReport.Bucket.Serialized bucket) {
private String formatHistoryBodyBucket(DrawRef drawRef, String title, LotteryReport.Bucket.Serialized bucket) {
String repoName = drawRef.repositoryRef().repositoryName();
StringBuilder builder = new StringBuilder("## ").append(title).append('\n');
var issueNumbers = bucket.issueNumbers();
if (!issueNumbers.isEmpty()) {
builder.append(issueNumbers.stream()
.map(issueId -> "#" + issueId)
.map(issueId -> repoName + "#" + issueId)
.collect(MARKDOWN_BULLET_LIST_COLLECTOR));
}
return builder.toString();
Expand Down
Expand Up @@ -15,7 +15,7 @@ public class NotificationService {

public static GitHubRepository notificationRepository(GitHubService gitHubService, DrawRef drawRef,
LotteryConfig.NotificationsConfig config) {
return gitHubService.repository(new GitHubRepositoryRef(drawRef.repositoryRef().installationId(),
return gitHubService.repository(new GitHubRepositoryRef(drawRef.repositoryRef().installationRef(),
config.createIssues().repository()));
}

Expand Down