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

New maintenance category for newly created issues/PRs #198

Merged
merged 3 commits into from
Dec 13, 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
46 changes: 42 additions & 4 deletions README.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,12 @@ If the `maintenance` section is present, you will get notified about issues/PRs
related to a specific area (e.g. `area/hibernate-orm`)
that may be stalled and require intervention from maintainers or reporters.

Issues/PRs in "maintenance" notifications will be split in three categories:
Issues/PRs in "maintenance" notifications will be split in several categories:

Created::
Issues or PRs that just got created in your area.
+
Please review, ask for reproducer/information, or plan future work.
Feedback Needed::
Issues with missing reproducer/information.
+
Expand Down Expand Up @@ -164,6 +168,8 @@ participants:
maintenance:
labels: ["area/hibernate-orm", "area/hibernate-search", "area/elasticsearch"]
days: ["MONDAY", "TUESDAY", "WEDNESDAY", "THURSDAY", "FRIDAY"]
created:
maxIssues: 5
feedback:
needed:
maxIssues: 4
Expand All @@ -182,21 +188,26 @@ Array of Strings, mandatory, no default.
On which days you wish to get notified about maintenance.
+
Array of ``WeekDay``s, mandatory, no default.
`created.maxIssues`::
How many issues/PRs, at most, you wish to be included in the "Created" category
for each notification.
+
Integer, mandatory if the `created` section is present, no default.
`feedback.needed.maxIssues`::
How many issues/PRs, at most, you wish to be included in the "Feedback needed" category
for each notification.
+
Integer, mandatory, no default.
Integer, mandatory if the `feedback` section is present, no default.
`feedback.provided.maxIssues`::
How many issues/PRs, at most, you wish to be included in the "Feedback provided" category
for each notification.
+
Integer, mandatory, no default.
Integer, mandatory if the `feedback` section is present, no default.
`stale.maxIssues`::
How many issues/PRs, at most, you wish to be included in the "Stale" category
for each notification.
+
Integer, mandatory, no default.
Integer, mandatory if the `stale` section is present, no default.

[[participants-stewardship]]
=== Stewardship
Expand Down Expand Up @@ -291,6 +302,11 @@ buckets:
delay: PT0S
timeout: P3D
maintenance:
created:
delay: PT0S
timeout: P1D
expiry: P14D
ignoreLabels: ["triage/on-ice"]
feedback:
labels: ["triage/needs-reproducer"]
needed:
Expand Down Expand Up @@ -328,6 +344,28 @@ How much time to wait after an issue/PR was last notified about
before including it again in the lottery in the "triage" bucket.
+
String in https://en.wikipedia.org/wiki/ISO_8601#Durations[ISO-8601 duration format], mandatory, no default.
+
`buckets.maintenance.created.delay`::
How much time to wait after the creation of an issue/PR
before including it in the lottery in the "created" bucket.
+
String in https://en.wikipedia.org/wiki/ISO_8601#Durations[ISO-8601 duration format], mandatory, no default.
`buckets.maintenance.created.timeout`::
How much time to wait after an issue/PR was last notified about
before including it again in the lottery in the "created" bucket.
+
String in https://en.wikipedia.org/wiki/ISO_8601#Durations[ISO-8601 duration format], mandatory, no default.
+
`buckets.maintenance.created.expiry`::
How much time to wait after the creation of an issue/PR
before excluding it from the lottery in the "created" bucket.
+
String in https://en.wikipedia.org/wiki/ISO_8601#Durations[ISO-8601 duration format], mandatory, no default.
`buckets.maintenance.created.ignoreLabels`::
The labels identifying GitHub issues/PRs that should be ignored for the "created" bucket.
Issues/PRs with one of these labels will never be added to the bucket.
+
Array of Strings, optional, defaults to an empty array.
`buckets.maintenance.feedback.labels`::
The labels identifying GitHub issues for which feedback (a reproducer, more information, ...) was requested.
+
Expand Down
20 changes: 19 additions & 1 deletion src/main/java/io/quarkus/github/lottery/LotteryService.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,18 @@
import java.time.ZoneOffset;
import java.time.ZonedDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Optional;
import java.util.Set;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Inject;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import io.quarkus.github.lottery.config.LotteryConfig;
import io.quarkus.github.lottery.draw.DrawRef;
import io.quarkus.github.lottery.draw.Lottery;
Expand All @@ -32,6 +37,7 @@
@ApplicationScoped
public class LotteryService {

private static final Logger log = LoggerFactory.getLogger(LotteryService.class);
@Inject
GitHubService gitHubService;

Expand Down Expand Up @@ -81,7 +87,19 @@ private void doDrawForRepository(GitHubRepository repo, LotteryConfig lotteryCon
var now = Instant.now(clock);
var drawRef = new DrawRef(repo.ref(), now);

Lottery lottery = new Lottery(now, lotteryConfig.buckets());
// Note: this map only gives partial information -- some maintainers may not be registered for the lottery.
// That's why the information is only used for optimization (to skip issues that we know for sure aren't relevant).
var maintainerUsernamesByAreaLabel = new HashMap<String, Set<String>>();
for (LotteryConfig.Participant participant : lotteryConfig.participants()) {
participant.maintenance().ifPresent(m -> {
for (String label : m.labels()) {
maintainerUsernamesByAreaLabel.computeIfAbsent(label, key -> new LinkedHashSet<>())
.add(participant.username());
}
});
}

Lottery lottery = new Lottery(now, lotteryConfig.buckets(), maintainerUsernamesByAreaLabel);

try (var notifier = notificationService.notifier(drawRef, lotteryConfig.notifications())) {
var history = historyService.fetch(drawRef, lotteryConfig);
Expand Down
17 changes: 17 additions & 0 deletions src/main/java/io/quarkus/github/lottery/config/LotteryConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,25 @@ public Triage(@JsonProperty(required = true) String label,
}

public record Maintenance(
@JsonProperty(required = true) Created created,
@JsonProperty(required = true) Feedback feedback,
@JsonProperty(required = true) Stale stale) {

public record Created(
@JsonUnwrapped @JsonProperty(access = JsonProperty.Access.READ_ONLY) Notification notification,
@JsonProperty(required = true) Duration expiry,
@JsonProperty(required = true) List<String> ignoreLabels) {
// https://stackoverflow.com/a/71539100/6692043
// Also gives us a less verbose constructor for tests
@JsonCreator
public Created(@JsonProperty(required = true) Duration delay,
@JsonProperty(required = true) Duration timeout,
@JsonProperty(required = true) Duration expiry,
@JsonProperty(required = false) List<String> ignoreLabels) {
this(new Notification(delay, timeout), expiry, ignoreLabels == null ? List.of() : ignoreLabels);
}
}

public record Feedback(
@JsonProperty(required = true) List<String> labels,
@JsonProperty(required = true) Needed needed,
Expand Down Expand Up @@ -141,6 +157,7 @@ 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,
@JsonProperty Optional<Participation> created,
Optional<Feedback> feedback,
@JsonProperty Optional<Participation> stale) {
public record Feedback(
Expand Down
42 changes: 39 additions & 3 deletions src/main/java/io/quarkus/github/lottery/draw/Lottery.java
Original file line number Diff line number Diff line change
Expand Up @@ -27,14 +27,16 @@ public final class Lottery {

private final Instant now;
private final LotteryConfig.Buckets config;
private final Map<String, Set<String>> maintainerUsernamesByAreaLabel;
private final Random random;
private final Triage triage;
private final Map<String, Maintenance> maintenanceByLabel;
private final Stewardship stewardship;

public Lottery(Instant now, LotteryConfig.Buckets config) {
public Lottery(Instant now, LotteryConfig.Buckets config, Map<String, Set<String>> maintainerUsernamesByAreaLabel) {
this.now = now;
this.config = config;
this.maintainerUsernamesByAreaLabel = maintainerUsernamesByAreaLabel;
this.random = new Random();
this.triage = new Triage();
this.maintenanceByLabel = new LinkedHashMap<>();
Expand All @@ -46,7 +48,11 @@ Bucket triage() {
}

Maintenance maintenance(String areaLabel) {
return maintenanceByLabel.computeIfAbsent(areaLabel, Maintenance::new);
return maintenanceByLabel.computeIfAbsent(areaLabel, this::createMaintenance);
}

private Maintenance createMaintenance(String areaLabel) {
return new Maintenance(areaLabel, maintainerUsernamesByAreaLabel.getOrDefault(areaLabel, Set.of()));
}

Bucket stewardship() {
Expand Down Expand Up @@ -105,18 +111,30 @@ void createDraws(GitHubRepository repo, LotteryHistory lotteryHistory, List<Draw

final class Maintenance {
private final String areaLabel;
private final Set<String> maintainerUsernames;
private final Bucket created;
private final Bucket feedbackNeeded;
private final Bucket feedbackProvided;
private final Bucket stale;

Maintenance(String areaLabel) {
Maintenance(String areaLabel, Set<String> maintainerUsernames) {
this.areaLabel = areaLabel;
this.maintainerUsernames = maintainerUsernames;
String namePrefix = "maintenance - '" + areaLabel + "' - ";
created = new Bucket(namePrefix + "created");
feedbackNeeded = new Bucket(namePrefix + "feedbackNeeded");
feedbackProvided = new Bucket(namePrefix + "feedbackProvided");
stale = new Bucket(namePrefix + "stale");
}

public void addMaintainer(String username) {
maintainerUsernames.add(username);
}

Bucket created() {
return created;
}

Bucket feedbackNeeded() {
return feedbackNeeded;
}
Expand All @@ -131,6 +149,24 @@ Bucket stale() {

void createDraws(GitHubRepository repo, LotteryHistory lotteryHistory, List<Draw> draws,
Set<Integer> allWinnings) throws IOException {
if (created.hasParticipation()) {
var maxCutoff = now.minus(config.maintenance().created().notification().delay());
var minCutoff = now.minus(config.maintenance().created().expiry());
// Remove duplicates, but preserve order
var ignoreLabels = new LinkedHashSet<String>();
// Ignore issues with feedback request labels,
// since they evidently got some attention from the team already.
ignoreLabels.addAll(config.maintenance().feedback().labels());
ignoreLabels.addAll(config.maintenance().created().ignoreLabels());
var history = lotteryHistory.created();
draws.add(created.createDraw(
repo.issuesOrPullRequestsNeverActedOnByTeamAndCreatedBetween(areaLabel, ignoreLabels,
maintainerUsernames, minCutoff,
maxCutoff)
.filter(issue -> history.lastNotificationTimedOutForIssueNumber(issue.number()))
.iterator(),
allWinnings));
}
// Remove duplicates, but preserve order
Set<String> needFeedbackLabels = new LinkedHashSet<>(config.maintenance().feedback().labels());
if (feedbackNeeded.hasParticipation()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ public record LotteryReport(
Optional<ZoneId> timezone,
Config config,
Optional<Bucket> triage,
Optional<Bucket> created,
Optional<Bucket> feedbackNeeded,
Optional<Bucket> feedbackProvided,
Optional<Bucket> stale,
Optional<Bucket> stewardship) {

Stream<Bucket> buckets() {
return Stream.of(triage, feedbackNeeded, feedbackProvided, stale, stewardship)
return Stream.of(triage, created, feedbackNeeded, feedbackProvided, stale, stewardship)
.filter(Optional::isPresent)
.map(Optional::get);
}
Expand Down Expand Up @@ -59,6 +60,7 @@ public record Serialized(

public Serialized serialized() {
return new Serialized(drawRef.instant(), username, triage.map(Bucket::serialized),
created.map(Bucket::serialized),
feedbackNeeded.map(Bucket::serialized),
feedbackProvided.map(Bucket::serialized),
stale.map(Bucket::serialized),
Expand All @@ -69,6 +71,7 @@ public record Serialized(
Instant instant,
String username,
Optional<Bucket.Serialized> triage,
Optional<Bucket.Serialized> created,
@JsonAlias("reproducerNeeded") Optional<Bucket.Serialized> feedbackNeeded,
@JsonAlias("reproducerProvided") Optional<Bucket.Serialized> feedbackProvided,
Optional<Bucket.Serialized> stale,
Expand Down
16 changes: 13 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 @@ -84,6 +84,7 @@ public LotteryReport report(String triageLabel, Set<String> feedbackLabels) {
feedbackLabels,
maintenance.map(m -> m.labels).orElseGet(Set::of)),
triage.map(Participation::issues).map(LotteryReport.Bucket::new),
maintenance.flatMap(m -> m.created).map(Participation::issues).map(LotteryReport.Bucket::new),
maintenance.flatMap(m -> m.feedbackNeeded).map(Participation::issues).map(LotteryReport.Bucket::new),
maintenance.flatMap(m -> m.feedbackProvided).map(Participation::issues).map(LotteryReport.Bucket::new),
maintenance.flatMap(m -> m.stale).map(Participation::issues).map(LotteryReport.Bucket::new),
Expand All @@ -92,31 +93,39 @@ public LotteryReport report(String triageLabel, Set<String> feedbackLabels) {

private static final class Maintenance {
public static Optional<Maintenance> create(String username, LotteryConfig.Participant.Maintenance config) {
var created = config.created()
.flatMap(p -> Participation.create(username, p));
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()) {
if (created.isEmpty() && feedbackNeeded.isEmpty() && feedbackProvided.isEmpty() && stale.isEmpty()) {
return Optional.empty();
}

return Optional.of(new Maintenance(config.labels(), feedbackNeeded, feedbackProvided, stale));
return Optional.of(new Maintenance(username, config.labels(), created, feedbackNeeded, feedbackProvided, stale));
}

private final String username;
private final Set<String> labels;

private final Optional<Participation> created;
private final Optional<Participation> feedbackNeeded;
private final Optional<Participation> feedbackProvided;
private final Optional<Participation> stale;

private Maintenance(List<String> labels, Optional<Participation> feedbackNeeded,
private Maintenance(String username, List<String> labels,
Optional<Participation> created,
Optional<Participation> feedbackNeeded,
Optional<Participation> feedbackProvided,
Optional<Participation> stale) {
this.username = username;
// Remove duplicates, but preserve order
this.labels = new LinkedHashSet<>(labels);
this.created = created;
this.feedbackNeeded = feedbackNeeded;
this.feedbackProvided = feedbackProvided;
this.stale = stale;
Expand All @@ -125,6 +134,7 @@ private Maintenance(List<String> labels, Optional<Participation> feedbackNeeded,
public void participate(Lottery lottery) {
for (String label : labels) {
Lottery.Maintenance maintenance = lottery.maintenance(label);
created.ifPresent(maintenance.created()::participate);
feedbackNeeded.ifPresent(maintenance.feedbackNeeded()::participate);
feedbackProvided.ifPresent(maintenance.feedbackProvided()::participate);
stale.ifPresent(maintenance.stale()::participate);
Expand Down
Loading
Loading