Skip to content

Skip GitHubOrgWebHook.register when using a GH app receiving all relevant events #289

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
8 changes: 7 additions & 1 deletion docs/github-app.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@ Permissions this plugin uses:
- Metadata: Read-only
- Pull requests: Read-only

Under _Subscribe to events_, enable all events.
You are not required to use webhooks but it strongly recommeded over alternatives such as polling. To use webhooks, you can give your GitHub App "Read and Write" permission to Webhooks (noted above) and the plugin will configure your repositories to send the appropriate events. Otherwise you can manually configure the app handle all events needed by this plugin, in which case the app will not need webhook permissions.

You should configure the app to receive at least the following events:

- Pull request
- Push
- Repository (automatically add required read-only "Administration" permission)

Click 'Create GitHub app'

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,44 @@ public String getEncodedAuthorization() throws IOException {
}
}

GHAppInstallation getAppInstallation() throws IOException {
return getAppInstallation(null, appID, privateKey.getPlainText(), apiUri, owner);
}

static GHAppInstallation getAppInstallation(
GitHub gitHubApp, String appId, String appPrivateKey, String apiUrl, String owner) throws IOException {
if (gitHubApp == null) {
gitHubApp = TokenProvider.createTokenRefreshGitHub(appId, appPrivateKey, apiUrl);
}

GHApp app;
try {
app = gitHubApp.getApp();
} catch (IOException e) {
throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId), e);
}

GHAppInstallation appInstallation;
List<GHAppInstallation> appInstallations = app.listInstallations().asList();
if (appInstallations.isEmpty()) {
throw new IllegalArgumentException(String.format(ERROR_NOT_INSTALLED, appId));
} else if (appInstallations.size() == 1) {
appInstallation = appInstallations.get(0);
} else {
final String ownerOrEmpty = owner != null ? owner : "";
appInstallation = appInstallations.stream()
.filter(installation -> installation
.getAccount()
.getLogin()
.toLowerCase(Locale.ROOT)
.equals(ownerOrEmpty.toLowerCase(Locale.ROOT)))
.findAny()
.orElseThrow(() ->
new IllegalArgumentException(String.format(ERROR_NO_OWNER_MATCHING, appId, ownerOrEmpty)));
}
return appInstallation;
}

@SuppressWarnings("deprecation") // preview features are required for GitHub app integration, GitHub api adds
// deprecated to all preview methods
static AppInstallationToken generateAppInstallationToken(
Expand All @@ -207,36 +245,7 @@ static AppInstallationToken generateAppInstallationToken(
// We expect this to be fast but if anything hangs in here we do not want to block indefinitely

try (Timeout ignored = Timeout.limit(30, TimeUnit.SECONDS)) {
if (gitHubApp == null) {
gitHubApp = TokenProvider.createTokenRefreshGitHub(appId, appPrivateKey, apiUrl);
}

GHApp app;
try {
app = gitHubApp.getApp();
} catch (IOException e) {
throw new IllegalArgumentException(String.format(ERROR_AUTHENTICATING_GITHUB_APP, appId), e);
}

List<GHAppInstallation> appInstallations = app.listInstallations().asList();
if (appInstallations.isEmpty()) {
throw new IllegalArgumentException(String.format(ERROR_NOT_INSTALLED, appId));
}
GHAppInstallation appInstallation;
if (appInstallations.size() == 1) {
appInstallation = appInstallations.get(0);
} else {
final String ownerOrEmpty = owner != null ? owner : "";
appInstallation = appInstallations.stream()
.filter(installation -> installation
.getAccount()
.getLogin()
.toLowerCase(Locale.ROOT)
.equals(ownerOrEmpty.toLowerCase(Locale.ROOT)))
.findAny()
.orElseThrow(() -> new IllegalArgumentException(
String.format(ERROR_NO_OWNER_MATCHING, appId, ownerOrEmpty)));
}
GHAppInstallation appInstallation = getAppInstallation(gitHubApp, appId, appPrivateKey, apiUrl, owner);

GHAppInstallationToken appInstallationToken = appInstallation
.createToken(appInstallation.getPermissions())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ private static File getTrackingFile(String orgName) {
return new File(Jenkins.get().getRootDir(), "github-webhooks/GitHubOrgHook." + orgName);
}

// TODO never called?

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The uninstallation of an app sends out an "action":"deleted" installation event, similar to installation of an app which also sends out an installation event but with "action":"created"

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes but AFAICT this Java method is not called.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah looking at it, it might be designed to act on a deregister for a webhook, but I cannot find any such event for it on the Github API document :/

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the intention was for this to be called when the OrganizationFolder was deleted, but from e424aa2 I am guessing this was never implemented.

public static void deregister(GitHub hub, String orgName) throws IOException {
String rootUrl = Jenkins.get().getRootUrl();
if (rootUrl == null) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
import hudson.util.ListBoxModel;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.lang.reflect.Method;
import java.net.MalformedURLException;
import java.time.Duration;
import java.time.format.DateTimeParseException;
Expand Down Expand Up @@ -96,9 +97,11 @@
import org.jenkins.ui.icon.IconSpec;
import org.jenkinsci.Symbol;
import org.jenkinsci.plugins.github.config.GitHubServerConfig;
import org.jenkinsci.plugins.github.extension.GHEventsSubscriber;
import org.kohsuke.accmod.Restricted;
import org.kohsuke.accmod.restrictions.DoNotUse;
import org.kohsuke.accmod.restrictions.NoExternalUse;
import org.kohsuke.github.GHEvent;
import org.kohsuke.github.GHMyself;
import org.kohsuke.github.GHOrganization;
import org.kohsuke.github.GHRepository;
Expand Down Expand Up @@ -1627,6 +1630,35 @@ public void afterSave(@NonNull SCMNavigatorOwner owner) {
// FIXME MINOR HACK ALERT
StandardCredentials credentials =
Connector.lookupScanCredentials((Item) owner, getApiUri(), credentialsId, repoOwner);
if (credentials instanceof GitHubAppCredentials) {
try {
List<GHEvent> handledEvents = ((GitHubAppCredentials) credentials)
.getAppInstallation()
.getEvents();
Method eventsM = GHEventsSubscriber.class.getMethod("events"); // TODO protected
eventsM.setAccessible(true);
boolean good = true;
for (GHEventsSubscriber subscriber : GHEventsSubscriber.all()) {
@SuppressWarnings("unchecked")
Set<GHEvent> subscribedEvents = (Set<GHEvent>) eventsM.invoke(subscriber);
if (!handledEvents.containsAll(subscribedEvents)) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would make sure no events go unhandled :+1

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be too strict—to activate this patch you need to include the repository events which you might otherwise have skipped if you are only dealing with a small list of repositories—but I erred on the conservative side.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this will break on PingGHEventSubscriber. Not yet tested.

DescriptorImpl.LOGGER.log(
Level.WARNING,
"The GitHub App is not configured to receive some desired events: {0}",
subscribedEvents);
good = false;
}
}
if (good) {
DescriptorImpl.LOGGER.info(
"The GitHub App is configured to receive some desired events; no additional webhook need be installed on the organization");
return;
}
} catch (Exception x) {
DescriptorImpl.LOGGER.log(
Level.WARNING, "Could not check whether the GitHub App receives all desired events", x);
}
}
GitHub hub = Connector.connect(getApiUri(), credentials);
try {
GitHubOrgWebHook.register(hub, repoOwner);
Expand Down