From 82a24dc99b22ffec26854d3e319fd1118279514f Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 17 Aug 2022 18:06:23 +0100 Subject: [PATCH 01/10] Add new workflow approval logic --- README.adoc | 42 ++ .../java/io/quarkus/bot/ApproveWorkflow.java | 214 +++++++++ .../java/io/quarkus/bot/config/Feature.java | 3 +- .../config/QuarkusGitHubBotConfigFile.java | 29 +- .../bot/util/PullRequestFilesMatcher.java | 74 +++ src/main/java/io/quarkus/bot/util/Triage.java | 34 +- .../java/io/quarkus/bot/it/MockHelper.java | 24 +- .../quarkus/bot/it/WorkflowApprovalTest.java | 451 ++++++++++++++++++ .../resources/workflow-approval-needed.json | 357 ++++++++++++++ .../resources/workflow-from-committer.json | 381 +++++++++++++++ ...w-unknown-contributor-approval-needed.json | 357 ++++++++++++++ 11 files changed, 1929 insertions(+), 37 deletions(-) create mode 100644 src/main/java/io/quarkus/bot/ApproveWorkflow.java create mode 100644 src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java create mode 100644 src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java create mode 100644 src/test/resources/workflow-approval-needed.json create mode 100644 src/test/resources/workflow-from-committer.json create mode 100644 src/test/resources/workflow-unknown-contributor-approval-needed.json diff --git a/README.adoc b/README.adoc index 2acb427..9b7235f 100644 --- a/README.adoc +++ b/README.adoc @@ -193,6 +193,48 @@ When a workflow run associated to a pull request is completed, a report is gener > image::documentation/screenshots/workflow-run-report.png[] +=== Approve workflow runs + +This rule applies more fine-grained protections to workflow runs +than is provided by the basic GitHub settings. If a repository +is https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-features/enabling-features-for-your-repository/managing-github-actions-settings-for-a-repository[set up to only allow workflow runs from committers], +the bot can automatically approve some workflows which meet a set of rules. + +Syntax of the `.github/quarkus-github-bot.yml` file is as follows: + +[source, yaml] +---- +features: [ APPROVE_WORKFLOWS ] +workflows: + rules: + - allow: + directories: + - ./src + - ./documentation + files: + - README.md + users: + minContributions: 5 + unless: + directories: + - ./github + files: + - bad.xml +---- + +Workflows will be allowed if they meet one of the rules in the `allow` section, +unless one of the rules in the `unless` section is triggered. + +In the example above, any file called `README.md` would be allowed, except for `./github/README.md`. +Users who had made at least 5 commits to the repository would be allowed to make any changes, +except to `bad.xml` and any files in `.github`. Other users could make changes to `./src` or `./documentation`. + +If the rule is triggered, the following actions will be executed: + +* `approve` - will approve the workflow which needs approval + +If the workflow is not approved, it will be left untouched, for a human approver to look at. + === Mark closed pull requests as invalid If a pull request is closed without being merged, we automatically add the `triage/invalid` label to the pull request. diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java new file mode 100644 index 0000000..d62b49a --- /dev/null +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -0,0 +1,214 @@ +package io.quarkus.bot; + +import io.quarkiverse.githubapp.ConfigFile; +import io.quarkiverse.githubapp.event.WorkflowRun; +import io.quarkus.bot.config.Feature; +import io.quarkus.bot.config.QuarkusGitHubBotConfig; +import io.quarkus.bot.config.QuarkusGitHubBotConfigFile; +import io.quarkus.bot.util.PullRequestFilesMatcher; +import org.jboss.logging.Logger; +import org.kohsuke.github.GHEventPayload; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepositoryStatistics; +import org.kohsuke.github.GHWorkflowRun; +import org.kohsuke.github.PagedIterable; + +import javax.inject.Inject; +import java.io.IOException; + +class ApproveWorkflow { + + private static final Logger LOG = Logger.getLogger(ApproveWorkflow.class); + + @Inject + QuarkusGitHubBotConfig quarkusBotConfig; + + void evaluatePullRequest( + @WorkflowRun GHEventPayload.WorkflowRun workflowPayload, + @ConfigFile("quarkus-github-bot.yml") QuarkusGitHubBotConfigFile quarkusBotConfigFile) throws IOException { + if (!Feature.APPROVE_WORKFLOWS.isEnabled(quarkusBotConfigFile)) { + return; + } + + // Don't bother checking if there are no rules + if (quarkusBotConfigFile.workflows.rules != null && quarkusBotConfigFile.workflows.rules.isEmpty()) { + return; + } + GHWorkflowRun workflowRun = workflowPayload.getWorkflowRun(); + + // Only check workflows which need action + if (!GHWorkflowRun.Conclusion.ACTION_REQUIRED.equals(workflowRun.getConclusion())) { + return; + } + + ApprovalStatus approval = new ApprovalStatus(); + + checkUser(workflowPayload, quarkusBotConfigFile, approval); + + // Don't bother checking more if we have a red flag + // (but don't return because we need to do stuff with the answer) + if (!approval.hasRedFlag()) { + checkFiles(quarkusBotConfigFile, workflowRun, approval); + } + + if (approval.isApproved()) { + processApproval(workflowRun); + + } + + } + + private void processApproval(GHWorkflowRun workflowRun) throws IOException { + // We could also do things here like adding comments, subject to config + if (!quarkusBotConfig.isDryRun()) { + workflowRun.approve(); + } + } + + private void checkUser(GHEventPayload.WorkflowRun workflowPayload, QuarkusGitHubBotConfigFile quarkusBotConfigFile, + ApprovalStatus approval) { + for (QuarkusGitHubBotConfigFile.WorkflowApprovalRule rule : quarkusBotConfigFile.workflows.rules) { + // We allow if the files or directories match the allow rule ... + if ((rule.allow != null && rule.allow.users != null) || (rule.unless != null && rule.unless.users != null)) { + GHRepositoryStatistics.ContributorStats stats = getStatsForUser(workflowPayload); + if (matchRuleForUser(stats, rule.allow)) { + approval.shouldApprove = true; + } + + if (matchRuleForUser(stats, rule.unless)) { + approval.shouldNotApprove = true; + } + } + } + } + + private void checkFiles(QuarkusGitHubBotConfigFile quarkusBotConfigFile, GHWorkflowRun workflowRun, + ApprovalStatus approval) { + String sha = workflowRun.getHeadSha(); + + // Now we want to get the pull request we're supposed to be checking. + // It would be nice to use commit.listPullRequests() but that only returns something if the + // base and head of the PR are from the same repository, which rules out most scenarios where we would want to do an approval + + String fullyQualifiedBranchName = workflowRun.getHeadRepository().getOwnerName() + ":" + workflowRun.getHeadBranch(); + + PagedIterable pullRequestsForThisBranch = workflowRun.getRepository().queryPullRequests() + .head(fullyQualifiedBranchName) + .list(); + + // The number of PRs with matching branch name should be exactly one, but if the PR + // has been closed it sometimes disappears from the list; also, if two branch names + // start with the same string, both will turn up in the query. + for (GHPullRequest pullRequest : pullRequestsForThisBranch) { + + // Only look at PRs whose commit sha matches + if (sha.equals(pullRequest.getHead().getSha())) { + + for (QuarkusGitHubBotConfigFile.WorkflowApprovalRule rule : quarkusBotConfigFile.workflows.rules) { + // We allow if the files or directories match the allow rule ... + if (matchRuleFromChangedFiles(pullRequest, rule.allow)) { + approval.shouldApprove = true; + } + // ... unless we also match the unless rule + if (matchRuleFromChangedFiles(pullRequest, rule.unless)) { + approval.shouldNotApprove = true; + } + } + } + } + } + + public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, + QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { + // for now, we only use the files but we could also use the other rules at some point + if (rule == null) { + return false; + } + + boolean matches = false; + + PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); + if (matchDirectories(prMatcher, rule)) { + matches = true; + } else if (matchFiles(prMatcher, rule)) { + matches = true; + } + + return matches; + } + + private static boolean matchDirectories(PullRequestFilesMatcher prMatcher, + QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { + if (rule.directories == null || rule.directories.isEmpty()) { + return false; + } + if (prMatcher.changedFilesMatchDirectory(rule.directories)) { + return true; + } + return false; + } + + private static boolean matchFiles(PullRequestFilesMatcher prMatcher, + QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { + if (rule.files == null || rule.files.isEmpty()) { + return false; + } + if (prMatcher.changedFilesMatchFile(rule.files)) { + return true; + } + return false; + } + + private boolean matchRuleForUser(GHRepositoryStatistics.ContributorStats stats, + QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { + if (rule == null || stats == null) { + return false; + } + + if (rule.users == null) { + return false; + } + + if (rule.users.minContributions != null && stats.getTotal() >= rule.users.minContributions) { + return true; + } + + // We can add more rules here, for example how long the user has been contributing + + return false; + } + + private GHRepositoryStatistics.ContributorStats getStatsForUser(GHEventPayload.WorkflowRun workflowPayload) { + if (workflowPayload.getSender().getLogin() != null) { + try { + GHRepositoryStatistics statistics = workflowPayload.getRepository().getStatistics(); + if (statistics != null) { + PagedIterable contributors = statistics.getContributorStats(); + for (GHRepositoryStatistics.ContributorStats contributor : contributors) { + if (workflowPayload.getSender().getLogin().equals(contributor.getAuthor().getLogin())) { + return contributor; + } + } + } + } catch (InterruptedException | IOException e) { + LOG.info("Could not get contributors" + e); + } + } + return null; + } + + private static class ApprovalStatus { + // There are two variables here because we check a number of indicators and a number of counter-indicators + // (ie green flags and red flags) + boolean shouldApprove = false; + boolean shouldNotApprove = false; + + boolean isApproved() { + return shouldApprove && !shouldNotApprove; + } + + public boolean hasRedFlag() { + return shouldNotApprove; + } + } +} diff --git a/src/main/java/io/quarkus/bot/config/Feature.java b/src/main/java/io/quarkus/bot/config/Feature.java index ebd4c80..3ea66ee 100644 --- a/src/main/java/io/quarkus/bot/config/Feature.java +++ b/src/main/java/io/quarkus/bot/config/Feature.java @@ -10,7 +10,8 @@ public enum Feature { SET_AREA_LABEL_COLOR, TRIAGE_ISSUES_AND_PULL_REQUESTS, TRIAGE_DISCUSSIONS, - PUSH_TO_PROJECTS; + PUSH_TO_PROJECTS, + APPROVE_WORKFLOWS; public boolean isEnabled(QuarkusGitHubBotConfigFile quarkusBotConfigFile) { if (quarkusBotConfigFile == null) { diff --git a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java index cf75f7e..d783c31 100644 --- a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java +++ b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java @@ -1,13 +1,13 @@ package io.quarkus.bot.config; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; + import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.TreeSet; -import com.fasterxml.jackson.databind.annotation.JsonDeserialize; - public class QuarkusGitHubBotConfigFile { @JsonDeserialize(as = HashSet.class) @@ -21,6 +21,8 @@ public class QuarkusGitHubBotConfigFile { public ProjectsClassic projectsClassic = new ProjectsClassic(); + public Workflows workflows = new Workflows(); + public static class TriageConfig { public List rules = new ArrayList<>(); @@ -81,6 +83,11 @@ public static class WorkflowRunAnalysisConfig { public Set workflows = new HashSet<>(); } + public static class Workflows { + + public List rules = new ArrayList<>(); + } + public static class Projects { public List rules = new ArrayList<>(); @@ -105,6 +112,24 @@ public static class ProjectTriageRule { public String status; } + public static class WorkflowApprovalRule { + + public WorkflowApprovalCondition allow; + public WorkflowApprovalCondition unless; + + } + + public static class WorkflowApprovalCondition { + public List directories; + public List files; + public UserRule users; + + } + + public static class UserRule { + public Integer minContributions; + } + boolean isFeatureEnabled(Feature feature) { return features.contains(Feature.ALL) || features.contains(feature); } diff --git a/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java new file mode 100644 index 0000000..abfac80 --- /dev/null +++ b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java @@ -0,0 +1,74 @@ +package io.quarkus.bot.util; + +import com.hrakaroo.glob.GlobPattern; +import com.hrakaroo.glob.MatchingEngine; +import org.jboss.logging.Logger; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHPullRequestFileDetail; +import org.kohsuke.github.PagedIterable; + +import java.util.Collection; + +public class PullRequestFilesMatcher { + + private static final Logger LOG = Logger.getLogger(PullRequestFilesMatcher.class); + + private final GHPullRequest pullRequest; + + public PullRequestFilesMatcher(GHPullRequest pullRequest) { + this.pullRequest = pullRequest; + } + + public boolean changedFilesMatchDirectory(Collection directories) { + for (GHPullRequestFileDetail changedFile : pullRequest.listFiles()) { + for (String directory : directories) { + + if (!directory.contains("*")) { + if (changedFile.getFilename().startsWith(directory)) { + return true; + } + } else { + try { + MatchingEngine matchingEngine = GlobPattern.compile(directory); + if (matchingEngine.matches(changedFile.getFilename())) { + return true; + } + } catch (Exception e) { + LOG.error("Error evaluating glob expression: " + directory, e); + } + } + } + } + return false; + } + + public boolean changedFilesMatchFile(Collection files) { + + PagedIterable prFiles = pullRequest.listFiles(); + + if (prFiles == null || files == null) { + return false; + } + + for (GHPullRequestFileDetail changedFile : prFiles) { + for (String file : files) { + + if (!file.contains("*")) { + if (changedFile.getFilename().endsWith(file)) { + return true; + } + } else { + try { + MatchingEngine matchingEngine = GlobPattern.compile(file); + if (matchingEngine.matches(changedFile.getFilename())) { + return true; + } + } catch (Exception e) { + LOG.error("Error evaluating glob expression: " + file, e); + } + } + } + } + return false; + } +} diff --git a/src/main/java/io/quarkus/bot/util/Triage.java b/src/main/java/io/quarkus/bot/util/Triage.java index 6f03ea9..c816e7e 100644 --- a/src/main/java/io/quarkus/bot/util/Triage.java +++ b/src/main/java/io/quarkus/bot/util/Triage.java @@ -1,19 +1,15 @@ package io.quarkus.bot.util; +import io.quarkus.bot.config.QuarkusGitHubBotConfigFile.TriageRule; +import io.quarkus.bot.el.SimpleELContext; +import org.jboss.logging.Logger; +import org.kohsuke.github.GHPullRequest; + import javax.el.ELContext; import javax.el.ELManager; import javax.el.ExpressionFactory; import javax.el.ValueExpression; -import com.hrakaroo.glob.GlobPattern; -import com.hrakaroo.glob.MatchingEngine; -import org.jboss.logging.Logger; - -import io.quarkus.bot.config.QuarkusGitHubBotConfigFile.TriageRule; -import io.quarkus.bot.el.SimpleELContext; -import org.kohsuke.github.GHPullRequest; -import org.kohsuke.github.GHPullRequestFileDetail; - public final class Triage { private static final Logger LOG = Logger.getLogger(Triage.class); @@ -86,23 +82,9 @@ public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, Triag return false; } - for (GHPullRequestFileDetail changedFile : pullRequest.listFiles()) { - for (String directory : rule.directories) { - if (!directory.contains("*")) { - if (changedFile.getFilename().startsWith(directory)) { - return true; - } - } else { - try { - MatchingEngine matchingEngine = GlobPattern.compile(directory); - if (matchingEngine.matches(changedFile.getFilename())) { - return true; - } - } catch (Exception e) { - LOG.error("Error evaluating glob expression: " + directory, e); - } - } - } + PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); + if (prMatcher.changedFilesMatchDirectory(rule.directories)) { + return true; } return false; diff --git a/src/test/java/io/quarkus/bot/it/MockHelper.java b/src/test/java/io/quarkus/bot/it/MockHelper.java index 348e64a..c28b8df 100644 --- a/src/test/java/io/quarkus/bot/it/MockHelper.java +++ b/src/test/java/io/quarkus/bot/it/MockHelper.java @@ -1,20 +1,22 @@ package io.quarkus.bot.it; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import org.kohsuke.github.GHPullRequestFileDetail; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.PagedIterable; +import org.kohsuke.github.PagedIterator; import java.util.Iterator; import java.util.List; -import org.kohsuke.github.GHPullRequestFileDetail; -import org.kohsuke.github.PagedIterable; -import org.kohsuke.github.PagedIterator; +import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class MockHelper { public static GHPullRequestFileDetail mockGHPullRequestFileDetail(String filename) { GHPullRequestFileDetail mock = mock(GHPullRequestFileDetail.class); - when(mock.getFilename()).thenReturn(filename); + lenient().when(mock.getFilename()).thenReturn(filename); return mock; } @@ -22,14 +24,20 @@ public static GHPullRequestFileDetail mockGHPullRequestFileDetail(String filenam @SuppressWarnings("unchecked") public static PagedIterable mockPagedIterable(T... contentMocks) { PagedIterable iterableMock = mock(PagedIterable.class); - when(iterableMock.iterator()).thenAnswer(ignored -> { + lenient().when(iterableMock.iterator()).thenAnswer(ignored -> { PagedIterator iteratorMock = mock(PagedIterator.class); Iterator actualIterator = List.of(contentMocks).iterator(); when(iteratorMock.next()).thenAnswer(ignored2 -> actualIterator.next()); - when(iteratorMock.hasNext()).thenAnswer(ignored2 -> actualIterator.hasNext()); + lenient().when(iteratorMock.hasNext()).thenAnswer(ignored2 -> actualIterator.hasNext()); return iteratorMock; }); return iterableMock; } + public static GHUser mockUser(String login) { + GHUser user = mock(GHUser.class); + when(user.getLogin()).thenReturn(login); + return user; + } + } diff --git a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java new file mode 100644 index 0000000..8dda814 --- /dev/null +++ b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java @@ -0,0 +1,451 @@ +package io.quarkus.bot.it; + +import io.quarkiverse.githubapp.testing.GitHubAppTest; +import io.quarkiverse.githubapp.testing.dsl.GitHubMockSetupContext; +import io.quarkiverse.githubapp.testing.dsl.GitHubMockVerificationContext; +import io.quarkus.test.junit.QuarkusTest; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.kohsuke.github.GHCommitPointer; +import org.kohsuke.github.GHEvent; +import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHPullRequestFileDetail; +import org.kohsuke.github.GHPullRequestQueryBuilder; +import org.kohsuke.github.GHRepository; +import org.kohsuke.github.GHRepositoryStatistics; +import org.kohsuke.github.GHUser; +import org.kohsuke.github.GHWorkflowRun; +import org.kohsuke.github.PagedIterable; +import org.mockito.Answers; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.io.IOException; + +import static io.quarkiverse.githubapp.testing.GitHubAppTesting.given; +import static io.quarkus.bot.it.MockHelper.mockUser; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.withSettings; + +@QuarkusTest +@GitHubAppTest +@ExtendWith(MockitoExtension.class) +public class WorkflowApprovalTest { + + private void setupMockQueriesAndCommits(GitHubMockSetupContext mocks) { + GHRepository repoMock = mocks.repository("bot-playground"); + GHPullRequestQueryBuilder workflowRunQueryBuilderMock = mock(GHPullRequestQueryBuilder.class, + withSettings().defaultAnswer(Answers.RETURNS_SELF)); + when(repoMock.queryPullRequests()) + .thenReturn(workflowRunQueryBuilderMock); + PagedIterable iterableMock = MockHelper.mockPagedIterable(pr(mocks)); + when(workflowRunQueryBuilderMock.list()) + .thenReturn(iterableMock); + + GHCommitPointer head = mock(GHCommitPointer.class); + when(pr(mocks).getHead()).thenReturn(head); + when(head.getSha()).thenReturn("f2b91b5e80e1880f03a91fdde381bb24debf102c"); + } + + private void setupMockUsers(GitHubMockSetupContext mocks) throws InterruptedException, IOException { + GHRepository repoMock = mocks.repository("bot-playground"); + GHRepositoryStatistics stats = mock(GHRepositoryStatistics.class); + GHRepositoryStatistics.ContributorStats contributorStats = mock(GHRepositoryStatistics.ContributorStats.class); + GHUser user = mockUser("holly-test-holly"); + when(contributorStats.getAuthor()).thenReturn(user); + PagedIterable iterableStats = MockHelper.mockPagedIterable(contributorStats); + when(stats.getContributorStats()).thenReturn(iterableStats); + when(repoMock.getStatistics()).thenReturn(stats); + + } + + @Test + void changeToAnAllowedDirectoryShouldBeApproved() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - ./src + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./src/innocuous.java")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks)).listFiles(); + verifyApproved(mocks); + }); + } + + @Test + void changeToAWildcardedDirectoryShouldBeApproved() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - "*" + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./src/innocuous.java")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks)).listFiles(); + verifyApproved(mocks); + }); + } + + @Test + void changeToADirectoryWithNoRulesShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - ./src + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./github/important.yml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks)).listFiles(); + verifyNotApproved(mocks); + }); + } + + @Test + void changeToAnAllowedAndUnlessedDirectoryShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - "*" + unless: + directories: + - ./github + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./github/important.yml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks), times(2)).listFiles(); + verifyNotApproved(mocks); + }); + } + + @Test + void changeToAnAllowedDirectoryWithAnIrrelevantUnlessedDirectoryShouldBeAccepted() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - "*" + unless: + directories: + - ./github + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./innocuous/important.yml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks), times(2)).listFiles(); + verifyApproved(mocks); + }); + } + + @Test + void changeToAnAllowedFileShouldBeApproved() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + files: + - pom.xml + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./innocuous/something/pom.xml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks)).listFiles(); + verifyApproved(mocks); + }); + } + + @Test + void changeToAFileInAnAllowedDirectoryWithAnIrrelevantUnlessShouldBeAllowed() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - ./src + unless: + files: + - bad.xml + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./src/good.xml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks), times(2)).listFiles(); + verifyApproved(mocks); + }); + } + + @Test + void changeToAnUnlessedFileInAnAllowedDirectoryShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - ./src + unless: + files: + - bad.xml + """); + setupMockQueriesAndCommits(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./src/bad.xml")); + when(pr(mocks).listFiles()).thenReturn(paths); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verify(pr(mocks), times(2)).listFiles(); + verifyNotApproved(mocks); + }); + } + + @Test + void changeFromAnUnknownUserShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + users: + minContributions: 5 + """); + setupMockQueriesAndCommits(mocks); + setupMockUsers(mocks); + }) + .when().payloadFromClasspath("/workflow-unknown-contributor-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verifyNotApproved(mocks); + }); + } + + @Test + void changeFromANewishUserShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + users: + minContributions: 5 + """); + setupMockQueriesAndCommits(mocks); + setupMockUsers(mocks); + GHRepositoryStatistics.ContributorStats contributorStats = mocks.repository("bot-playground").getStatistics() + .getContributorStats().iterator().next(); + when(contributorStats.getTotal()).thenReturn(1); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verifyNotApproved(mocks); + }); + } + + @Test + void changeFromAnEstablishedUserShouldBeAllowed() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + users: + minContributions: 5 + """); + setupMockQueriesAndCommits(mocks); + setupMockUsers(mocks); + GHRepositoryStatistics.ContributorStats contributorStats = mocks.repository("bot-playground").getStatistics() + .getContributorStats().iterator().next(); + when(contributorStats.getTotal()).thenReturn(20); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verifyApproved(mocks); + }); + } + + @Test + void changeFromAnEstablishedUserToADangerousFileShouldBeSoftRejected() throws Exception { + given().github(mocks -> { + mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + users: + minContributions: 5 + unless: + files: + - bad.xml + """); + setupMockQueriesAndCommits(mocks); + setupMockUsers(mocks); + PagedIterable paths = MockHelper + .mockPagedIterable(MockHelper.mockGHPullRequestFileDetail("./src/bad.xml")); + GHRepositoryStatistics.ContributorStats contributorStats = mocks.repository("bot-playground").getStatistics() + .getContributorStats().iterator().next(); + when(contributorStats.getTotal()).thenReturn(20); + }) + .when().payloadFromClasspath("/workflow-approval-needed.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verifyApproved(mocks); + }); + } + + @Test + void workflowIsPreApprovedShouldDoNothing() throws Exception { + given().github(mocks -> mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS ] + workflows: + rules: + - allow: + directories: + - ./src + unless: + files: + - bad.xml + """)) + .when().payloadFromClasspath("/workflow-from-committer.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + // No interactions expected, because the workflow is already in an active state + verifyNoMoreInteractions(mocks.ghObjects()); + }); + } + + @Test + void noRulesShouldDoNothing() throws Exception { + given().github(mocks -> mocks.configFileFromString( + "quarkus-github-bot.yml", + """ + features: [ APPROVE_WORKFLOWS, ALL ] + workflows: + rules: + """)) + .when().payloadFromClasspath("/workflow-from-committer.json") + .event(GHEvent.WORKFLOW_RUN) + .then().github(mocks -> { + verifyNoMoreInteractions(mocks.ghObjects()); + }); + } + + private void verifyApproved(GitHubMockVerificationContext mocks) throws Exception { + GHWorkflowRun workflow = mocks.ghObject(GHWorkflowRun.class, 2860832197l); + verify(workflow).approve(); + + } + + private void verifyNotApproved(GitHubMockVerificationContext mocks) throws Exception { + GHWorkflowRun workflow = mocks.ghObject(GHWorkflowRun.class, 2860832197l); + verify(workflow, never()).approve(); + + } + + private GHPullRequest pr(GitHubMockSetupContext mocks) { + return mocks.pullRequest(527350930); + } + + private GHPullRequest pr(GitHubMockVerificationContext mocks) { + return mocks.pullRequest(527350930); + } + +} diff --git a/src/test/resources/workflow-approval-needed.json b/src/test/resources/workflow-approval-needed.json new file mode 100644 index 0000000..522f977 --- /dev/null +++ b/src/test/resources/workflow-approval-needed.json @@ -0,0 +1,357 @@ +{ + "action": "requested", + "workflow_run": { + "id": 2860832197, + "name": "CI", + "node_id": "WFR_kwLOHzCew86qhNXF", + "head_branch": "main", + "head_sha": "f2b91b5e80e1880f03a91fdde381bb24debf102c", + "path": ".github/workflows/blank.yml", + "run_number": 15, + "event": "pull_request", + "status": "completed", + "conclusion": "action_required", + "workflow_id": 32423768, + "check_suite_id": 7817139885, + "check_suite_node_id": "CS_kwDOHzCew88AAAAB0fAWrQ", + "url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197", + "html_url": "https://github.com/holly-cummins/bot-playground/actions/runs/2860832197", + "pull_requests": [], + "created_at": "2022-08-15T13:08:52Z", + "updated_at": "2022-08-15T13:08:52Z", + "actor": { + "login": "holly-test-holly", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-test-holly", + "html_url": "https://github.com/holly-test-holly", + "followers_url": "https://api.github.com/users/holly-test-holly/followers", + "following_url": "https://api.github.com/users/holly-test-holly/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-test-holly/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-test-holly/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-test-holly/subscriptions", + "organizations_url": "https://api.github.com/users/holly-test-holly/orgs", + "repos_url": "https://api.github.com/users/holly-test-holly/repos", + "events_url": "https://api.github.com/users/holly-test-holly/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-test-holly/received_events", + "type": "User", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2022-08-15T13:08:52Z", + "triggering_actor": { + "login": "holly-test-holly", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-test-holly", + "html_url": "https://github.com/holly-test-holly", + "followers_url": "https://api.github.com/users/holly-test-holly/followers", + "following_url": "https://api.github.com/users/holly-test-holly/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-test-holly/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-test-holly/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-test-holly/subscriptions", + "organizations_url": "https://api.github.com/users/holly-test-holly/orgs", + "repos_url": "https://api.github.com/users/holly-test-holly/repos", + "events_url": "https://api.github.com/users/holly-test-holly/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-test-holly/received_events", + "type": "User", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197/jobs", + "logs_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197/logs", + "check_suite_url": "https://api.github.com/repos/holly-cummins/bot-playground/check-suites/7817139885", + "artifacts_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197/artifacts", + "cancel_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197/cancel", + "rerun_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860832197/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/workflows/32423768", + "head_commit": { + "id": "f2b91b5e80e1880f03a91fdde381bb24debf102c", + "tree_id": "9de3ce570c143c11b5d3b6ad38ea02b95fd9437b", + "message": "Merge branch 'holly-cummins:main' into main", + "timestamp": "2022-08-15T12:42:33Z", + "author": { + "name": "holly-test-holly", + "email": "111282252+holly-test-holly@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "holly-cummins/bot-playground", + "private": false, + "owner": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-cummins/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "forks_url": "https://api.github.com/repos/holly-cummins/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-cummins/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-cummins/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-cummins/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-cummins/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-cummins/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-cummins/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-cummins/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-cummins/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-cummins/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-cummins/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-cummins/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-cummins/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-cummins/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-cummins/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-cummins/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-cummins/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-cummins/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-cummins/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-cummins/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-cummins/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-cummins/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-cummins/bot-playground/deployments" + }, + "head_repository": { + "id": 524953530, + "node_id": "R_kgDOH0onug", + "name": "bot-playground", + "full_name": "holly-test-holly/bot-playground", + "private": false, + "owner": { + "login": "holly-test-holly", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-test-holly", + "html_url": "https://github.com/holly-test-holly", + "followers_url": "https://api.github.com/users/holly-test-holly/followers", + "following_url": "https://api.github.com/users/holly-test-holly/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-test-holly/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-test-holly/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-test-holly/subscriptions", + "organizations_url": "https://api.github.com/users/holly-test-holly/orgs", + "repos_url": "https://api.github.com/users/holly-test-holly/repos", + "events_url": "https://api.github.com/users/holly-test-holly/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-test-holly/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-test-holly/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": true, + "url": "https://api.github.com/repos/holly-test-holly/bot-playground", + "forks_url": "https://api.github.com/repos/holly-test-holly/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-test-holly/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-test-holly/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-test-holly/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-test-holly/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-test-holly/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-test-holly/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-test-holly/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-test-holly/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-test-holly/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-test-holly/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-test-holly/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-test-holly/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-test-holly/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-test-holly/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-test-holly/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-test-holly/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-test-holly/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-test-holly/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-test-holly/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-test-holly/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-test-holly/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-test-holly/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-test-holly/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-test-holly/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-test-holly/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-test-holly/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-test-holly/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-test-holly/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-test-holly/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-test-holly/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-test-holly/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-test-holly/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-test-holly/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-test-holly/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-test-holly/bot-playground/deployments" + } + }, + "workflow": { + "id": 32423768, + "node_id": "W_kwDOHzCew84B7r9Y", + "name": "CI", + "path": ".github/workflows/blank.yml", + "state": "active", + "created_at": "2022-08-15T11:10:23.000Z", + "updated_at": "2022-08-15T11:10:44.000Z", + "url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/workflows/32423768", + "html_url": "https://github.com/holly-cummins/bot-playground/blob/main/.github/workflows/blank.yml", + "badge_url": "https://github.com/holly-cummins/bot-playground/workflows/CI/badge.svg" + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "holly-cummins/bot-playground", + "private": false, + "owner": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-cummins/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "forks_url": "https://api.github.com/repos/holly-cummins/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-cummins/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-cummins/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-cummins/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-cummins/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-cummins/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-cummins/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-cummins/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-cummins/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-cummins/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-cummins/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-cummins/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-cummins/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-cummins/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-cummins/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-cummins/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-cummins/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-cummins/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-cummins/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-cummins/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-cummins/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-cummins/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-cummins/bot-playground/deployments", + "created_at": "2022-08-10T09:27:22Z", + "updated_at": "2022-08-10T09:27:22Z", + "pushed_at": "2022-08-15T13:08:50Z", + "git_url": "git://github.com/holly-cummins/bot-playground.git", + "ssh_url": "git@github.com:holly-cummins/bot-playground.git", + "clone_url": "https://github.com/holly-cummins/bot-playground.git", + "svn_url": "https://github.com/holly-cummins/bot-playground", + "homepage": null, + "size": 5, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 1, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "holly-test-holly", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-test-holly", + "html_url": "https://github.com/holly-test-holly", + "followers_url": "https://api.github.com/users/holly-test-holly/followers", + "following_url": "https://api.github.com/users/holly-test-holly/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-test-holly/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-test-holly/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-test-holly/subscriptions", + "organizations_url": "https://api.github.com/users/holly-test-holly/orgs", + "repos_url": "https://api.github.com/users/holly-test-holly/repos", + "events_url": "https://api.github.com/users/holly-test-holly/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-test-holly/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 28125889, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjgxMjU4ODk=" + } +} \ No newline at end of file diff --git a/src/test/resources/workflow-from-committer.json b/src/test/resources/workflow-from-committer.json new file mode 100644 index 0000000..04752d4 --- /dev/null +++ b/src/test/resources/workflow-from-committer.json @@ -0,0 +1,381 @@ +{ + "action": "requested", + "workflow_run": { + "id": 2860920997, + "name": "CI", + "node_id": "WFR_kwLOHzCew86qhjCl", + "head_branch": "holly-cummins-patch-2", + "head_sha": "aae2daff80aa1207f26a4a068235bb7f55596f93", + "path": ".github/workflows/blank.yml", + "run_number": 16, + "event": "pull_request", + "status": "queued", + "conclusion": null, + "workflow_id": 32423768, + "check_suite_id": 7817373106, + "check_suite_node_id": "CS_kwDOHzCew88AAAAB0fOlsg", + "url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997", + "html_url": "https://github.com/holly-cummins/bot-playground/actions/runs/2860920997", + "pull_requests": [ + { + "url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls/12", + "id": 1026461109, + "number": 12, + "head": { + "ref": "holly-cummins-patch-2", + "sha": "aae2daff80aa1207f26a4a068235bb7f55596f93", + "repo": { + "id": 523280067, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "name": "bot-playground" + } + }, + "base": { + "ref": "main", + "sha": "011ba01fe097f0f74fd505a83db26a20432c6c06", + "repo": { + "id": 523280067, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "name": "bot-playground" + } + } + } + ], + "created_at": "2022-08-15T13:24:37Z", + "updated_at": "2022-08-15T13:24:37Z", + "actor": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2022-08-15T13:24:37Z", + "triggering_actor": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997/jobs", + "logs_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997/logs", + "check_suite_url": "https://api.github.com/repos/holly-cummins/bot-playground/check-suites/7817373106", + "artifacts_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997/artifacts", + "cancel_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997/cancel", + "rerun_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/runs/2860920997/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/workflows/32423768", + "head_commit": { + "id": "aae2daff80aa1207f26a4a068235bb7f55596f93", + "tree_id": "484ba2e9f6602aa5df34bc1fec1f2250e14bdb73", + "message": "Update README.md", + "timestamp": "2022-08-15T13:23:53Z", + "author": { + "name": "Holly Cummins", + "email": "hcummins@redhat.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "holly-cummins/bot-playground", + "private": false, + "owner": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-cummins/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "forks_url": "https://api.github.com/repos/holly-cummins/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-cummins/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-cummins/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-cummins/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-cummins/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-cummins/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-cummins/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-cummins/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-cummins/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-cummins/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-cummins/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-cummins/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-cummins/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-cummins/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-cummins/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-cummins/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-cummins/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-cummins/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-cummins/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-cummins/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-cummins/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-cummins/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-cummins/bot-playground/deployments" + }, + "head_repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "holly-cummins/bot-playground", + "private": false, + "owner": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-cummins/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "forks_url": "https://api.github.com/repos/holly-cummins/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-cummins/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-cummins/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-cummins/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-cummins/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-cummins/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-cummins/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-cummins/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-cummins/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-cummins/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-cummins/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-cummins/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-cummins/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-cummins/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-cummins/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-cummins/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-cummins/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-cummins/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-cummins/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-cummins/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-cummins/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-cummins/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-cummins/bot-playground/deployments" + } + }, + "workflow": { + "id": 32423768, + "node_id": "W_kwDOHzCew84B7r9Y", + "name": "CI", + "path": ".github/workflows/blank.yml", + "state": "active", + "created_at": "2022-08-15T11:10:23.000Z", + "updated_at": "2022-08-15T11:10:44.000Z", + "url": "https://api.github.com/repos/holly-cummins/bot-playground/actions/workflows/32423768", + "html_url": "https://github.com/holly-cummins/bot-playground/blob/main/.github/workflows/blank.yml", + "badge_url": "https://github.com/holly-cummins/bot-playground/workflows/CI/badge.svg" + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "holly-cummins/bot-playground", + "private": false, + "owner": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/holly-cummins/bot-playground", + "description": "A playground repository used for testing https://github.com/holly-cummins/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/holly-cummins/bot-playground", + "forks_url": "https://api.github.com/repos/holly-cummins/bot-playground/forks", + "keys_url": "https://api.github.com/repos/holly-cummins/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/holly-cummins/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/holly-cummins/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/holly-cummins/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/holly-cummins/bot-playground/events", + "assignees_url": "https://api.github.com/repos/holly-cummins/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/holly-cummins/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/holly-cummins/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/holly-cummins/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/holly-cummins/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/holly-cummins/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/holly-cummins/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/holly-cummins/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/holly-cummins/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/holly-cummins/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/holly-cummins/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/holly-cummins/bot-playground/merges", + "archive_url": "https://api.github.com/repos/holly-cummins/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/holly-cummins/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/holly-cummins/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/holly-cummins/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/holly-cummins/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/holly-cummins/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/holly-cummins/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/holly-cummins/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/holly-cummins/bot-playground/deployments", + "created_at": "2022-08-10T09:27:22Z", + "updated_at": "2022-08-10T09:27:22Z", + "pushed_at": "2022-08-15T13:24:35Z", + "git_url": "git://github.com/holly-cummins/bot-playground.git", + "ssh_url": "git@github.com:holly-cummins/bot-playground.git", + "clone_url": "https://github.com/holly-cummins/bot-playground.git", + "svn_url": "https://github.com/holly-cummins/bot-playground", + "homepage": null, + "size": 5, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 2, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 2, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "holly-cummins", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/holly-cummins", + "html_url": "https://github.com/holly-cummins", + "followers_url": "https://api.github.com/users/holly-cummins/followers", + "following_url": "https://api.github.com/users/holly-cummins/following{/other_user}", + "gists_url": "https://api.github.com/users/holly-cummins/gists{/gist_id}", + "starred_url": "https://api.github.com/users/holly-cummins/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/holly-cummins/subscriptions", + "organizations_url": "https://api.github.com/users/holly-cummins/orgs", + "repos_url": "https://api.github.com/users/holly-cummins/repos", + "events_url": "https://api.github.com/users/holly-cummins/events{/privacy}", + "received_events_url": "https://api.github.com/users/holly-cummins/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 28125889, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjgxMjU4ODk=" + } +} \ No newline at end of file diff --git a/src/test/resources/workflow-unknown-contributor-approval-needed.json b/src/test/resources/workflow-unknown-contributor-approval-needed.json new file mode 100644 index 0000000..256d4df --- /dev/null +++ b/src/test/resources/workflow-unknown-contributor-approval-needed.json @@ -0,0 +1,357 @@ +{ + "action": "requested", + "workflow_run": { + "id": 2860832197, + "name": "CI", + "node_id": "WFR_kwLOHzCew86qhNXF", + "head_branch": "main", + "head_sha": "f2b91b5e80e1880f03a91fdde381bb24debf102c", + "path": ".github/workflows/blank.yml", + "run_number": 15, + "event": "pull_request", + "status": "completed", + "conclusion": "action_required", + "workflow_id": 32423768, + "check_suite_id": 7817139885, + "check_suite_node_id": "CS_kwDOHzCew88AAAAB0fAWrQ", + "url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197", + "html_url": "https://github.com/the-anonymous-one/bot-playground/actions/runs/2860832197", + "pull_requests": [], + "created_at": "2022-08-15T13:08:52Z", + "updated_at": "2022-08-15T13:08:52Z", + "actor": { + "login": "doctor-anonymous", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/doctor-anonymous", + "html_url": "https://github.com/doctor-anonymous", + "followers_url": "https://api.github.com/users/doctor-anonymous/followers", + "following_url": "https://api.github.com/users/doctor-anonymous/following{/other_user}", + "gists_url": "https://api.github.com/users/doctor-anonymous/gists{/gist_id}", + "starred_url": "https://api.github.com/users/doctor-anonymous/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/doctor-anonymous/subscriptions", + "organizations_url": "https://api.github.com/users/doctor-anonymous/orgs", + "repos_url": "https://api.github.com/users/doctor-anonymous/repos", + "events_url": "https://api.github.com/users/doctor-anonymous/events{/privacy}", + "received_events_url": "https://api.github.com/users/doctor-anonymous/received_events", + "type": "User", + "site_admin": false + }, + "run_attempt": 1, + "referenced_workflows": [], + "run_started_at": "2022-08-15T13:08:52Z", + "triggering_actor": { + "login": "doctor-anonymous", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/doctor-anonymous", + "html_url": "https://github.com/doctor-anonymous", + "followers_url": "https://api.github.com/users/doctor-anonymous/followers", + "following_url": "https://api.github.com/users/doctor-anonymous/following{/other_user}", + "gists_url": "https://api.github.com/users/doctor-anonymous/gists{/gist_id}", + "starred_url": "https://api.github.com/users/doctor-anonymous/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/doctor-anonymous/subscriptions", + "organizations_url": "https://api.github.com/users/doctor-anonymous/orgs", + "repos_url": "https://api.github.com/users/doctor-anonymous/repos", + "events_url": "https://api.github.com/users/doctor-anonymous/events{/privacy}", + "received_events_url": "https://api.github.com/users/doctor-anonymous/received_events", + "type": "User", + "site_admin": false + }, + "jobs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197/jobs", + "logs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197/logs", + "check_suite_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/check-suites/7817139885", + "artifacts_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197/artifacts", + "cancel_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197/cancel", + "rerun_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/runs/2860832197/rerun", + "previous_attempt_url": null, + "workflow_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/workflows/32423768", + "head_commit": { + "id": "f2b91b5e80e1880f03a91fdde381bb24debf102c", + "tree_id": "9de3ce570c143c11b5d3b6ad38ea02b95fd9437b", + "message": "Merge branch 'the-anonymous-one:main' into main", + "timestamp": "2022-08-15T12:42:33Z", + "author": { + "name": "doctor-anonymous", + "email": "111282252+doctor-anonymous@users.noreply.github.com" + }, + "committer": { + "name": "GitHub", + "email": "noreply@github.com" + } + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "the-anonymous-one/bot-playground", + "private": false, + "owner": { + "login": "the-anonymous-one", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/the-anonymous-one", + "html_url": "https://github.com/the-anonymous-one", + "followers_url": "https://api.github.com/users/the-anonymous-one/followers", + "following_url": "https://api.github.com/users/the-anonymous-one/following{/other_user}", + "gists_url": "https://api.github.com/users/the-anonymous-one/gists{/gist_id}", + "starred_url": "https://api.github.com/users/the-anonymous-one/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/the-anonymous-one/subscriptions", + "organizations_url": "https://api.github.com/users/the-anonymous-one/orgs", + "repos_url": "https://api.github.com/users/the-anonymous-one/repos", + "events_url": "https://api.github.com/users/the-anonymous-one/events{/privacy}", + "received_events_url": "https://api.github.com/users/the-anonymous-one/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/the-anonymous-one/bot-playground", + "description": "A playground repository used for testing https://github.com/the-anonymous-one/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/the-anonymous-one/bot-playground", + "forks_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/forks", + "keys_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/events", + "assignees_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/merges", + "archive_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/deployments" + }, + "head_repository": { + "id": 524953530, + "node_id": "R_kgDOH0onug", + "name": "bot-playground", + "full_name": "doctor-anonymous/bot-playground", + "private": false, + "owner": { + "login": "doctor-anonymous", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/doctor-anonymous", + "html_url": "https://github.com/doctor-anonymous", + "followers_url": "https://api.github.com/users/doctor-anonymous/followers", + "following_url": "https://api.github.com/users/doctor-anonymous/following{/other_user}", + "gists_url": "https://api.github.com/users/doctor-anonymous/gists{/gist_id}", + "starred_url": "https://api.github.com/users/doctor-anonymous/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/doctor-anonymous/subscriptions", + "organizations_url": "https://api.github.com/users/doctor-anonymous/orgs", + "repos_url": "https://api.github.com/users/doctor-anonymous/repos", + "events_url": "https://api.github.com/users/doctor-anonymous/events{/privacy}", + "received_events_url": "https://api.github.com/users/doctor-anonymous/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/doctor-anonymous/bot-playground", + "description": "A playground repository used for testing https://github.com/the-anonymous-one/quarkus-github-bot", + "fork": true, + "url": "https://api.github.com/repos/doctor-anonymous/bot-playground", + "forks_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/forks", + "keys_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/events", + "assignees_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/merges", + "archive_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/doctor-anonymous/bot-playground/deployments" + } + }, + "workflow": { + "id": 32423768, + "node_id": "W_kwDOHzCew84B7r9Y", + "name": "CI", + "path": ".github/workflows/blank.yml", + "state": "active", + "created_at": "2022-08-15T11:10:23.000Z", + "updated_at": "2022-08-15T11:10:44.000Z", + "url": "https://api.github.com/repos/the-anonymous-one/bot-playground/actions/workflows/32423768", + "html_url": "https://github.com/the-anonymous-one/bot-playground/blob/main/.github/workflows/blank.yml", + "badge_url": "https://github.com/the-anonymous-one/bot-playground/workflows/CI/badge.svg" + }, + "repository": { + "id": 523280067, + "node_id": "R_kgDOHzCeww", + "name": "bot-playground", + "full_name": "the-anonymous-one/bot-playground", + "private": false, + "owner": { + "login": "the-anonymous-one", + "id": 11509290, + "node_id": "MDQ6VXNlcjExNTA5Mjkw", + "avatar_url": "https://avatars.githubusercontent.com/u/11509290?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/the-anonymous-one", + "html_url": "https://github.com/the-anonymous-one", + "followers_url": "https://api.github.com/users/the-anonymous-one/followers", + "following_url": "https://api.github.com/users/the-anonymous-one/following{/other_user}", + "gists_url": "https://api.github.com/users/the-anonymous-one/gists{/gist_id}", + "starred_url": "https://api.github.com/users/the-anonymous-one/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/the-anonymous-one/subscriptions", + "organizations_url": "https://api.github.com/users/the-anonymous-one/orgs", + "repos_url": "https://api.github.com/users/the-anonymous-one/repos", + "events_url": "https://api.github.com/users/the-anonymous-one/events{/privacy}", + "received_events_url": "https://api.github.com/users/the-anonymous-one/received_events", + "type": "User", + "site_admin": false + }, + "html_url": "https://github.com/the-anonymous-one/bot-playground", + "description": "A playground repository used for testing https://github.com/the-anonymous-one/quarkus-github-bot", + "fork": false, + "url": "https://api.github.com/repos/the-anonymous-one/bot-playground", + "forks_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/forks", + "keys_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/keys{/key_id}", + "collaborators_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/collaborators{/collaborator}", + "teams_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/teams", + "hooks_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/hooks", + "issue_events_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues/events{/number}", + "events_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/events", + "assignees_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/assignees{/user}", + "branches_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/branches{/branch}", + "tags_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/tags", + "blobs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/blobs{/sha}", + "git_tags_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/tags{/sha}", + "git_refs_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/refs{/sha}", + "trees_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/trees{/sha}", + "statuses_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/statuses/{sha}", + "languages_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/languages", + "stargazers_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/stargazers", + "contributors_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/contributors", + "subscribers_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/subscribers", + "subscription_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/subscription", + "commits_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/commits{/sha}", + "git_commits_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/git/commits{/sha}", + "comments_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/comments{/number}", + "issue_comment_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues/comments{/number}", + "contents_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/contents/{+path}", + "compare_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/compare/{base}...{head}", + "merges_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/merges", + "archive_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/{archive_format}{/ref}", + "downloads_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/downloads", + "issues_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/issues{/number}", + "pulls_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/pulls{/number}", + "milestones_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/milestones{/number}", + "notifications_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/notifications{?since,all,participating}", + "labels_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/labels{/name}", + "releases_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/releases{/id}", + "deployments_url": "https://api.github.com/repos/the-anonymous-one/bot-playground/deployments", + "created_at": "2022-08-10T09:27:22Z", + "updated_at": "2022-08-10T09:27:22Z", + "pushed_at": "2022-08-15T13:08:50Z", + "git_url": "git://github.com/the-anonymous-one/bot-playground.git", + "ssh_url": "git@github.com:the-anonymous-one/bot-playground.git", + "clone_url": "https://github.com/the-anonymous-one/bot-playground.git", + "svn_url": "https://github.com/the-anonymous-one/bot-playground", + "homepage": null, + "size": 5, + "stargazers_count": 0, + "watchers_count": 0, + "language": null, + "has_issues": true, + "has_projects": true, + "has_downloads": true, + "has_wiki": true, + "has_pages": false, + "forks_count": 1, + "mirror_url": null, + "archived": false, + "disabled": false, + "open_issues_count": 1, + "license": null, + "allow_forking": true, + "is_template": false, + "web_commit_signoff_required": false, + "topics": [], + "visibility": "public", + "forks": 1, + "open_issues": 1, + "watchers": 0, + "default_branch": "main" + }, + "sender": { + "login": "doctor-anonymous", + "id": 111282252, + "node_id": "U_kgDOBqIITA", + "avatar_url": "https://avatars.githubusercontent.com/u/111282252?v=4", + "gravatar_id": "", + "url": "https://api.github.com/users/doctor-anonymous", + "html_url": "https://github.com/doctor-anonymous", + "followers_url": "https://api.github.com/users/doctor-anonymous/followers", + "following_url": "https://api.github.com/users/doctor-anonymous/following{/other_user}", + "gists_url": "https://api.github.com/users/doctor-anonymous/gists{/gist_id}", + "starred_url": "https://api.github.com/users/doctor-anonymous/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/doctor-anonymous/subscriptions", + "organizations_url": "https://api.github.com/users/doctor-anonymous/orgs", + "repos_url": "https://api.github.com/users/doctor-anonymous/repos", + "events_url": "https://api.github.com/users/doctor-anonymous/events{/privacy}", + "received_events_url": "https://api.github.com/users/doctor-anonymous/received_events", + "type": "User", + "site_admin": false + }, + "installation": { + "id": 28125889, + "node_id": "MDIzOkludGVncmF0aW9uSW5zdGFsbGF0aW9uMjgxMjU4ODk=" + } +} \ No newline at end of file From bfc6d31bd85320e45e92d64a2862e6b82713e501 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 24 Aug 2022 16:06:38 +0100 Subject: [PATCH 02/10] Update src/main/java/io/quarkus/bot/ApproveWorkflow.java Co-authored-by: Guillaume Smet --- src/main/java/io/quarkus/bot/ApproveWorkflow.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java index d62b49a..cea99cb 100644 --- a/src/main/java/io/quarkus/bot/ApproveWorkflow.java +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -191,7 +191,7 @@ private GHRepositoryStatistics.ContributorStats getStatsForUser(GHEventPayload.W } } } catch (InterruptedException | IOException e) { - LOG.info("Could not get contributors" + e); + LOG.error("Could not get repository contributor statistics", e); } } return null; From 7a7e2dff4a894bfe0b455d274e54e831e82e85f9 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Thu, 25 Aug 2022 12:06:28 +0100 Subject: [PATCH 03/10] Cache contributor stats --- .../java/io/quarkus/bot/ApproveWorkflow.java | 54 ++++++++++++++----- src/main/resources/application.properties | 3 ++ .../java/io/quarkus/bot/it/MockHelper.java | 9 ++++ .../quarkus/bot/it/WorkflowApprovalTest.java | 6 ++- 4 files changed, 57 insertions(+), 15 deletions(-) diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java index cea99cb..eb9bab6 100644 --- a/src/main/java/io/quarkus/bot/ApproveWorkflow.java +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -6,15 +6,22 @@ import io.quarkus.bot.config.QuarkusGitHubBotConfig; import io.quarkus.bot.config.QuarkusGitHubBotConfigFile; import io.quarkus.bot.util.PullRequestFilesMatcher; +import io.quarkus.cache.CacheKey; +import io.quarkus.cache.CacheResult; import org.jboss.logging.Logger; import org.kohsuke.github.GHEventPayload; import org.kohsuke.github.GHPullRequest; +import org.kohsuke.github.GHRepository; import org.kohsuke.github.GHRepositoryStatistics; import org.kohsuke.github.GHWorkflowRun; import org.kohsuke.github.PagedIterable; import javax.inject.Inject; import java.io.IOException; +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; class ApproveWorkflow { @@ -179,20 +186,39 @@ private boolean matchRuleForUser(GHRepositoryStatistics.ContributorStats stats, } private GHRepositoryStatistics.ContributorStats getStatsForUser(GHEventPayload.WorkflowRun workflowPayload) { - if (workflowPayload.getSender().getLogin() != null) { - try { - GHRepositoryStatistics statistics = workflowPayload.getRepository().getStatistics(); - if (statistics != null) { - PagedIterable contributors = statistics.getContributorStats(); - for (GHRepositoryStatistics.ContributorStats contributor : contributors) { - if (workflowPayload.getSender().getLogin().equals(contributor.getAuthor().getLogin())) { - return contributor; - } - } - } - } catch (InterruptedException | IOException e) { - LOG.error("Could not get repository contributor statistics", e); - } + + String login = workflowPayload.getSender().getLogin(); + if (login != null) { + return getStatsForUser(workflowPayload.getRepository(), login); + } + return null; + } + + @CacheResult(cacheName = "contributor-cache") + GHRepositoryStatistics.ContributorStats getStatsForUser(GHRepository repository, @CacheKey String login) { + try { + Map contributorStats = getContributorStats(repository); + return contributorStats.get(login); + } catch (IOException | InterruptedException e) { + LOG.error("Could not get repository contributor statistics", e); + } + + return null; + } + + @CacheResult(cacheName = "stats-cache") + Map getContributorStats(GHRepository repository) + throws IOException, InterruptedException { + GHRepositoryStatistics statistics = repository.getStatistics(); + if (statistics != null) { + PagedIterable contributors = statistics.getContributorStats(); + // Pull the iterable into a list object to force the traversal of the entire list, + // since then we get a fully-warmed cache on our first request + // Convert to a map for convenience of retrieval + List statsList = contributors.toList(); + return statsList.stream() + .collect( + Collectors.toMap(contributorStats -> contributorStats.getAuthor().getLogin(), Function.identity())); } return null; } diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 894e37f..69f11b9 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -10,6 +10,9 @@ quarkus.cache.caffeine."PushToProject.getStatusFieldValue".initial-capacity=10 quarkus.cache.caffeine."PushToProject.getStatusFieldValue".maximum-size=100 quarkus.cache.caffeine."PushToProject.getStatusFieldValue".expire-after-write=2H +quarkus.cache.caffeine."contributor-cache".expire-after-write=P2D +quarkus.cache.caffeine."stats-cache".expire-after-write=P2D + quarkus.openshift.labels."app"=quarkus-bot quarkus.openshift.annotations."kubernetes.io/tls-acme"=true quarkus.openshift.env.vars.QUARKUS_GITHUB_APP_APP_ID=90234 diff --git a/src/test/java/io/quarkus/bot/it/MockHelper.java b/src/test/java/io/quarkus/bot/it/MockHelper.java index c28b8df..9fcc378 100644 --- a/src/test/java/io/quarkus/bot/it/MockHelper.java +++ b/src/test/java/io/quarkus/bot/it/MockHelper.java @@ -5,6 +5,7 @@ import org.kohsuke.github.PagedIterable; import org.kohsuke.github.PagedIterator; +import java.io.IOException; import java.util.Iterator; import java.util.List; @@ -24,11 +25,19 @@ public static GHPullRequestFileDetail mockGHPullRequestFileDetail(String filenam @SuppressWarnings("unchecked") public static PagedIterable mockPagedIterable(T... contentMocks) { PagedIterable iterableMock = mock(PagedIterable.class); + try { + lenient().when(iterableMock.toList()).thenAnswer(ignored2 -> List.of(contentMocks)); + } catch (IOException e) { + // This should never happen + // That's a classic unwise comment, but it's a mock, so surely we're safe? :) + throw new RuntimeException(e); + } lenient().when(iterableMock.iterator()).thenAnswer(ignored -> { PagedIterator iteratorMock = mock(PagedIterator.class); Iterator actualIterator = List.of(contentMocks).iterator(); when(iteratorMock.next()).thenAnswer(ignored2 -> actualIterator.next()); lenient().when(iteratorMock.hasNext()).thenAnswer(ignored2 -> actualIterator.hasNext()); + return iteratorMock; }); return iterableMock; diff --git a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java index 8dda814..d0a32db 100644 --- a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java +++ b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java @@ -3,6 +3,7 @@ import io.quarkiverse.githubapp.testing.GitHubAppTest; import io.quarkiverse.githubapp.testing.dsl.GitHubMockSetupContext; import io.quarkiverse.githubapp.testing.dsl.GitHubMockVerificationContext; +import io.quarkus.cache.CacheInvalidateAll; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -36,7 +37,10 @@ @ExtendWith(MockitoExtension.class) public class WorkflowApprovalTest { - private void setupMockQueriesAndCommits(GitHubMockSetupContext mocks) { + // We may change our user stats in individual tests, so wipe caches before each test + @CacheInvalidateAll(cacheName = "contributor-cache") + @CacheInvalidateAll(cacheName = "stats-cache") + void setupMockQueriesAndCommits(GitHubMockSetupContext mocks) { GHRepository repoMock = mocks.repository("bot-playground"); GHPullRequestQueryBuilder workflowRunQueryBuilderMock = mock(GHPullRequestQueryBuilder.class, withSettings().defaultAnswer(Answers.RETURNS_SELF)); From 0ca03c744abd305f5a4c257dba4dfc89386bc98c Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Thu, 25 Aug 2022 15:52:55 +0100 Subject: [PATCH 04/10] Simplify file matching to not distinguish between files and directories --- README.adoc | 12 ++-- .../java/io/quarkus/bot/ApproveWorkflow.java | 30 +-------- .../bot/util/PullRequestFilesMatcher.java | 61 ++++++------------- src/main/java/io/quarkus/bot/util/Triage.java | 2 +- .../quarkus/bot/it/WorkflowApprovalTest.java | 20 +++--- 5 files changed, 38 insertions(+), 87 deletions(-) diff --git a/README.adoc b/README.adoc index 9b7235f..f19b7d3 100644 --- a/README.adoc +++ b/README.adoc @@ -210,16 +210,14 @@ workflows: - allow: directories: - ./src - - ./documentation - files: - - README.md + - ./doc* + - "**/README.md" users: minContributions: 5 unless: directories: - - ./github - files: - - bad.xml + - ./.github + - "**/pom.xml" ---- Workflows will be allowed if they meet one of the rules in the `allow` section, @@ -227,7 +225,7 @@ unless one of the rules in the `unless` section is triggered. In the example above, any file called `README.md` would be allowed, except for `./github/README.md`. Users who had made at least 5 commits to the repository would be allowed to make any changes, -except to `bad.xml` and any files in `.github`. Other users could make changes to `./src` or `./documentation`. +except to a `pom.xml` or any files in `.github`. Other users could make changes to `./src` or directories whose name started with `./doc`. If the rule is triggered, the following actions will be executed: diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java index eb9bab6..ffc1874 100644 --- a/src/main/java/io/quarkus/bot/ApproveWorkflow.java +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -132,38 +132,12 @@ public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, return false; } - boolean matches = false; - - PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); - if (matchDirectories(prMatcher, rule)) { - matches = true; - } else if (matchFiles(prMatcher, rule)) { - matches = true; - } - - return matches; - } - - private static boolean matchDirectories(PullRequestFilesMatcher prMatcher, - QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { if (rule.directories == null || rule.directories.isEmpty()) { return false; } - if (prMatcher.changedFilesMatchDirectory(rule.directories)) { - return true; - } - return false; - } - private static boolean matchFiles(PullRequestFilesMatcher prMatcher, - QuarkusGitHubBotConfigFile.WorkflowApprovalCondition rule) { - if (rule.files == null || rule.files.isEmpty()) { - return false; - } - if (prMatcher.changedFilesMatchFile(rule.files)) { - return true; - } - return false; + PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); + return prMatcher.changedFilesMatch(rule.directories); } private boolean matchRuleForUser(GHRepositoryStatistics.ContributorStats stats, diff --git a/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java index abfac80..3b0706f 100644 --- a/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java +++ b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java @@ -2,6 +2,7 @@ import com.hrakaroo.glob.GlobPattern; import com.hrakaroo.glob.MatchingEngine; +import io.quarkus.cache.CacheResult; import org.jboss.logging.Logger; import org.kohsuke.github.GHPullRequest; import org.kohsuke.github.GHPullRequestFileDetail; @@ -19,22 +20,25 @@ public PullRequestFilesMatcher(GHPullRequest pullRequest) { this.pullRequest = pullRequest; } - public boolean changedFilesMatchDirectory(Collection directories) { - for (GHPullRequestFileDetail changedFile : pullRequest.listFiles()) { - for (String directory : directories) { + public boolean changedFilesMatch(Collection filenamePatterns) { + PagedIterable prFiles = pullRequest.listFiles(); + if (prFiles != null) { + for (GHPullRequestFileDetail changedFile : prFiles) { + for (String filenamePattern : filenamePatterns) { - if (!directory.contains("*")) { - if (changedFile.getFilename().startsWith(directory)) { - return true; - } - } else { - try { - MatchingEngine matchingEngine = GlobPattern.compile(directory); - if (matchingEngine.matches(changedFile.getFilename())) { + if (!filenamePattern.contains("*")) { + if (changedFile.getFilename().startsWith(filenamePattern)) { return true; } - } catch (Exception e) { - LOG.error("Error evaluating glob expression: " + directory, e); + } else { + try { + MatchingEngine matchingEngine = compileGlob(filenamePattern); + if (matchingEngine.matches(changedFile.getFilename())) { + return true; + } + } catch (Exception e) { + LOG.error("Error evaluating glob expression: " + filenamePattern, e); + } } } } @@ -42,33 +46,8 @@ public boolean changedFilesMatchDirectory(Collection directories) { return false; } - public boolean changedFilesMatchFile(Collection files) { - - PagedIterable prFiles = pullRequest.listFiles(); - - if (prFiles == null || files == null) { - return false; - } - - for (GHPullRequestFileDetail changedFile : prFiles) { - for (String file : files) { - - if (!file.contains("*")) { - if (changedFile.getFilename().endsWith(file)) { - return true; - } - } else { - try { - MatchingEngine matchingEngine = GlobPattern.compile(file); - if (matchingEngine.matches(changedFile.getFilename())) { - return true; - } - } catch (Exception e) { - LOG.error("Error evaluating glob expression: " + file, e); - } - } - } - } - return false; + @CacheResult(cacheName = "glob-cache") + MatchingEngine compileGlob(String filenamePattern) { + return GlobPattern.compile(filenamePattern); } } diff --git a/src/main/java/io/quarkus/bot/util/Triage.java b/src/main/java/io/quarkus/bot/util/Triage.java index c816e7e..b1848d8 100644 --- a/src/main/java/io/quarkus/bot/util/Triage.java +++ b/src/main/java/io/quarkus/bot/util/Triage.java @@ -83,7 +83,7 @@ public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, Triag } PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); - if (prMatcher.changedFilesMatchDirectory(rule.directories)) { + if (prMatcher.changedFilesMatch(rule.directories)) { return true; } diff --git a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java index d0a32db..68e9dc8 100644 --- a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java +++ b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java @@ -213,8 +213,8 @@ void changeToAnAllowedFileShouldBeApproved() throws Exception { workflows: rules: - allow: - files: - - pom.xml + directories: + - "**/pom.xml" """); setupMockQueriesAndCommits(mocks); PagedIterable paths = MockHelper @@ -242,8 +242,8 @@ void changeToAFileInAnAllowedDirectoryWithAnIrrelevantUnlessShouldBeAllowed() th directories: - ./src unless: - files: - - bad.xml + directories: + - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); PagedIterable paths = MockHelper @@ -271,8 +271,8 @@ void changeToAnUnlessedFileInAnAllowedDirectoryShouldBeSoftRejected() throws Exc directories: - ./src unless: - files: - - bad.xml + directories: + - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); PagedIterable paths = MockHelper @@ -375,8 +375,8 @@ void changeFromAnEstablishedUserToADangerousFileShouldBeSoftRejected() throws Ex users: minContributions: 5 unless: - files: - - bad.xml + directories: + - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); setupMockUsers(mocks); @@ -405,8 +405,8 @@ void workflowIsPreApprovedShouldDoNothing() throws Exception { directories: - ./src unless: - files: - - bad.xml + directories: + - "**/bad.xml" """)) .when().payloadFromClasspath("/workflow-from-committer.json") .event(GHEvent.WORKFLOW_RUN) From 44b6c3c056c587ad43075bc0cef967f0ea19c391 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 6 Sep 2022 16:27:43 +0200 Subject: [PATCH 05/10] Add cache configuration for glob-cache --- src/main/resources/application.properties | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 69f11b9..3b0c4d7 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -6,6 +6,8 @@ quarkus.live-reload.instrumentation=false quarkus.qute.suffixes=md quarkus.qute.content-types."md"=text/markdown +quarkus.cache.caffeine."glob-cache".maximum-size=200 + quarkus.cache.caffeine."PushToProject.getStatusFieldValue".initial-capacity=10 quarkus.cache.caffeine."PushToProject.getStatusFieldValue".maximum-size=100 quarkus.cache.caffeine."PushToProject.getStatusFieldValue".expire-after-write=2H From ac5c84e894ceca16de29cd4d32b2d67f46989729 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 6 Sep 2022 16:28:05 +0200 Subject: [PATCH 06/10] Rename directories to files to avoid confusion I will do it for the existing triage features at some point but it needs to be properly coordinated. --- README.adoc | 4 +-- .../java/io/quarkus/bot/ApproveWorkflow.java | 6 ++-- .../config/QuarkusGitHubBotConfigFile.java | 1 - .../quarkus/bot/it/WorkflowApprovalTest.java | 30 +++++++++---------- 4 files changed, 19 insertions(+), 22 deletions(-) diff --git a/README.adoc b/README.adoc index f19b7d3..e7895cf 100644 --- a/README.adoc +++ b/README.adoc @@ -208,14 +208,14 @@ features: [ APPROVE_WORKFLOWS ] workflows: rules: - allow: - directories: + files: - ./src - ./doc* - "**/README.md" users: minContributions: 5 unless: - directories: + files: - ./.github - "**/pom.xml" ---- diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java index ffc1874..ed3a022 100644 --- a/src/main/java/io/quarkus/bot/ApproveWorkflow.java +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -60,9 +60,7 @@ void evaluatePullRequest( if (approval.isApproved()) { processApproval(workflowRun); - } - } private void processApproval(GHWorkflowRun workflowRun) throws IOException { @@ -132,12 +130,12 @@ public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, return false; } - if (rule.directories == null || rule.directories.isEmpty()) { + if (rule.files == null || rule.files.isEmpty()) { return false; } PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); - return prMatcher.changedFilesMatch(rule.directories); + return prMatcher.changedFilesMatch(rule.files); } private boolean matchRuleForUser(GHRepositoryStatistics.ContributorStats stats, diff --git a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java index d783c31..a9bd969 100644 --- a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java +++ b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java @@ -120,7 +120,6 @@ public static class WorkflowApprovalRule { } public static class WorkflowApprovalCondition { - public List directories; public List files; public UserRule users; diff --git a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java index 68e9dc8..9300777 100644 --- a/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java +++ b/src/test/java/io/quarkus/bot/it/WorkflowApprovalTest.java @@ -77,7 +77,7 @@ void changeToAnAllowedDirectoryShouldBeApproved() throws Exception { workflows: rules: - allow: - directories: + files: - ./src """); setupMockQueriesAndCommits(mocks); @@ -103,7 +103,7 @@ void changeToAWildcardedDirectoryShouldBeApproved() throws Exception { workflows: rules: - allow: - directories: + files: - "*" """); setupMockQueriesAndCommits(mocks); @@ -129,7 +129,7 @@ void changeToADirectoryWithNoRulesShouldBeSoftRejected() throws Exception { workflows: rules: - allow: - directories: + files: - ./src """); setupMockQueriesAndCommits(mocks); @@ -155,10 +155,10 @@ void changeToAnAllowedAndUnlessedDirectoryShouldBeSoftRejected() throws Exceptio workflows: rules: - allow: - directories: + files: - "*" unless: - directories: + files: - ./github """); setupMockQueriesAndCommits(mocks); @@ -184,10 +184,10 @@ void changeToAnAllowedDirectoryWithAnIrrelevantUnlessedDirectoryShouldBeAccepted workflows: rules: - allow: - directories: + files: - "*" unless: - directories: + files: - ./github """); setupMockQueriesAndCommits(mocks); @@ -213,7 +213,7 @@ void changeToAnAllowedFileShouldBeApproved() throws Exception { workflows: rules: - allow: - directories: + files: - "**/pom.xml" """); setupMockQueriesAndCommits(mocks); @@ -239,10 +239,10 @@ void changeToAFileInAnAllowedDirectoryWithAnIrrelevantUnlessShouldBeAllowed() th workflows: rules: - allow: - directories: + files: - ./src unless: - directories: + files: - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); @@ -268,10 +268,10 @@ void changeToAnUnlessedFileInAnAllowedDirectoryShouldBeSoftRejected() throws Exc workflows: rules: - allow: - directories: + files: - ./src unless: - directories: + files: - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); @@ -375,7 +375,7 @@ void changeFromAnEstablishedUserToADangerousFileShouldBeSoftRejected() throws Ex users: minContributions: 5 unless: - directories: + files: - "**/bad.xml" """); setupMockQueriesAndCommits(mocks); @@ -402,10 +402,10 @@ void workflowIsPreApprovedShouldDoNothing() throws Exception { workflows: rules: - allow: - directories: + files: - ./src unless: - directories: + files: - "**/bad.xml" """)) .when().payloadFromClasspath("/workflow-from-committer.json") From 422f8fa02973b70087739050a42013d38960ca6b Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 6 Sep 2022 16:32:13 +0200 Subject: [PATCH 07/10] Initialize files rules consistently --- .../io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java index a9bd969..4cecdaa 100644 --- a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java +++ b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java @@ -120,7 +120,9 @@ public static class WorkflowApprovalRule { } public static class WorkflowApprovalCondition { - public List files; + @JsonDeserialize(as = TreeSet.class) + public Set files = new TreeSet<>(); + public UserRule users; } From 40467f48a90a7057117030a14249d2aa91330dc5 Mon Sep 17 00:00:00 2001 From: Guillaume Smet Date: Tue, 6 Sep 2022 16:37:14 +0200 Subject: [PATCH 08/10] Deprecate directories config for triaging in favor of files --- README.adoc | 10 +++++----- .../quarkus/bot/config/QuarkusGitHubBotConfigFile.java | 7 +++++++ .../io/quarkus/bot/util/PullRequestFilesMatcher.java | 4 ++++ src/main/java/io/quarkus/bot/util/Triage.java | 5 ++++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/README.adoc b/README.adoc index e7895cf..d126a86 100644 --- a/README.adoc +++ b/README.adoc @@ -31,13 +31,13 @@ triage: - labels: [area/amazon-lambda] title: "lambda" notify: [patriot1burke, matejvasek] - directories: + files: - extensions/amazon-lambda - integration-tests/amazon-lambda - labels: [area/persistence] title: "db2" notify: [aguibert] - directories: + files: - extensions/reactive-db2-client/ - extensions/jdbc/jdbc-db2/ ---- @@ -88,9 +88,9 @@ There are a few differences though as it doesn't behave in the exact same way. For pull requests, each rule can be triggered by: -* `directories` - if any file in the commits of the pull requests match, trigger the rule. This is not a regexp (it uses `startsWith`) but glob type expression are supported too `extensions/test/**`. +* `files` - if any file in the commits of the pull requests match, trigger the rule. This is not a regexp (it uses `startsWith`) but glob type expression are supported too `extensions/test/**`. -If no rule is triggered based on directories, or if rules are triggered but they all specify `allowSecondPass: true`, +If no rule is triggered based on files, or if rules are triggered but they all specify `allowSecondPass: true`, a second pass will be executed; in that second pass, rules can be triggered by: * `title` - if the title matches this regular expression (case insensitively), trigger the rule @@ -113,7 +113,7 @@ triage: title: "lambda" notify: [patriot1burke, matejvasek] notifyInPullRequest: true - directories: + files: - extensions/amazon-lambda - integration-tests/amazon-lambda ---- diff --git a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java index 4cecdaa..28dcd8d 100644 --- a/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java +++ b/src/main/java/io/quarkus/bot/config/QuarkusGitHubBotConfigFile.java @@ -42,9 +42,16 @@ public static class TriageRule { public String expression; + /** + * @deprecated use files instead + */ @JsonDeserialize(as = TreeSet.class) + @Deprecated(forRemoval = true) public Set directories = new TreeSet<>(); + @JsonDeserialize(as = TreeSet.class) + public Set files = new TreeSet<>(); + @JsonDeserialize(as = TreeSet.class) public Set labels = new TreeSet<>(); diff --git a/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java index 3b0706f..ee6f2c3 100644 --- a/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java +++ b/src/main/java/io/quarkus/bot/util/PullRequestFilesMatcher.java @@ -21,6 +21,10 @@ public PullRequestFilesMatcher(GHPullRequest pullRequest) { } public boolean changedFilesMatch(Collection filenamePatterns) { + if (filenamePatterns.isEmpty()) { + return false; + } + PagedIterable prFiles = pullRequest.listFiles(); if (prFiles != null) { for (GHPullRequestFileDetail changedFile : prFiles) { diff --git a/src/main/java/io/quarkus/bot/util/Triage.java b/src/main/java/io/quarkus/bot/util/Triage.java index b1848d8..e9612f9 100644 --- a/src/main/java/io/quarkus/bot/util/Triage.java +++ b/src/main/java/io/quarkus/bot/util/Triage.java @@ -78,11 +78,14 @@ public static boolean matchRuleFromDescription(String title, String body, Triage public static boolean matchRuleFromChangedFiles(GHPullRequest pullRequest, TriageRule rule) { // for now, we only use the files but we could also use the other rules at some point - if (rule.directories.isEmpty()) { + if (rule.directories.isEmpty() && rule.files.isEmpty()) { return false; } PullRequestFilesMatcher prMatcher = new PullRequestFilesMatcher(pullRequest); + if (prMatcher.changedFilesMatch(rule.files)) { + return true; + } if (prMatcher.changedFilesMatch(rule.directories)) { return true; } From 174362202e095f94b62e462b6035b42be2b6e2f0 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 7 Sep 2022 17:06:40 +0100 Subject: [PATCH 09/10] Narrow scope of listened events, and handle NPEs (a little bit) --- src/main/java/io/quarkus/bot/ApproveWorkflow.java | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/main/java/io/quarkus/bot/ApproveWorkflow.java b/src/main/java/io/quarkus/bot/ApproveWorkflow.java index ed3a022..82ea978 100644 --- a/src/main/java/io/quarkus/bot/ApproveWorkflow.java +++ b/src/main/java/io/quarkus/bot/ApproveWorkflow.java @@ -31,7 +31,7 @@ class ApproveWorkflow { QuarkusGitHubBotConfig quarkusBotConfig; void evaluatePullRequest( - @WorkflowRun GHEventPayload.WorkflowRun workflowPayload, + @WorkflowRun.Requested GHEventPayload.WorkflowRun workflowPayload, @ConfigFile("quarkus-github-bot.yml") QuarkusGitHubBotConfigFile quarkusBotConfigFile) throws IOException { if (!Feature.APPROVE_WORKFLOWS.isEnabled(quarkusBotConfigFile)) { return; @@ -171,13 +171,16 @@ GHRepositoryStatistics.ContributorStats getStatsForUser(GHRepository repository, try { Map contributorStats = getContributorStats(repository); return contributorStats.get(login); - } catch (IOException | InterruptedException e) { + } catch (IOException | InterruptedException | NullPointerException e) { + // We sometimes see an NPE from PagedIterator, if a fetch does not complete properly and leaves the object in an inconsistent state + // Catching these errors allows the null result for this contributor to be cached, which is ok LOG.error("Could not get repository contributor statistics", e); } return null; } + // We throw errors at this level to force the cache to retry and populate itself on the next request @CacheResult(cacheName = "stats-cache") Map getContributorStats(GHRepository repository) throws IOException, InterruptedException { From 8792ff236456e2cc008748dc3f55d0f453d4d483 Mon Sep 17 00:00:00 2001 From: Holly Cummins Date: Wed, 7 Sep 2022 17:07:10 +0100 Subject: [PATCH 10/10] Correct whitespace in docs, because yaml --- README.adoc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.adoc b/README.adoc index d126a86..a8bd3fb 100644 --- a/README.adoc +++ b/README.adoc @@ -214,8 +214,8 @@ workflows: - "**/README.md" users: minContributions: 5 - unless: - files: + unless: + files: - ./.github - "**/pom.xml" ----