Skip to content

Commit

Permalink
New maintenance category for newly created issues
Browse files Browse the repository at this point in the history
  • Loading branch information
yrodiere committed Dec 12, 2024
1 parent 7b27ee9 commit b2d9afb
Show file tree
Hide file tree
Showing 19 changed files with 567 additions and 45 deletions.
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
related to a specific area (e.g. `area/hibernate-orm`)
that may be stalled and require intervention from maintainers or reporters.

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

Created::
Issues that just got created in your area.
+
Please review, ask for reproducer/information, or plan.
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, 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, 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, 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, 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 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
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 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
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 that should be ignored for the "created" bucket.
Issues 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
41 changes: 38 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,23 @@ 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.issuesNeverActedOnByTeamAndCreatedBetween(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

0 comments on commit b2d9afb

Please sign in to comment.