Skip to content

Commit 267b411

Browse files
authored
Merge pull request #654 from gsmet/support-no-installation
Support events not containing an installation
2 parents 8120f99 + 2cc8707 commit 267b411

File tree

21 files changed

+397
-35
lines changed

21 files changed

+397
-35
lines changed

deployment/src/main/java/io/quarkiverse/githubapp/deployment/GitHubAppProcessor.java

Lines changed: 37 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858

5959
import io.quarkiverse.githubapp.ConfigFile;
6060
import io.quarkiverse.githubapp.GitHubEvent;
61+
import io.quarkiverse.githubapp.TokenGitHubClients;
6162
import io.quarkiverse.githubapp.deployment.DispatchingConfiguration.EventAnnotation;
6263
import io.quarkiverse.githubapp.deployment.DispatchingConfiguration.EventAnnotationLiteral;
6364
import io.quarkiverse.githubapp.deployment.DispatchingConfiguration.EventDispatchingConfiguration;
@@ -106,6 +107,8 @@
106107
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
107108
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
108109
import io.quarkus.gizmo.AnnotatedElement;
110+
import io.quarkus.gizmo.AssignableResultHandle;
111+
import io.quarkus.gizmo.BranchResult;
109112
import io.quarkus.gizmo.BytecodeCreator;
110113
import io.quarkus.gizmo.CatchBlockCreator;
111114
import io.quarkus.gizmo.ClassCreator;
@@ -221,7 +224,8 @@ void additionalBeans(CombinedIndexBuildItem index, BuildProducer<AdditionalBeanB
221224
DefaultErrorHandler.class,
222225
GitHubFileDownloader.class,
223226
GitHubConfigFileProviderImpl.class,
224-
CheckedConfigProvider.class)
227+
CheckedConfigProvider.class,
228+
TokenGitHubClients.class)
225229
.setUnremovable();
226230

227231
for (ClassInfo errorHandler : index.getIndex().getAllKnownImplementors(GitHubAppDotNames.ERROR_HANDLER)) {
@@ -554,24 +558,45 @@ private static void generateDispatcher(ClassOutput beanClassOutput,
554558

555559
TryBlock tryBlock = dispatchMethodCreator.tryBlock();
556560

557-
ResultHandle gitHubRh = tryBlock.invokeVirtualMethod(
561+
// if the installation id is defined, we can push the installation client
562+
// if not, we have to use the very limited application client
563+
AssignableResultHandle gitHubRh = tryBlock.createVariable(GitHub.class);
564+
AssignableResultHandle gitHubGraphQLClientRh = tryBlock.createVariable(DynamicGraphQLClient.class);
565+
BranchResult testInstallationId = tryBlock.ifNotNull(installationIdRh);
566+
BytecodeCreator installationIdSet = testInstallationId.trueBranch();
567+
installationIdSet.assign(gitHubRh, installationIdSet.invokeVirtualMethod(
558568
MethodDescriptor.ofMethod(GitHubService.class, "getInstallationClient", GitHub.class, long.class),
559-
tryBlock.readInstanceField(
569+
installationIdSet.readInstanceField(
560570
FieldDescriptor.of(dispatcherClassCreator.getClassName(), GITHUB_SERVICE_FIELD, GitHubService.class),
561-
tryBlock.getThis()),
562-
installationIdRh);
563-
564-
ResultHandle gitHubGraphQLClientRh = tryBlock.loadNull();
565-
571+
installationIdSet.getThis()),
572+
installationIdRh));
566573
if (dispatchingConfiguration.requiresGraphQLClient()) {
567-
gitHubGraphQLClientRh = tryBlock.invokeVirtualMethod(
574+
installationIdSet.assign(gitHubGraphQLClientRh, installationIdSet.invokeVirtualMethod(
568575
MethodDescriptor.ofMethod(GitHubService.class, "getInstallationGraphQLClient", DynamicGraphQLClient.class,
569576
long.class),
570-
tryBlock.readInstanceField(
577+
installationIdSet.readInstanceField(
571578
FieldDescriptor.of(dispatcherClassCreator.getClassName(), GITHUB_SERVICE_FIELD,
572579
GitHubService.class),
573-
tryBlock.getThis()),
574-
installationIdRh);
580+
installationIdSet.getThis()),
581+
installationIdRh));
582+
} else {
583+
installationIdSet.assign(gitHubGraphQLClientRh, installationIdSet.loadNull());
584+
}
585+
BytecodeCreator installationIdNull = testInstallationId.falseBranch();
586+
installationIdNull.assign(gitHubRh, installationIdNull.invokeVirtualMethod(
587+
MethodDescriptor.ofMethod(GitHubService.class, "getTokenOrApplicationClient", GitHub.class),
588+
installationIdNull.readInstanceField(
589+
FieldDescriptor.of(dispatcherClassCreator.getClassName(), GITHUB_SERVICE_FIELD, GitHubService.class),
590+
installationIdNull.getThis())));
591+
if (dispatchingConfiguration.requiresGraphQLClient()) {
592+
installationIdNull.assign(gitHubGraphQLClientRh, installationIdNull.invokeVirtualMethod(
593+
MethodDescriptor.ofMethod(GitHubService.class, "getTokenGraphQLClientOrNull", DynamicGraphQLClient.class),
594+
installationIdNull.readInstanceField(
595+
FieldDescriptor.of(dispatcherClassCreator.getClassName(), GITHUB_SERVICE_FIELD,
596+
GitHubService.class),
597+
installationIdNull.getThis())));
598+
} else {
599+
installationIdNull.assign(gitHubGraphQLClientRh, installationIdNull.loadNull());
575600
}
576601

577602
for (EventDispatchingConfiguration eventDispatchingConfiguration : dispatchingConfiguration.getEventConfigurations()
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package io.quarkiverse.githubapp.deployment;
2+
3+
import jakarta.inject.Inject;
4+
5+
import org.jboss.shrinkwrap.api.ShrinkWrap;
6+
import org.jboss.shrinkwrap.api.spec.JavaArchive;
7+
import org.junit.jupiter.api.Test;
8+
import org.junit.jupiter.api.extension.RegisterExtension;
9+
import org.kohsuke.github.GHEventPayload;
10+
11+
import io.quarkiverse.githubapp.TokenGitHubClients;
12+
import io.quarkiverse.githubapp.event.Label;
13+
import io.quarkus.test.QuarkusUnitTest;
14+
15+
public class TokenGitHubClientsInjectionTest {
16+
17+
@RegisterExtension
18+
static final QuarkusUnitTest config = new QuarkusUnitTest()
19+
.setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)
20+
.addClass(ListeningClass.class).addClass(ListeningClass2.class))
21+
.withConfigurationResource("application-token.properties");
22+
23+
@Test
24+
public void testGitHubGraphQLClientInjection() {
25+
}
26+
27+
static class ListeningClass {
28+
29+
@Inject
30+
TokenGitHubClients tokenGitHubClients;
31+
32+
void createLabel(@Label.Created GHEventPayload.Label labelPayload) {
33+
}
34+
}
35+
36+
static class ListeningClass2 {
37+
38+
void createLabel(@Label.Created GHEventPayload.Label labelPayload, TokenGitHubClients tokenGitHubClients) {
39+
}
40+
}
41+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
quarkus.github-app.app-name=quarkus-github-app-test
2+
quarkus.github-app.personal-access-token=ghp_mytoken

docs/modules/ROOT/pages/developer-reference.adoc

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,41 @@ The injected `DynamicGraphQLClient` instance is authenticated as an installation
343343
`DynamicGraphQLClient` is a dynamic SmallRye GraphQL client.
344344
You can find more information about the SmallRye GraphQL client https://quarkus.io/guides/smallrye-graphql-client[here] and https://github.com/smallrye/smallrye-graphql[here].
345345

346-
== Configuration Reference
346+
== Webhook events
347347

348-
The Quarkus GitHub App extension exposes the following configuration properties:
348+
While Quarkus GitHub App was primarily designed as a tool to develop GitHub Apps,
349+
it is also possible to use it to handle your webhook requests.
349350

350-
include::includes/quarkus-github-app.adoc[]
351+
It will take care of all the ceremony of authenticating the requests and will save you a lot of boilerplate.
352+
353+
There are a few differences though:
354+
355+
- You will have to use `@RawEvent` to listen to the events and get the raw JSON of the payload.
356+
- Webhook requests don't provide an installation id so we can't initialize an installation GitHub client nor a GraphQL client.
357+
358+
The default `GitHub` REST client instance that can be injected is an application client and has so few permissions that it is not really useful.
359+
360+
While it could be a major inconvenience, we present a nice feature dedicated to this use case in the following section.
361+
362+
== Providing a personal access token
363+
364+
When using Quarkus GitHub App, in most cases, the REST and GraphQL clients provided by the installation are what you are looking for:
365+
they have the permissions allowed for this GitHub App and it should be enough to do what your GitHub App has been designed for.
366+
367+
However, there are corner cases where you might need a client with additional permissions,
368+
the most common one is when you deal with webhooks as presented in the previous section.
369+
370+
For this situation, you can define a personal access token by using the `quarkus.github-app.personal-access-token` configuration property.
371+
The personal access token provided in this property will be used to initialize:
372+
373+
- an authenticated `GitHub` REST client
374+
- an authenticated `DynamicGraphQL` GraphQL client
375+
376+
These clients will be automatically injected in your methods when injecting clients *if the payload doesn't provide an installation id*
377+
(if it does provide one, the regular installation clients will be injected).
378+
379+
It is also possible to directly obtain the clients authenticated with the personal access token by injecting the `TokenGitHubClients` CDI bean.
380+
It provides methods to get the authenticated REST and GraphQL clients.
351381

352382
== Credentials provider
353383

@@ -389,6 +419,12 @@ public class MyGitHubCustomizer implements GitHubCustomizer {
389419
}
390420
----
391421

422+
== Configuration Reference
423+
424+
The Quarkus GitHub App extension exposes the following configuration properties:
425+
426+
include::includes/quarkus-github-app.adoc[]
427+
392428
== Architecture Overview
393429

394430
image::architecture.png[Architecture]

docs/modules/ROOT/pages/includes/attributes.adoc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
:quarkus-version: 3.11.3
1+
:quarkus-version: 3.13.3
22
:quarkus-github-app-version: 2.6.0
33

44
:github-api-javadoc-root-url: https://github-api.kohsuke.org/apidocs/org/kohsuke/github

docs/modules/ROOT/pages/includes/quarkus-github-app.adoc

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,29 @@ endif::add-copy-button-to-env-var[]
241241
|`${quarkus.github-app.instance-endpoint}/graphql`
242242

243243

244+
a| [[quarkus-github-app_quarkus-github-app-personal-access-token]]`link:#quarkus-github-app_quarkus-github-app-personal-access-token[quarkus.github-app.personal-access-token]`
245+
246+
247+
[.description]
248+
--
249+
A personal access token for use with `TokenGitHubClients`.
250+
251+
For standard use cases, you will use the installation client which comes with the installation permissions. It can be injected directly in your method.
252+
253+
However, if your payload comes from a webhook and doesn't have an installation id, it's handy to be able to use a client authenticated with a personal access token as the application client permissions are very limited.
254+
255+
This token will be used to authenticate the clients provided by `TokenGitHubClients`.
256+
257+
ifdef::add-copy-button-to-env-var[]
258+
Environment variable: env_var_with_copy_button:+++QUARKUS_GITHUB_APP_PERSONAL_ACCESS_TOKEN+++[]
259+
endif::add-copy-button-to-env-var[]
260+
ifndef::add-copy-button-to-env-var[]
261+
Environment variable: `+++QUARKUS_GITHUB_APP_PERSONAL_ACCESS_TOKEN+++`
262+
endif::add-copy-button-to-env-var[]
263+
--|string
264+
|
265+
266+
244267
a| [[quarkus-github-app_quarkus-github-app-debug-payload-directory]]`link:#quarkus-github-app_quarkus-github-app-debug-payload-directory[quarkus.github-app.debug.payload-directory]`
245268

246269

integration-tests/app/src/main/java/io/quarkiverse/githubapp/it/app/RawEventListener.java

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,12 @@
66

77
import io.quarkiverse.githubapp.GitHubEvent;
88
import io.quarkiverse.githubapp.event.RawEvent;
9+
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
910

1011
public class RawEventListener {
1112

12-
void testRawEventListenedTo(@RawEvent(event = "issues", action = "opened") GitHubEvent gitHubEvent, GitHub gitHub)
13-
throws IOException {
13+
void testRawEventListenedTo(@RawEvent(event = "issues", action = "opened") GitHubEvent gitHubEvent, GitHub gitHub,
14+
DynamicGraphQLClient graphQLClient) throws IOException {
1415
assert gitHubEvent.getEvent().equals("issues");
1516
assert gitHubEvent.getAction().equals("opened");
1617

@@ -35,8 +36,8 @@ void testRawEventCatchAllAction(@RawEvent(event = "issues") GitHubEvent gitHubEv
3536
}
3637

3738
void testRawEventCatchAllEventAction(@RawEvent GitHubEvent gitHubEvent, GitHub gitHub) throws IOException {
38-
assert gitHubEvent.getEvent().equals("issues");
39-
assert gitHubEvent.getAction().equals("opened");
39+
assert gitHubEvent.getEvent() != null;
40+
assert gitHubEvent.getAction() != null;
4041

4142
gitHub.getRepository("test/test").getIssue(1).addLabels("testRawEventCatchAllEventAction");
4243
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package io.quarkiverse.githubapp.it.app;
2+
3+
import java.io.IOException;
4+
5+
import org.kohsuke.github.GitHub;
6+
7+
import io.quarkiverse.githubapp.GitHubEvent;
8+
import io.quarkiverse.githubapp.event.RawEvent;
9+
import io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClient;
10+
11+
public class RawEventListenerWithoutInstallation {
12+
13+
public static final String EVENT_TYPE = "sponsor";
14+
15+
void testRawEventListenerWithoutInstallation(@RawEvent(event = EVENT_TYPE, action = "sponsored") GitHubEvent gitHubEvent,
16+
GitHub gitHub,
17+
DynamicGraphQLClient graphQLClient) throws IOException {
18+
assert gitHubEvent.getEvent().equals(EVENT_TYPE);
19+
assert gitHubEvent.getAction().equals("sponsored");
20+
21+
assert graphQLClient != null;
22+
assert gitHub != null;
23+
24+
gitHub.getRepository("test/test").getIssue(1).addLabels("testRawEventListenerWithoutInstallation");
25+
}
26+
}

integration-tests/app/src/main/resources/application.properties

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,4 @@ E0/FAoGATJvuAfgy9uiKR7za7MigYVacE0u4aD1sF7v6D4AFqBOGquPQQhePSdz9\
2828
G/UUwySoo+AQ+rd2EPhyexjqXBhRGe+EDGFVFivaQzTT8/5bt/VddbTcw2IpmXYj\
2929
LW6V8BbcP5MRhd2JQSRh16nWwSQJ2BdpUZFwayEEQ6UcrMfqvA0=\
3030
-----END RSA PRIVATE KEY-----
31+
%test.quarkus.github-app.personal-access-token=ghp_mytoken
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package io.quarkiverse.githubapp.it.app;
2+
3+
import static io.quarkiverse.githubapp.testing.GitHubAppTesting.given;
4+
import static org.mockito.Mockito.verify;
5+
import static org.mockito.Mockito.when;
6+
7+
import java.io.IOException;
8+
9+
import org.junit.jupiter.api.Test;
10+
11+
import io.quarkiverse.githubapp.testing.GitHubAppTest;
12+
import io.quarkus.test.junit.QuarkusTest;
13+
14+
@QuarkusTest
15+
@GitHubAppTest
16+
public class RawEventWithoutInstallationTest {
17+
18+
@Test
19+
void testRawEventWithoutInstallation() throws IOException {
20+
given().github(mocks -> {
21+
when(mocks.repository("test/test").getIssue(1))
22+
.thenReturn(mocks.issue(1L));
23+
})
24+
.when().payloadFromClasspath("/event-without-installation.json")
25+
.rawEvent(RawEventListenerWithoutInstallation.EVENT_TYPE)
26+
.then().github(mocks -> {
27+
verify(mocks.issue(1))
28+
.addLabels("testRawEventListenerWithoutInstallation");
29+
verify(mocks.issue(1))
30+
.addLabels("testRawEventCatchAllEventAction");
31+
});
32+
}
33+
}

0 commit comments

Comments
 (0)