diff --git a/docs/github-app.adoc b/docs/github-app.adoc index 439fe8bd0..334244300 100644 --- a/docs/github-app.adoc +++ b/docs/github-app.adoc @@ -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' diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java index c4f45cf6b..b6184ab18 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubAppCredentials.java @@ -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 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( @@ -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 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()) diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubOrgWebHook.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubOrgWebHook.java index 70b8c9514..6f22563ff 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubOrgWebHook.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubOrgWebHook.java @@ -97,6 +97,7 @@ private static File getTrackingFile(String orgName) { return new File(Jenkins.get().getRootDir(), "github-webhooks/GitHubOrgHook." + orgName); } + // TODO never called? public static void deregister(GitHub hub, String orgName) throws IOException { String rootUrl = Jenkins.get().getRootUrl(); if (rootUrl == null) { diff --git a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java index d2adf273b..78e038b53 100644 --- a/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java +++ b/src/main/java/org/jenkinsci/plugins/github_branch_source/GitHubSCMNavigator.java @@ -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; @@ -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; @@ -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 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 subscribedEvents = (Set) eventsM.invoke(subscriber); + if (!handledEvents.containsAll(subscribedEvents)) { + 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);