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

Use the "search issues" API instead of the "query issues" API #194

Merged
merged 5 commits into from
Dec 10, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading