Skip to content

Commit 77fdc5d

Browse files
committed
fix(retest): acknowledge successful reruns and simplify head matching and test fixtures
1 parent 6e1dfd5 commit 77fdc5d

17 files changed

Lines changed: 478 additions & 750 deletions

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ features: [ RETEST_PULL_REQUEST_WORKFLOWS ]
252252
----
253253

254254
Only failed jobs from the latest run of each workflow are retriggered. If the latest run is still in progress, the bot asks you to retry once it completes.
255+
When reruns are started, the bot posts a confirmation comment linking to the workflow runs.
255256

256257
This feature requires:
257258

src/main/java/io/quarkus/bot/ApproveWorkflow.java

Lines changed: 12 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import io.quarkus.bot.config.Feature;
2222
import io.quarkus.bot.config.QuarkusGitHubBotConfig;
2323
import io.quarkus.bot.config.QuarkusGitHubBotConfigFile;
24+
import io.quarkus.bot.util.GHPullRequests;
2425
import io.quarkus.bot.util.PullRequestFilesMatcher;
2526
import io.quarkus.cache.CacheKey;
2627
import io.quarkus.cache.CacheResult;
@@ -90,36 +91,19 @@ private void checkUser(GHEventPayload.WorkflowRun workflowPayload, QuarkusGitHub
9091
}
9192

9293
private void checkFiles(QuarkusGitHubBotConfigFile quarkusBotConfigFile, GHWorkflowRun workflowRun,
93-
ApprovalStatus approval) {
94+
ApprovalStatus approval) throws IOException {
9495
String sha = workflowRun.getHeadSha();
9596

96-
// Now we want to get the pull request we're supposed to be checking.
97-
// It would be nice to use commit.listPullRequests() but that only returns something if the
98-
// base and head of the PR are from the same repository, which rules out most scenarios where we would want to do an approval
99-
100-
String fullyQualifiedBranchName = workflowRun.getHeadRepository().getOwnerName() + ":" + workflowRun.getHeadBranch();
101-
102-
PagedIterable<GHPullRequest> pullRequestsForThisBranch = workflowRun.getRepository().queryPullRequests()
103-
.head(fullyQualifiedBranchName)
104-
.list();
105-
106-
// The number of PRs with matching branch name should be exactly one, but if the PR
107-
// has been closed it sometimes disappears from the list; also, if two branch names
108-
// start with the same string, both will turn up in the query.
109-
for (GHPullRequest pullRequest : pullRequestsForThisBranch) {
110-
111-
// Only look at PRs whose commit sha matches
112-
if (sha.equals(pullRequest.getHead().getSha())) {
113-
114-
for (QuarkusGitHubBotConfigFile.WorkflowApprovalRule rule : quarkusBotConfigFile.workflows.rules) {
115-
// We allow if the files or directories match the allow rule ...
116-
if (matchRuleFromChangedFiles(pullRequest, rule.allow)) {
117-
approval.shouldApprove = true;
118-
}
119-
// ... unless we also match the unless rule
120-
if (matchRuleFromChangedFiles(pullRequest, rule.unless)) {
121-
approval.shouldNotApprove = true;
122-
}
97+
for (GHPullRequest pullRequest : GHPullRequests.matchingHeadPullRequests(workflowRun.getRepository(),
98+
workflowRun.getHeadRepository(), workflowRun.getHeadBranch(), sha)) {
99+
for (QuarkusGitHubBotConfigFile.WorkflowApprovalRule rule : quarkusBotConfigFile.workflows.rules) {
100+
// We allow if the files or directories match the allow rule ...
101+
if (matchRuleFromChangedFiles(pullRequest, rule.allow)) {
102+
approval.shouldApprove = true;
103+
}
104+
// ... unless we also match the unless rule
105+
if (matchRuleFromChangedFiles(pullRequest, rule.unless)) {
106+
approval.shouldNotApprove = true;
123107
}
124108
}
125109
}

src/main/java/io/quarkus/bot/retest/RetestCommand.java

Lines changed: 39 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package io.quarkus.bot.retest;
22

33
import java.io.IOException;
4+
import java.net.URL;
45
import java.util.ArrayList;
56
import java.util.List;
7+
import java.util.stream.Collectors;
68

79
import jakarta.inject.Inject;
810

@@ -23,6 +25,7 @@
2325
import io.quarkus.bot.config.Feature;
2426
import io.quarkus.bot.config.QuarkusGitHubBotConfig;
2527
import io.quarkus.bot.config.QuarkusGitHubBotConfigFile;
28+
import io.quarkus.bot.service.GHIssueCommentService;
2629

2730
/**
2831
* Handles {@code @quarkusbot retest} comments on pull requests.
@@ -43,6 +46,9 @@ class RetestCommand implements RetestCommandHandler {
4346
@Inject
4447
FailedJobsRerunner failedJobsRerunner;
4548

49+
@Inject
50+
GHIssueCommentService issueCommentService;
51+
4652
@Override
4753
public void run(QuarkusGitHubBotConfigFile quarkusBotConfigFile, GHEventPayload.IssueComment issueCommentPayload) {
4854
if (!Feature.RETEST_PULL_REQUEST_WORKFLOWS.isEnabled(quarkusBotConfigFile)) {
@@ -60,7 +66,7 @@ public void run(QuarkusGitHubBotConfigFile quarkusBotConfigFile, GHEventPayload.
6066
throw RetestCommandException.noEligibleWorkflowRuns(workflowSelection.noEligibleReason());
6167
}
6268

63-
List<Long> startedWorkflowRunIds = new ArrayList<>();
69+
List<GHWorkflowRun> startedWorkflowRuns = new ArrayList<>();
6470
for (GHWorkflowRun workflowRun : workflowSelection.eligibleRuns()) {
6571
if (quarkusBotConfig.isDryRun()) {
6672
LOG.infof("Pull request #%d - Retest failed jobs for workflow run #%d (dry-run)",
@@ -70,15 +76,21 @@ public void run(QuarkusGitHubBotConfigFile quarkusBotConfigFile, GHEventPayload.
7076

7177
try {
7278
failedJobsRerunner.rerunFailedJobs(issueCommentPayload, workflowRun);
73-
startedWorkflowRunIds.add(workflowRun.getId());
79+
startedWorkflowRuns.add(workflowRun);
7480
} catch (RuntimeException e) {
75-
if (startedWorkflowRunIds.isEmpty()) {
81+
if (startedWorkflowRuns.isEmpty()) {
7682
throw e;
7783
}
7884

79-
throw RetestCommandException.partialRerunFailure(startedWorkflowRunIds, workflowRun.getId(), e);
85+
throw RetestCommandException.partialRerunFailure(
86+
startedWorkflowRuns.stream().map(GHWorkflowRun::getId).toList(), workflowRun.getId(), e);
8087
}
8188
}
89+
90+
if (!startedWorkflowRuns.isEmpty()) {
91+
issueCommentService.addComment(issueCommentPayload.getIssue(), successMessage(startedWorkflowRuns), false,
92+
quarkusBotConfig.isDryRun());
93+
}
8294
}
8395

8496
private static GHPullRequest getPullRequest(GHEventPayload.IssueComment issueCommentPayload) {
@@ -97,4 +109,27 @@ private RetestWorkflowSelection getWorkflowSelection(GHPullRequest pullRequest)
97109
throw RetestCommandException.unableToInspectWorkflowRuns(e);
98110
}
99111
}
112+
113+
private static String successMessage(List<GHWorkflowRun> startedWorkflowRuns) {
114+
String workflowRunLinks = startedWorkflowRuns.stream()
115+
.map(RetestCommand::workflowRunReference)
116+
.collect(Collectors.joining(", "));
117+
String label = startedWorkflowRuns.size() == 1 ? "workflow run " : "workflow runs ";
118+
return ":arrows_counterclockwise: Retest started for failed jobs in " + label + workflowRunLinks + ".";
119+
}
120+
121+
private static String workflowRunReference(GHWorkflowRun workflowRun) {
122+
String label = "#" + workflowRun.getId();
123+
URL htmlUrl;
124+
try {
125+
htmlUrl = workflowRun.getHtmlUrl();
126+
} catch (IOException e) {
127+
return label;
128+
}
129+
if (htmlUrl == null) {
130+
return label;
131+
}
132+
133+
return "[" + label + "](" + htmlUrl + ")";
134+
}
100135
}

src/main/java/io/quarkus/bot/retest/RetestExecutionErrorHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@
77
import org.kohsuke.github.GHEventPayload;
88

99
import io.quarkiverse.githubapp.command.airline.ExecutionErrorHandler;
10-
import io.quarkus.bot.GHIssueCommentService;
1110
import io.quarkus.bot.config.QuarkusGitHubBotConfig;
11+
import io.quarkus.bot.service.GHIssueCommentService;
1212

1313
/**
1414
* Formats command failures as pull-request comments.

src/main/java/io/quarkus/bot/retest/RetestParseErrorHandler.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
import io.quarkiverse.githubapp.ConfigFile;
1919
import io.quarkiverse.githubapp.GitHubConfigFileProvider;
2020
import io.quarkiverse.githubapp.command.airline.ParseErrorHandler;
21-
import io.quarkus.bot.GHIssueCommentService;
2221
import io.quarkus.bot.config.Feature;
2322
import io.quarkus.bot.config.QuarkusGitHubBotConfig;
2423
import io.quarkus.bot.config.QuarkusGitHubBotConfigFile;
24+
import io.quarkus.bot.service.GHIssueCommentService;
2525

2626
/**
2727
* Dry-run-aware parse error handler for retest commands.

src/main/java/io/quarkus/bot/retest/RetestWorkflowRunSelector.java

Lines changed: 8 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
import org.kohsuke.github.GHWorkflowRun;
2121
import org.kohsuke.github.PagedIterable;
2222

23+
import io.quarkus.bot.util.GHPullRequests;
24+
2325
/**
2426
* Selects the latest rerunnable workflow runs for a pull request head.
2527
*/
@@ -48,8 +50,9 @@ RetestWorkflowSelection selectWorkflowRuns(GHPullRequest pullRequest) throws IOE
4850
private static PullRequestHeadContext pullRequestHeadContext(GHPullRequest pullRequest) throws IOException {
4951
GHCommitPointer pullRequestHead = pullRequest.getHead();
5052
GHRepository pullRequestHeadRepository = pullRequestHead.getRepository();
51-
boolean ambiguousPullRequestHead = matchingPullRequestsForHead(pullRequest, pullRequestHead,
52-
pullRequestHeadRepository).size() > 1;
53+
boolean ambiguousPullRequestHead = pullRequestHeadRepository != null
54+
&& GHPullRequests.matchingHeadPullRequests(pullRequest.getRepository(), pullRequestHeadRepository,
55+
pullRequestHead.getRef(), pullRequestHead.getSha(), GHIssueState.ALL).size() > 1;
5356
return new PullRequestHeadContext(pullRequestHead, pullRequestHeadRepository, ambiguousPullRequestHead);
5457
}
5558

@@ -127,7 +130,7 @@ private static boolean matchesPullRequestHead(GHWorkflowRun workflowRun, PullReq
127130
if (!headContext.pullRequestHead().getRef().equals(workflowRun.getHeadBranch())) {
128131
return false;
129132
}
130-
return isSameRepository(headContext.pullRequestHeadRepository(), workflowRun.getHeadRepository());
133+
return GHPullRequests.isSameRepository(headContext.pullRequestHeadRepository(), workflowRun.getHeadRepository());
131134
}
132135

133136
private static boolean isAssociatedWithCurrentPullRequest(List<GHPullRequest> associatedPullRequests,
@@ -221,50 +224,6 @@ private static boolean hasFailedLatestJob(GHWorkflowRun workflowRun) {
221224
return false;
222225
}
223226

224-
private static boolean isSameRepository(GHRepository left, GHRepository right) {
225-
if (left == null || right == null) {
226-
return false;
227-
}
228-
229-
String leftFullName = left.getFullName();
230-
String rightFullName = right.getFullName();
231-
if (leftFullName == null || rightFullName == null) {
232-
return false;
233-
}
234-
235-
return leftFullName.equals(rightFullName);
236-
}
237-
238-
private static List<GHPullRequest> matchingPullRequestsForHead(GHPullRequest pullRequest,
239-
GHCommitPointer pullRequestHead,
240-
GHRepository pullRequestHeadRepository) throws IOException {
241-
if (pullRequestHeadRepository == null) {
242-
return List.of(pullRequest);
243-
}
244-
245-
String fullyQualifiedBranchName = pullRequestHeadRepository.getOwnerName() + ":" + pullRequestHead.getRef();
246-
List<GHPullRequest> matchingPullRequests = new ArrayList<>();
247-
248-
for (GHPullRequest candidatePullRequest : pullRequest.getRepository().queryPullRequests()
249-
.state(GHIssueState.ALL)
250-
.head(fullyQualifiedBranchName)
251-
.list()) {
252-
if (!pullRequestHead.getRef().equals(candidatePullRequest.getHead().getRef())) {
253-
continue;
254-
}
255-
if (!pullRequestHead.getSha().equals(candidatePullRequest.getHead().getSha())) {
256-
continue;
257-
}
258-
if (!isSameRepository(pullRequestHeadRepository, candidatePullRequest.getHead().getRepository())) {
259-
continue;
260-
}
261-
262-
matchingPullRequests.add(candidatePullRequest);
263-
}
264-
265-
return matchingPullRequests;
266-
}
267-
268227
private static String workflowIdentity(GHWorkflowRun workflowRun, String event) {
269228
if (workflowRun.getWorkflowId() > 0) {
270229
return "workflow-id:" + workflowRun.getWorkflowId() + ":event:" + event;
@@ -275,7 +234,8 @@ private static String workflowIdentity(GHWorkflowRun workflowRun, String event)
275234
return "workflow-url:" + workflowUrl + ":event:" + event;
276235
}
277236

278-
return event + "::" + workflowRun.getName() + "::" + workflowRun.getRunNumber();
237+
throw RetestCommandException.unableToInspectWorkflowRuns(
238+
new IllegalStateException("Workflow run #" + workflowRun.getId() + " is missing workflow identity."));
279239
}
280240

281241
private record PullRequestHeadContext(GHCommitPointer pullRequestHead, GHRepository pullRequestHeadRepository,

src/main/java/io/quarkus/bot/util/GHPullRequests.java

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,14 @@
11
package io.quarkus.bot.util;
22

3+
import java.io.IOException;
4+
import java.util.ArrayList;
5+
import java.util.List;
36
import java.util.regex.Pattern;
47

8+
import org.kohsuke.github.GHIssueState;
59
import org.kohsuke.github.GHLabel;
610
import org.kohsuke.github.GHPullRequest;
11+
import org.kohsuke.github.GHRepository;
712

813
public final class GHPullRequests {
914

@@ -18,6 +23,29 @@ public static boolean hasLabel(GHPullRequest pullRequest, String labelName) {
1823
return false;
1924
}
2025

26+
public static List<GHPullRequest> matchingHeadPullRequests(GHRepository repository, GHRepository headRepository,
27+
String headRef, String headSha) throws IOException {
28+
if (repository == null || headRepository == null || headRef == null || headSha == null) {
29+
return List.of();
30+
}
31+
32+
return matchingHeadPullRequests(repository.queryPullRequests()
33+
.head(headRepository.getOwnerName() + ":" + headRef)
34+
.list(), headRepository, headRef, headSha);
35+
}
36+
37+
public static List<GHPullRequest> matchingHeadPullRequests(GHRepository repository, GHRepository headRepository,
38+
String headRef, String headSha, GHIssueState state) throws IOException {
39+
if (repository == null || headRepository == null || headRef == null || headSha == null) {
40+
return List.of();
41+
}
42+
43+
return matchingHeadPullRequests(repository.queryPullRequests()
44+
.state(state)
45+
.head(headRepository.getOwnerName() + ":" + headRef)
46+
.list(), headRepository, headRef, headSha);
47+
}
48+
2149
public static String dropVersionSuffix(String title, String branch) {
2250
if (title == null || title.isBlank()) {
2351
return title;
@@ -39,4 +67,47 @@ public static String normalizeTitle(String title, String branch) {
3967

4068
return "[" + branch + "] " + dropVersionSuffix(title, branch);
4169
}
70+
71+
private static List<GHPullRequest> matchingHeadPullRequests(Iterable<GHPullRequest> pullRequests,
72+
GHRepository headRepository, String headRef, String headSha) {
73+
List<GHPullRequest> matchingPullRequests = new ArrayList<>();
74+
75+
for (GHPullRequest pullRequest : pullRequests) {
76+
if (!headRef.equals(pullRequest.getHead().getRef())) {
77+
continue;
78+
}
79+
if (!headSha.equals(pullRequest.getHead().getSha())) {
80+
continue;
81+
}
82+
if (!isSameRepository(headRepository, pullRequest.getHead().getRepository())) {
83+
continue;
84+
}
85+
86+
matchingPullRequests.add(pullRequest);
87+
}
88+
89+
return matchingPullRequests;
90+
}
91+
92+
public static boolean isSameRepository(GHRepository left, GHRepository right) {
93+
if (left == null || right == null) {
94+
return false;
95+
}
96+
97+
String leftFullName = left.getFullName();
98+
String rightFullName = right.getFullName();
99+
if (leftFullName != null && rightFullName != null) {
100+
return leftFullName.equals(rightFullName);
101+
}
102+
103+
String leftOwner = left.getOwnerName();
104+
String rightOwner = right.getOwnerName();
105+
String leftName = left.getName();
106+
String rightName = right.getName();
107+
if (leftOwner == null || rightOwner == null || leftName == null || rightName == null) {
108+
return false;
109+
}
110+
111+
return leftOwner.equals(rightOwner) && leftName.equals(rightName);
112+
}
42113
}

0 commit comments

Comments
 (0)