Skip to content

Commit

Permalink
Merge pull request #194 from yrodiere/i182
Browse files Browse the repository at this point in the history
Use the "search issues" API instead of the "query issues" API
  • Loading branch information
yrodiere authored Dec 10, 2024
2 parents ff61cce + 2ce4575 commit c457711
Show file tree
Hide file tree
Showing 10 changed files with 596 additions and 422 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,8 @@ public record Maintenance(
// TODO default to all labels configured for this user in .github/quarkus-bot.yml
@JsonProperty(required = true) List<String> labels,
@JsonProperty(required = true) @JsonDeserialize(as = TreeSet.class) Set<DayOfWeek> days,
Feedback feedback,
@JsonProperty(required = true) Participation stale) {
Optional<Feedback> feedback,
@JsonProperty Optional<Participation> stale) {
public record Feedback(
@JsonProperty(required = true) Participation needed,
@JsonProperty(required = true) Participation provided) {
Expand Down
9 changes: 6 additions & 3 deletions src/main/java/io/quarkus/github/lottery/draw/Participant.java
Original file line number Diff line number Diff line change
Expand Up @@ -88,9 +88,12 @@ public LotteryReport report() {

private static final class Maintenance {
public static Optional<Maintenance> create(String username, LotteryConfig.Participant.Maintenance config) {
var feedbackNeeded = Participation.create(username, config.feedback().needed());
var feedbackProvided = Participation.create(username, config.feedback().provided());
var stale = Participation.create(username, config.stale());
var feedbackNeeded = config.feedback().map(LotteryConfig.Participant.Maintenance.Feedback::needed)
.flatMap(p -> Participation.create(username, p));
var feedbackProvided = config.feedback().map(LotteryConfig.Participant.Maintenance.Feedback::provided)
.flatMap(p -> Participation.create(username, p));
var stale = config.stale()
.flatMap(p -> Participation.create(username, p));

if (feedbackNeeded.isEmpty() && feedbackProvided.isEmpty() && stale.isEmpty()) {
return Optional.empty();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package io.quarkus.github.lottery.github;

import io.quarkus.github.lottery.util.GitHubConstants;

/**
* A reference to a GitHub application installation.
*
Expand All @@ -9,7 +11,7 @@
public record GitHubInstallationRef(String appSlug, long installationId) {

public String appLogin() {
return appSlug() + "[bot]";
return appSlug() + GitHubConstants.BOT_LOGIN_SUFFIX;
}

}
126 changes: 70 additions & 56 deletions src/main/java/io/quarkus/github/lottery/github/GitHubRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,18 @@
import org.kohsuke.github.GHIssueComment;
import org.kohsuke.github.GHIssueCommentQueryBuilder;
import org.kohsuke.github.GHIssueEvent;
import org.kohsuke.github.GHIssueQueryBuilder;
import org.kohsuke.github.GHIssueSearchBuilder;
import org.kohsuke.github.GHIssueState;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GHUser;
import org.kohsuke.github.GitHub;

import io.quarkiverse.githubapp.ConfigFile;
import io.quarkiverse.githubapp.GitHubClientProvider;
import io.quarkiverse.githubapp.GitHubConfigFileProvider;
import io.quarkus.github.lottery.config.LotteryConfig;
import io.quarkus.github.lottery.message.MessageFormatter;
import io.quarkus.github.lottery.util.GitHubConstants;
import io.quarkus.github.lottery.util.Streams;
import io.quarkus.logging.Log;
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
Expand Down Expand Up @@ -92,6 +94,12 @@ private GHRepository repository() throws IOException {
return repository;
}

private GHIssueSearchBuilder searchIssues() {
return client().searchIssues()
.q(GitHubSearchClauses.repo(ref))
.q(GitHubSearchClauses.isIssue());
}

private DynamicGraphQLClient graphQLClient() {
if (graphQLClient == null) {
graphQLClient = clientProvider.getInstallationGraphQLClient(ref.installationRef().installationId());
Expand All @@ -109,17 +117,15 @@ public Optional<LotteryConfig> fetchLotteryConfig() throws IOException {
*
* @param updatedBefore An instant; all returned issues must have been last updated before that instant.
* @return A lazily populated stream of matching issues.
* @throws IOException In case of I/O failure.
* @throws java.io.UncheckedIOException In case of I/O failure.
*/
public Stream<Issue> issuesLastUpdatedBefore(Instant updatedBefore) throws IOException {
return toStream(repository().queryIssues()
.state(GHIssueState.OPEN)
.sort(GHIssueQueryBuilder.Sort.UPDATED)
.direction(GHDirection.DESC)
public Stream<Issue> issuesLastUpdatedBefore(Instant updatedBefore) {
return toStream(searchIssues()
.isOpen()
.q(GitHubSearchClauses.updatedBefore(updatedBefore))
.sort(GHIssueSearchBuilder.Sort.UPDATED)
.order(GHDirection.DESC)
.list())
.filter(notPullRequest())
.filter(updatedBefore(updatedBefore))
.map(toIssueRecord());
}

Expand All @@ -129,17 +135,16 @@ public Stream<Issue> issuesLastUpdatedBefore(Instant updatedBefore) throws IOExc
* @param label A GitHub label; if non-null, all returned issues must have been assigned that label.
* @param updatedBefore An instant; all returned issues must have been last updated before that instant.
* @return A lazily populated stream of matching issues.
* @throws IOException In case of I/O failure.
* @throws java.io.UncheckedIOException In case of I/O failure.
*/
public Stream<Issue> issuesWithLabelLastUpdatedBefore(String label, Instant updatedBefore) throws IOException {
return toStream(repository().queryIssues().label(label)
.state(GHIssueState.OPEN)
.sort(GHIssueQueryBuilder.Sort.UPDATED)
.direction(GHDirection.DESC)
public Stream<Issue> issuesWithLabelLastUpdatedBefore(String label, Instant updatedBefore) {
return toStream(searchIssues()
.isOpen()
.q(GitHubSearchClauses.label(label))
.q(GitHubSearchClauses.updatedBefore(updatedBefore))
.sort(GHIssueSearchBuilder.Sort.UPDATED)
.order(GHDirection.DESC)
.list())
.filter(notPullRequest())
.filter(updatedBefore(updatedBefore))
.map(toIssueRecord());
}

Expand All @@ -153,35 +158,21 @@ public Stream<Issue> issuesWithLabelLastUpdatedBefore(String label, Instant upda
* This label is not relevant to determining the last action.
* @param updatedBefore An instant; all returned issues must have been last updated before that instant.
* @return A lazily populated stream of matching issues.
* @throws IOException In case of I/O failure.
* @throws java.io.UncheckedIOException In case of I/O failure.
*/
public Stream<Issue> issuesLastActedOnByAndLastUpdatedBefore(Set<String> initialActionLabels, String filterLabel,
IssueActionSide lastActionSide, Instant updatedBefore) throws IOException {
var theRepository = repository();
var streams = initialActionLabels.stream()
.map(initialActionLabel -> toStream(theRepository.queryIssues()
.label(initialActionLabel)
.label(filterLabel)
.state(GHIssueState.OPEN)
.sort(GHIssueQueryBuilder.Sort.UPDATED)
.direction(GHDirection.DESC)
.list())
.filter(notPullRequest())
.filter(updatedBefore(updatedBefore))
.filter(uncheckedIO((GHIssue ghIssue) -> lastActionSide
.equals(lastActionSide(ghIssue, initialActionLabels)))::apply)
.map(toIssueRecord()))
.toList();
return Streams.interleave(streams);
}

private Predicate<GHIssue> updatedBefore(Instant updatedBefore) {
return uncheckedIO((GHIssue ghIssue) -> ghIssue.getUpdatedAt().toInstant().isBefore(updatedBefore))::apply;
}

private Predicate<GHIssue> notPullRequest() {
return (GHIssue ghIssue) -> !ghIssue.isPullRequest();
IssueActionSide lastActionSide, Instant updatedBefore) {
return toStream(searchIssues()
.isOpen()
.q(GitHubSearchClauses.anyLabel(initialActionLabels))
.q(GitHubSearchClauses.label(filterLabel))
.q(GitHubSearchClauses.updatedBefore(updatedBefore))
.sort(GHIssueSearchBuilder.Sort.UPDATED)
.order(GHDirection.DESC)
.list())
.filter(uncheckedIO((GHIssue ghIssue) -> lastActionSide
.equals(lastActionSide(ghIssue, initialActionLabels)))::apply)
.map(toIssueRecord());
}

private IssueActionSide lastActionSide(GHIssue ghIssue, Set<String> initialActionLabels) throws IOException {
Expand All @@ -194,19 +185,27 @@ private IssueActionSide lastActionSide(GHIssue ghIssue, Set<String> initialActio
lastEventActionSideInstant = event.getCreatedAt().toInstant();
}
}
GHIssueCommentQueryBuilder queryCommentsBuilder = ghIssue.queryComments();
if (lastEventActionSideInstant != null) {
queryCommentsBuilder.since(Date.from(lastEventActionSideInstant));
}

Optional<GHIssueComment> lastComment = toStream(queryCommentsBuilder.list()).reduce(Streams.last());
Optional<GHIssueComment> lastComment = getNonBotCommentsSince(ghIssue, lastEventActionSideInstant)
.reduce(Streams.last());
if (lastComment.isEmpty()) {
// No action since the label was assigned.
return IssueActionSide.TEAM;
}
return switch (repository().getPermission(lastComment.get().getUser())) {
case ADMIN, WRITE -> IssueActionSide.TEAM;
case READ, UNKNOWN, NONE -> IssueActionSide.OUTSIDER;
return getIssueActionSide(ghIssue, lastComment.get().getUser());
}

private IssueActionSide getIssueActionSide(GHIssue issue, GHUser user) throws IOException {
if (issue.getUser().getLogin().equals(user.getLogin())) {
// This is the reporter; even if part of the team,
// we'll consider he's acting as an outsider here,
// because he's unlikely to ask for feedback from himself.
return IssueActionSide.OUTSIDER;
}

return switch (repository().getPermission(user)) {
case ADMIN, WRITE, UNKNOWN -> IssueActionSide.TEAM; // "Unknown" includes "triage"
case READ, NONE -> IssueActionSide.OUTSIDER;
};
}

Expand Down Expand Up @@ -302,12 +301,12 @@ public void comment(String topicSuffix, String markdownBody)
}

private Stream<GHIssue> getDedicatedIssues() throws IOException {
var builder = repository().queryIssues().creator(appLogin());
var builder = searchIssues()
.q(GitHubSearchClauses.author(appLogin()));
if (ref.assignee() != null) {
builder.assignee(ref.assignee());
builder.q(GitHubSearchClauses.assignee(ref.assignee()));
}
builder.state(GHIssueState.ALL);
return Streams.toStream(builder.list())
return toStream(builder.list())
.filter(ref.expectedSuffixStart() != null
? issue -> issue.getTitle().startsWith(ref.topic() + ref.expectedSuffixStart())
// Try exact match in this case to avoid confusion if there are two issues and one is
Expand Down Expand Up @@ -337,10 +336,25 @@ private GHIssue createDedicatedIssue(String title, String lastCommentMarkdownBod

private Stream<GHIssueComment> getAppCommentsSince(GHIssue issue, Instant since) {
String appLogin = appLogin();
return toStream(issue.queryComments().since(Date.from(since)).list())
GHIssueCommentQueryBuilder queryCommentsBuilder = issue.queryComments();
if (since != null) {
queryCommentsBuilder.since(Date.from(since));
}
return toStream(queryCommentsBuilder.list())
.filter(uncheckedIO((GHIssueComment comment) -> appLogin.equals(comment.getUser().getLogin()))::apply);
}

private Stream<GHIssueComment> getNonBotCommentsSince(GHIssue issue, Instant since) {
GHIssueCommentQueryBuilder queryCommentsBuilder = issue.queryComments();
if (since != null) {
queryCommentsBuilder.since(Date.from(since));
}
return toStream(queryCommentsBuilder.list())
// Relying on the login rather than getType(), because that would involve an additional request.
.filter(uncheckedIO((GHIssueComment comment) -> !comment.getUser().getLogin()
.endsWith(GitHubConstants.BOT_LOGIN_SUFFIX))::apply);
}

private void minimizeOutdatedComment(GHIssueComment comment) {
try {
Map<String, Object> variables = new HashMap<>();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package io.quarkus.github.lottery.github;

import java.time.Instant;
import java.time.ZoneOffset;
import java.util.Set;

public final class GitHubSearchClauses {

private GitHubSearchClauses() {
}

public static String repo(GitHubRepositoryRef ref) {
return "repo:" + ref.repositoryName();
}

public static String isIssue() {
return "is:issue";
}

public static String anyLabel(Set<String> labels) {
return label(String.join(",", labels));
}

public static String label(String label) {
return "label:" + label;
}

public static String updatedBefore(Instant updatedBefore) {
return "updated:<" + updatedBefore.atOffset(ZoneOffset.UTC).toLocalDateTime().toString();
}

public static String author(String author) {
return "author:" + author;
}

public static String assignee(String assignee) {
return "assignee:" + assignee;
}
}
10 changes: 10 additions & 0 deletions src/main/java/io/quarkus/github/lottery/util/GitHubConstants.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package io.quarkus.github.lottery.util;

public final class GitHubConstants {

private GitHubConstants() {
}

public static final String BOT_LOGIN_SUFFIX = "[bot]";

}
1 change: 1 addition & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ quarkus.info.enabled=true
%dev.quarkus.scheduler.enabled=false
%dev.quarkus.log.min-level=TRACE
%dev.quarkus.log.category."io.quarkus.github.lottery".level=TRACE
%dev.quarkus.log.category."org.kohsuke.github.GitHubClient".level=TRACE

%prod.quarkus.openshift.labels."app"=quarkus-github-lottery
# Renew the SSL certificate automatically
Expand Down
Loading

0 comments on commit c457711

Please sign in to comment.