Skip to content

Commit

Permalink
Merge pull request #310 from gcw-it/pr_ghrepo
Browse files Browse the repository at this point in the history
Allow injecting fully set up GHRepository into action method
  • Loading branch information
gsmet authored Aug 23, 2024
2 parents c6dc64d + 8f13196 commit 1ced88f
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 36 deletions.
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:
- '*.adoc'
- '*.txt'
- '.all-contributorsrc'
workflow_dispatch:

jobs:
build:
Expand All @@ -39,3 +40,6 @@ jobs:

- name: Build with Maven
run: mvn -B formatter:validate clean install --file pom.xml -Dnative
env:
CURRENT_TOKEN: ${{ secrets.GITHUB_TOKEN }}
CURRENT_REPO: ${{ github.repository }}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import java.util.Set;

import org.jboss.jandex.DotName;
import org.kohsuke.github.GHRepository;
import org.kohsuke.github.GitHub;

import io.quarkiverse.githubaction.Action;
Expand All @@ -22,12 +23,14 @@ final class GitHubActionDotNames {
static final DotName CONFIG_FILE = DotName.createSimple(ConfigFile.class.getName());

static final DotName GITHUB = DotName.createSimple(GitHub.class.getName());
static final DotName REPOSITORY = DotName.createSimple(GHRepository.class.getName());
static final DotName DYNAMIC_GRAPHQL_CLIENT = DotName.createSimple(DynamicGraphQLClient.class.getName());
static final DotName CONTEXT = DotName.createSimple(Context.class.getName());
static final DotName INPUTS = DotName.createSimple(Inputs.class.getName());
static final DotName COMMANDS = DotName.createSimple(Commands.class.getName());

static final Set<DotName> INJECTABLE_TYPES = Set.of(GITHUB, DYNAMIC_GRAPHQL_CLIENT, CONTEXT, INPUTS, COMMANDS);
static final Set<DotName> INJECTABLE_TYPES = Set.of(
GITHUB, REPOSITORY, DYNAMIC_GRAPHQL_CLIENT, CONTEXT, INPUTS, COMMANDS);

private GitHubActionDotNames() {
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,6 @@
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
import io.quarkus.deployment.builditem.FeatureBuildItem;
import io.quarkus.deployment.builditem.GeneratedClassBuildItem;
import io.quarkus.deployment.builditem.LaunchModeBuildItem;
import io.quarkus.deployment.builditem.RunTimeConfigurationDefaultBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveClassBuildItem;
import io.quarkus.deployment.builditem.nativeimage.ReflectiveHierarchyBuildItem;
Expand Down Expand Up @@ -125,15 +124,13 @@ void silentStartup(BuildProducer<RunTimeConfigurationDefaultBuildItem> configura

@BuildStep
void registerForReflection(CombinedIndexBuildItem combinedIndex,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses,
BuildProducer<ReflectiveHierarchyBuildItem> reflectiveHierarchies) {
// Types used for config files
for (AnnotationInstance configFileAnnotationInstance : combinedIndex.getIndex().getAnnotations(CONFIG_FILE)) {
MethodParameterInfo methodParameter = configFileAnnotationInstance.target().asMethodParameter();
short parameterPosition = methodParameter.position();
Type parameterType = methodParameter.method().parameterTypes().get(parameterPosition);
reflectiveHierarchies.produce(new ReflectiveHierarchyBuildItem.Builder()
.type(parameterType)
reflectiveHierarchies.produce(ReflectiveHierarchyBuildItem.builder(parameterType)
.index(combinedIndex.getIndex())
.source(GitHubActionProcessor.class.getSimpleName() + " > " + methodParameter.method().declaringClass()
+ "#"
Expand All @@ -144,7 +141,7 @@ void registerForReflection(CombinedIndexBuildItem combinedIndex,

/**
* The bridge methods added for binary compatibility in the GitHub API are causing issues with Mockito
* and more specifically with Byte Buddy (see https://github.com/raphw/byte-buddy/issues/1162).
* and more specifically with Byte Buddy (see <a href="https://github.com/raphw/byte-buddy/issues/1162">...</a>).
* They don't bring much to the plate for new applications that are regularly updated so let's remove them altogether.
*/
@BuildStep
Expand Down Expand Up @@ -174,7 +171,7 @@ void removeCompatibilityBridgeMethodsFromGitHubApi(
}

@BuildStep
void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem launchMode,
void generateClasses(CombinedIndexBuildItem combinedIndex,
BuildProducer<AdditionalBeanBuildItem> additionalBeans,
BuildProducer<GeneratedBeanBuildItem> generatedBeans,
BuildProducer<GeneratedClassBuildItem> generatedClasses,
Expand All @@ -185,7 +182,7 @@ void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem l
// Add @Vetoed to all the user-defined event listening classes
annotationsTransformer
.produce(new AnnotationsTransformerBuildItem(new VetoUserDefinedEventListeningClassesAnnotationsTransformer(
allEventDefinitions.stream().map(d -> d.getAnnotation()).collect(Collectors.toSet()))));
allEventDefinitions.stream().map(EventDefinition::getAnnotation).collect(Collectors.toSet()))));

// Add the qualifiers as beans
String[] subscriberAnnotations = allEventDefinitions.stream().map(d -> d.getAnnotation().toString())
Expand All @@ -201,13 +198,12 @@ void generateClasses(CombinedIndexBuildItem combinedIndex, LaunchModeBuildItem l

ClassOutput beanClassOutput = new GeneratedBeanGizmoAdaptor(generatedBeans);
generatePayloadTypeResolver(beanClassOutput, reflectiveClasses, allEventDefinitions);
generateActionMain(beanClassOutput, combinedIndex, launchMode, dispatchingConfiguration, reflectiveClasses);
generateActionMain(beanClassOutput, dispatchingConfiguration, reflectiveClasses);
generateMultiplexers(beanClassOutput, dispatchingConfiguration, reflectiveClasses);
}

private static Collection<EventDefinition> getAllEventDefinitions(IndexView index) {
Collection<EventDefinition> mainEventDefinitions = new ArrayList<>();
Collection<EventDefinition> allEventDefinitions = new ArrayList<>();

for (AnnotationInstance eventInstance : index.getAnnotations(EVENT)) {
if (eventInstance.target().kind() == Kind.CLASS) {
Expand All @@ -218,7 +214,7 @@ private static Collection<EventDefinition> getAllEventDefinitions(IndexView inde
}
}

allEventDefinitions.addAll(mainEventDefinitions);
Collection<EventDefinition> allEventDefinitions = new ArrayList<>(mainEventDefinitions);

for (EventDefinition mainEventDefinition : mainEventDefinitions) {
for (AnnotationInstance eventInstance : index.getAnnotations(mainEventDefinition.getAnnotation())) {
Expand All @@ -243,7 +239,7 @@ private static DispatchingConfiguration getDispatchingConfiguration(
Collection<AnnotationInstance> actionInstances = index.getAnnotations(ACTION)
.stream()
.filter(ai -> ai.target().kind() == Kind.METHOD)
.collect(Collectors.toList());
.toList();
for (AnnotationInstance actionInstance : actionInstances) {
String name = actionInstance.valueWithDefault(index).asString();

Expand Down Expand Up @@ -293,7 +289,7 @@ private static DispatchingConfiguration getDispatchingConfiguration(
.stream()
.filter(ai -> ai.target().kind() == Kind.METHOD_PARAMETER)
.filter(ai -> !ai.target().asMethodParameter().method().hasAnnotation(ACTION))
.collect(Collectors.toList());
.toList();
for (AnnotationInstance eventSubscriberInstance : eventSubscriberInstances) {
MethodParameterInfo annotatedParameter = eventSubscriberInstance.target().asMethodParameter();
MethodInfo methodInfo = annotatedParameter.method();
Expand Down Expand Up @@ -387,21 +383,22 @@ private static void generatePayloadTypeResolver(ClassOutput beanClassOutput,
payloadTypeResolverClassCreator.addAnnotation(Singleton.class);

Map<String, DotName> payloadTypeMapping = eventDefinitions.stream()
.collect(Collectors.toMap(ed -> ed.getEvent(), ed -> ed.getPayloadType(), (ed1, ed2) -> ed1));
.collect(Collectors.toMap(EventDefinition::getEvent, EventDefinition::getPayloadType, (ed1, ed2) -> ed1));

MethodCreator getPayloadTypeMethodCreator = payloadTypeResolverClassCreator.getMethodCreator("getPayloadType",
Class.class, String.class);

ResultHandle eventRh = getPayloadTypeMethodCreator.getMethodParam(0);

for (Entry<String, DotName> payloadTypeMappingEntry : payloadTypeMapping.entrySet()) {
BytecodeCreator matches = getPayloadTypeMethodCreator.ifTrue(getPayloadTypeMethodCreator.invokeVirtualMethod(
MethodDescriptors.OBJECT_EQUALS, getPayloadTypeMethodCreator.load(payloadTypeMappingEntry.getKey()),
eventRh))
BytecodeCreator matches = getPayloadTypeMethodCreator.ifTrue(
getPayloadTypeMethodCreator.invokeVirtualMethod(
MethodDescriptors.OBJECT_EQUALS,
getPayloadTypeMethodCreator.load(payloadTypeMappingEntry.getKey()),
eventRh))
.trueBranch();
matches.returnValue(matches.loadClass(payloadTypeMappingEntry.getValue().toString()));
}

getPayloadTypeMethodCreator.returnValue(getPayloadTypeMethodCreator.loadNull());

payloadTypeResolverClassCreator.close();
Expand All @@ -413,8 +410,6 @@ private static void generatePayloadTypeResolver(ClassOutput beanClassOutput,
* It emits the GitHub events as CDI events that will then be caught by the multiplexers.
*/
private static void generateActionMain(ClassOutput beanClassOutput,
CombinedIndexBuildItem combinedIndex,
LaunchModeBuildItem launchMode,
DispatchingConfiguration dispatchingConfiguration,
BuildProducer<ReflectiveClassBuildItem> reflectiveClasses) {
String gitHubEventHandlerClassName = GitHubEventHandler.class.getName() + "Impl";
Expand Down Expand Up @@ -460,7 +455,7 @@ private static void generateActionMain(ClassOutput beanClassOutput,

ResultHandle actionAnnotationLiteralRh = nameMatchesCreator.newInstance(MethodDescriptor
.ofConstructor(ActionLiteral.class, String.class),
new ResultHandle[] { nameMatchesCreator.load(name) });
nameMatchesCreator.load(name));

for (Entry<String, ActionDispatchingConfiguration> eventConfigurationEntry : actionConfiguration.entrySet()) {
String event = eventConfigurationEntry.getKey();
Expand Down Expand Up @@ -575,7 +570,7 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,
originalConstructor.parameterTypes().stream().map(t -> t.name().toString()).toArray(String[]::new)));

List<AnnotationInstance> originalMethodAnnotations = originalConstructor.annotations().stream()
.filter(ai -> ai.target().kind() == Kind.METHOD).collect(Collectors.toList());
.filter(ai -> ai.target().kind() == Kind.METHOD).toList();
for (AnnotationInstance originalMethodAnnotation : originalMethodAnnotations) {
constructorCreator.addAnnotation(originalMethodAnnotation);
}
Expand Down Expand Up @@ -662,7 +657,7 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,

MethodCreator methodCreator = multiplexerClassCreator.getMethodCreator(
originalMethod.name() + "_"
+ HashUtil.sha1(originalMethod.toString() + "_" + (eventSubscriberInstance != null
+ HashUtil.sha1(originalMethod + "_" + (eventSubscriberInstance != null
? eventSubscriberInstance.toString()
: EventDefinition.ALL)),
originalMethod.returnType().name().toString(),
Expand Down Expand Up @@ -726,7 +721,9 @@ private static void generateMultiplexers(ClassOutput beanClassOutput,
gitHubEventRh);
} else if (parameterAnnotations.stream().anyMatch(ai -> ai.name().equals(CONFIG_FILE))) {
AnnotationInstance configFileAnnotationInstance = parameterAnnotations.stream()
.filter(ai -> ai.name().equals(CONFIG_FILE)).findFirst().get();
.filter(ai -> ai.name().equals(CONFIG_FILE))
.findFirst()
.orElseThrow(() -> new AssertionError("ConfigFile annotation not present"));
String configObjectType = originalMethodParameterTypes.get(originalMethodParameterIndex).name()
.toString();

Expand Down
37 changes: 34 additions & 3 deletions docs/modules/ROOT/pages/developer-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -325,7 +325,7 @@ The following action method is executed any time an `issues` event is triggered
[source,java]
----
@Action
void test(@Issue GHEventPayload.Issue issuePayload) throws IOException {
void action(@Issue GHEventPayload.Issue issuePayload) throws IOException {
GHIssue issue = issuePayload.getIssue();
System.out.println("Repository: " + issue.getRepository().getFullName()); <1>
Expand All @@ -349,7 +349,7 @@ The following action method is executed when a new issue is opened:
[source,java]
----
@Action
void test(@Issue.Opened GHEventPayload.Issue issuePayload) {
void action(@Issue.Opened GHEventPayload.Issue issuePayload) {
System.out.println("Repository: " + issuePayload.getIssue().getRepository().getFullName());
System.out.println("Issue title: " + issuePayload.getIssue().getTitle());
}
Expand Down Expand Up @@ -377,7 +377,7 @@ If the path is absolute, the file is searched from the root of the repository.
[source,java]
----
@Action
void test(@ConfigFile("example-config-file.yml") ConfigFileBean configFileBean) { <1>
void action(@ConfigFile("example-config-file.yml") ConfigFileBean configFileBean) { <1>
System.out.println("Value 1: " + configFileBean.value1);
System.out.println("Value 2: " + configFileBean.value2);
}
Expand All @@ -391,6 +391,37 @@ If the path is absolute, the file is searched from the root of the repository.
----
<1> Read `.github/example-config-file.yml` from the default branch of the repository and inject the values into `ConfigFileBean` via Jackson deserialization.

=== GHRepository

You can inject the current repository into any action method. This allows you to access the REST API for https://docs.github.com/en/rest/repos[GitHub repositories].

This enables you to e.g.:

- create / query issues
- create / query pull requests
- access workflows
- and much more

[source,java]
----
@Action
void action(GHRepository repository) { <1>
System.out.println(repository.getFullName());
var issueBuilder = repository.createIssue("Issue Name"); <2>
var pullRequest = repository.getPullRequest(42); <3>
var workflows = repository.listWorkflows(); <4>
}
----
<1> Inject the current repository
<2> Create an issue
<3> Retrieve a pull request
<4> List all workflows


For a complete list of all possibilities and the concrete syntax see the Javadoc for https://github-api.kohsuke.org/apidocs/org/kohsuke/github/GHRepository.html[GHRepository].

== Debugging

If you need to debug the behavior of the Quarkus GitHub Action extension,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package io.quarkiverse.githubaction.it;

import org.kohsuke.github.GHRepository;

import io.quarkiverse.githubaction.Action;

public class RepositoryAction {
public static final String ACTION_NAME = "RepositoryAction";

@Action(ACTION_NAME)
void test(GHRepository repository) {
System.out.println(repository.getFullName());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package io.quarkiverse.githubaction.it;

import static org.assertj.core.api.Assertions.assertThat;

import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import jakarta.enterprise.inject.Alternative;
import jakarta.inject.Singleton;

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.condition.DisabledIf;
import org.junit.jupiter.api.condition.EnabledIf;

import io.quarkiverse.githubaction.Context;
import io.quarkiverse.githubaction.ContextInitializer;
import io.quarkiverse.githubaction.Inputs;
import io.quarkiverse.githubaction.InputsInitializer;
import io.quarkiverse.githubaction.it.RepositoryActionTest.RepositoryActionTestProfile;
import io.quarkiverse.githubaction.testing.DefaultTestContext;
import io.quarkiverse.githubaction.testing.DefaultTestInputs;
import io.quarkus.test.junit.QuarkusTestProfile;
import io.quarkus.test.junit.TestProfile;
import io.quarkus.test.junit.main.Launch;
import io.quarkus.test.junit.main.LaunchResult;
import io.quarkus.test.junit.main.QuarkusMainTest;

@QuarkusMainTest
@TestProfile(RepositoryActionTestProfile.class)
public class RepositoryActionTest {
private static final boolean LOCAL = System.getenv("CURRENT_REPO") == null;
private static final String REPOSITORY_NAME = LOCAL ? "my/local-repo" : System.getenv("CURRENT_REPO");
private static final String GITHUB_TOKEN = System.getenv("CURRENT_TOKEN");

@Test
@Launch(value = {})
@DisabledIf("isLocal")
public void shouldPrintRepositoryNameWhenRunningOnGitHub(LaunchResult result) {
assertThat(result.getOutput()).contains(REPOSITORY_NAME);
}

@Test
@Launch(value = {}, exitCode = 1)
@EnabledIf("isLocal")
public void shouldFailWhenRunningLocally(LaunchResult result) {
String fullOutput = result.getOutput() + "\n" + result.getErrorOutput();

assertThat(fullOutput).contains(REPOSITORY_NAME);
}

public static class RepositoryActionTestProfile implements QuarkusTestProfile {
@Override
public Set<Class<?>> getEnabledAlternatives() {
var alternatives = new HashSet<Class<?>>();
alternatives.add(MockInputsInitializer.class);
if (LOCAL) {
alternatives.add(MockContextInitializer.class);
}
return alternatives;
}
}

@Alternative
@Singleton
public static class MockInputsInitializer implements InputsInitializer {
@Override
public Inputs createInputs() {
var inputs = new HashMap<String, String>();
inputs.put(Inputs.ACTION, RepositoryAction.ACTION_NAME);
if (!LOCAL) {
inputs.put(Inputs.GITHUB_TOKEN, GITHUB_TOKEN);
}
return new DefaultTestInputs(inputs);
}
}

@Alternative
@Singleton
public static class MockContextInitializer implements ContextInitializer {
@Override
public Context createContext() {
return new DefaultTestContext() {
@Override
public String getGitHubRepository() {
return REPOSITORY_NAME;
}
};
}
}

private static boolean isLocal() {
return LOCAL;
}
}
Loading

0 comments on commit 1ced88f

Please sign in to comment.