From 56d6eec0603a0a394395e0afe970fe1ec4cc1fae Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Thu, 23 Oct 2025 16:49:38 -0400 Subject: [PATCH 1/4] feat: add GET /app endpoint to retrieve authenticated app details --- .gitignore | 5 +- .../com/spotify/github/v3/checks/App.java | 54 ++++++++++++++++++- .../github/v3/clients/GitHubClient.java | 13 ++++- .../github/v3/clients/GithubAppClient.java | 42 +++++++++++---- .../v3/clients/GithubAppClientTest.java | 30 +++++++++-- .../v3/githubapp/authenticated-app.json | 43 +++++++++++++++ 6 files changed, 170 insertions(+), 17 deletions(-) create mode 100644 src/test/resources/com/spotify/github/v3/githubapp/authenticated-app.json diff --git a/.gitignore b/.gitignore index 417741c2..dd496563 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ target .idea *.iml +## VS Code +.vscode + ## Logs *.log @@ -29,4 +32,4 @@ pom.xml.releaseBackup release.properties # macOS -.DS_Store \ No newline at end of file +.DS_Store diff --git a/src/main/java/com/spotify/github/v3/checks/App.java b/src/main/java/com/spotify/github/v3/checks/App.java index 8affe3a0..e130b754 100644 --- a/src/main/java/com/spotify/github/v3/checks/App.java +++ b/src/main/java/com/spotify/github/v3/checks/App.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,6 +22,7 @@ import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.spotify.github.GithubStyle; +import com.spotify.github.v3.User; import java.time.ZonedDateTime; import java.util.List; import java.util.Map; @@ -112,4 +113,53 @@ public interface App { * @return the optional count */ Optional installationsCount(); + + /** + * The client ID of the GitHub App. + * + * @return the optional client ID + */ + Optional clientId(); + + /** + * The name of the single file the GitHub App can access (if applicable). + * + * @return the optional single file name + */ + Optional singleFileName(); + + /** + * Whether the GitHub App has access to multiple single files. + * + * @return the optional boolean + */ + Optional hasMultipleSingleFiles(); + + /** + * The list of single file paths the GitHub App can access. + * + * @return the optional list of file paths + */ + Optional> singleFilePaths(); + + /** + * The slug name of the GitHub App. + * + * @return the optional app slug + */ + Optional appSlug(); + + /** + * The date the App was suspended. + * + * @return the optional suspended date + */ + Optional suspendedAt(); + + /** + * The user who suspended the App. + * + * @return the optional user + */ + Optional suspendedBy(); } diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java index 8d0a7fc2..b7809a47 100644 --- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java @@ -588,6 +588,15 @@ public UserClient createUserClient(final String owner) { return UserClient.create(this, owner); } + /** + * Create GitHub App API client + * + * @return GitHub App API client + */ + public GithubAppClient createGithubAppClient() { + return new GithubAppClient(this); + } + Json json() { return json; } @@ -1017,7 +1026,9 @@ private CompletableFuture getAuthorizationHeader(final String path) { } private boolean isJwtRequest(final String path) { - return path.startsWith("/app/installation") || path.endsWith("installation"); + return path.equals("/app") + || path.startsWith("/app/installation") + || path.endsWith("installation"); } /** diff --git a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java index e9150d96..12affbe3 100644 --- a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -24,6 +24,7 @@ import com.google.common.collect.ImmutableMap; import com.spotify.github.v3.apps.InstallationRepositoriesResponse; import com.spotify.github.v3.checks.AccessToken; +import com.spotify.github.v3.checks.App; import com.spotify.github.v3.checks.Installation; import java.util.List; import java.util.Map; @@ -34,6 +35,7 @@ /** Apps API client */ public class GithubAppClient { + private static final String GET_AUTHENTICATED_APP_URL = "/app"; private static final String GET_INSTALLATION_BY_ID_URL = "/app/installations/%s"; private static final String GET_ACCESS_TOKEN_URL = "/app/installations/%s/access_tokens"; private static final String GET_INSTALLATIONS_URL = "/app/installations?per_page=100"; @@ -48,7 +50,7 @@ public class GithubAppClient { private static final String GET_INSTALLATION_USER_URL = "/users/%s/installation"; private final GitHubClient github; - private final String owner; + private final Optional maybeOwner; private final Optional maybeRepo; private final Map extraHeaders = @@ -59,13 +61,19 @@ public class GithubAppClient { GithubAppClient(final GitHubClient github, final String owner, final String repo) { this.github = github; - this.owner = owner; + this.maybeOwner = Optional.of(owner); this.maybeRepo = Optional.of(repo); } GithubAppClient(final GitHubClient github, final String owner) { this.github = github; - this.owner = owner; + this.maybeOwner = Optional.of(owner); + this.maybeRepo = Optional.empty(); + } + + GithubAppClient(final GitHubClient github) { + this.github = github; + this.maybeOwner = Optional.empty(); this.maybeRepo = Optional.empty(); } @@ -99,29 +107,32 @@ public CompletableFuture getInstallation(final Integer installatio /** * Get an installation of a repo + * * @return an Installation */ private CompletableFuture getRepoInstallation(final String repo) { return github.request( - String.format(GET_INSTALLATION_REPO_URL, owner, repo), Installation.class); + String.format(GET_INSTALLATION_REPO_URL, maybeOwner.get(), repo), Installation.class); } /** * Get an installation of an org + * * @return an Installation */ private CompletableFuture getOrgInstallation() { return github.request( - String.format(GET_INSTALLATION_ORG_URL, owner), Installation.class); + String.format(GET_INSTALLATION_ORG_URL, maybeOwner.get()), Installation.class); } - /** + /** * Get an installation of a user + * * @return an Installation */ public CompletableFuture getUserInstallation() { return github.request( - String.format(GET_INSTALLATION_USER_URL, owner), Installation.class); + String.format(GET_INSTALLATION_USER_URL, maybeOwner.get()), Installation.class); } /** @@ -146,4 +157,17 @@ public CompletableFuture listAccessibleReposit return GitHubClient.scopeForInstallationId(github, installationId) .request(LIST_ACCESSIBLE_REPOS_URL, InstallationRepositoriesResponse.class, extraHeaders); } + + /** + * Get the authenticated GitHub App. + * + *

Returns the authenticated app. You must use a JWT to access this endpoint. + * + *

see https://docs.github.com/en/rest/apps/apps#get-the-authenticated-app + * + * @return the authenticated App + */ + public CompletableFuture getAuthenticatedApp() { + return github.request(GET_AUTHENTICATED_APP_URL, App.class); + } } diff --git a/src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java b/src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java index 62e92757..bccb8bc3 100644 --- a/src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/GithubAppClientTest.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -30,6 +30,7 @@ import com.google.common.io.Resources; import com.spotify.github.FixtureHelper; import com.spotify.github.v3.apps.InstallationRepositoriesResponse; +import com.spotify.github.v3.checks.App; import com.spotify.github.v3.checks.Installation; import java.io.File; import java.io.IOException; @@ -110,8 +111,7 @@ public void listAccessibleRepositories() throws Exception { .setResponseCode(200) .setBody(FixtureHelper.loadFixture("githubapp/accessible-repositories.json"))); - InstallationRepositoriesResponse response = - client.listAccessibleRepositories(1234).join(); + InstallationRepositoriesResponse response = client.listAccessibleRepositories(1234).join(); assertThat(response.totalCount(), is(2)); assertThat(response.repositories().size(), is(2)); @@ -160,4 +160,26 @@ public void getInstallationByInstallationId() throws Exception { RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS); assertThat(recordedRequest.getRequestUrl().encodedPath(), is("/app/installations/1234")); } + + @Test + public void getAuthenticatedApp() throws Exception { + mockServer.enqueue( + new MockResponse() + .setResponseCode(200) + .setBody(FixtureHelper.loadFixture("githubapp/authenticated-app.json"))); + + App app = client.getAuthenticatedApp().join(); + + assertThat(app.id(), is(1)); + assertThat(app.slug().get(), is("octoapp")); + assertThat(app.name(), is("Octocat App")); + assertThat(app.clientId().get(), is("Iv1.8a61f9b3a7aba766")); + assertThat(app.installationsCount().get(), is(5)); + + RecordedRequest recordedRequest = mockServer.takeRequest(1, TimeUnit.MILLISECONDS); + assertThat(recordedRequest.getRequestUrl().encodedPath(), is("/app")); + assertThat( + recordedRequest.getHeaders().values("Authorization").get(0).startsWith("Bearer "), + is(true)); + } } diff --git a/src/test/resources/com/spotify/github/v3/githubapp/authenticated-app.json b/src/test/resources/com/spotify/github/v3/githubapp/authenticated-app.json new file mode 100644 index 00000000..b5dc8eab --- /dev/null +++ b/src/test/resources/com/spotify/github/v3/githubapp/authenticated-app.json @@ -0,0 +1,43 @@ +{ + "id": 1, + "slug": "octoapp", + "node_id": "MDExOkludGVncmF0aW9uMQ==", + "owner": { + "login": "github", + "id": 1, + "node_id": "MDEyOk9yZ2FuaXphdGlvbjE=", + "url": "https://api.github.com/orgs/github", + "repos_url": "https://api.github.com/orgs/github/repos", + "events_url": "https://api.github.com/orgs/github/events", + "avatar_url": "https://github.com/images/error/octocat_happy.gif", + "gravatar_id": "", + "html_url": "https://github.com/github", + "followers_url": "https://api.github.com/users/github/followers", + "following_url": "https://api.github.com/users/github/following{/other_user}", + "gists_url": "https://api.github.com/users/github/gists{/gist_id}", + "starred_url": "https://api.github.com/users/github/starred{/owner}{/repo}", + "subscriptions_url": "https://api.github.com/users/github/subscriptions", + "organizations_url": "https://api.github.com/users/github/orgs", + "received_events_url": "https://api.github.com/users/github/received_events", + "type": "User", + "site_admin": true + }, + "name": "Octocat App", + "description": "Test GitHub App", + "external_url": "https://example.com", + "html_url": "https://github.com/apps/octoapp", + "created_at": "2017-07-08T16:18:44-04:00", + "updated_at": "2017-07-08T16:18:44-04:00", + "permissions": { + "metadata": "read", + "contents": "read", + "issues": "write", + "single_file": "write" + }, + "events": ["push", "pull_request"], + "installations_count": 5, + "client_id": "Iv1.8a61f9b3a7aba766", + "client_secret": "1726be1638095a19edd134c77bde3aa2ece1e5d8", + "webhook_secret": "e340154128314309424b7c8e90325147d99fdafa", + "pem": "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEAuEPzOUE+kiEH1WLiMeBytTEF856j0hgg...\n-----END RSA PRIVATE KEY-----" +} From 9c57a772ce134a7285e3238bedda3ee79b4d164c Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Thu, 23 Oct 2025 17:03:00 -0400 Subject: [PATCH 2/4] refactor: add requireOwner helper for better error messages Replaces Optional.get() calls with requireOwner() helper that provides clear error messages when owner-dependent methods are called on app-level clients without owner context. --- .../github/v3/clients/GithubAppClient.java | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java index 12affbe3..2c8751da 100644 --- a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java @@ -77,6 +77,21 @@ public class GithubAppClient { this.maybeRepo = Optional.empty(); } + /** + * Gets the owner, throwing a descriptive exception if not present. + * + * @return the owner string + * @throws IllegalStateException if owner is not present + */ + private String requireOwner() { + return maybeOwner.orElseThrow(() -> + new IllegalStateException( + "This operation requires an owner context. " + + "Use GitHubClient.createOrganisationClient(owner).createGithubAppClient() " + + "or GitHubClient.createRepositoryClient(owner, repo).createGithubAppClient() " + + "instead of GitHubClient.createGithubAppClient()")); + } + /** * List Installations of an app. * @@ -112,7 +127,7 @@ public CompletableFuture getInstallation(final Integer installatio */ private CompletableFuture getRepoInstallation(final String repo) { return github.request( - String.format(GET_INSTALLATION_REPO_URL, maybeOwner.get(), repo), Installation.class); + String.format(GET_INSTALLATION_REPO_URL, requireOwner(), repo), Installation.class); } /** @@ -122,7 +137,7 @@ private CompletableFuture getRepoInstallation(final String repo) { */ private CompletableFuture getOrgInstallation() { return github.request( - String.format(GET_INSTALLATION_ORG_URL, maybeOwner.get()), Installation.class); + String.format(GET_INSTALLATION_ORG_URL, requireOwner()), Installation.class); } /** @@ -132,7 +147,7 @@ private CompletableFuture getOrgInstallation() { */ public CompletableFuture getUserInstallation() { return github.request( - String.format(GET_INSTALLATION_USER_URL, maybeOwner.get()), Installation.class); + String.format(GET_INSTALLATION_USER_URL, requireOwner()), Installation.class); } /** From 36fe1d6f91cdfd72cecbf988e9aaa8665cf5ea03 Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Thu, 23 Oct 2025 17:04:12 -0400 Subject: [PATCH 3/4] format --- .../spotify/github/v3/clients/GithubAppClient.java | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java index 2c8751da..3d462ece 100644 --- a/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GithubAppClient.java @@ -84,12 +84,13 @@ public class GithubAppClient { * @throws IllegalStateException if owner is not present */ private String requireOwner() { - return maybeOwner.orElseThrow(() -> - new IllegalStateException( - "This operation requires an owner context. " - + "Use GitHubClient.createOrganisationClient(owner).createGithubAppClient() " - + "or GitHubClient.createRepositoryClient(owner, repo).createGithubAppClient() " - + "instead of GitHubClient.createGithubAppClient()")); + return maybeOwner.orElseThrow( + () -> + new IllegalStateException( + "This operation requires an owner context. " + + "Use GitHubClient.createOrganisationClient(owner).createGithubAppClient() " + + "or GitHubClient.createRepositoryClient(owner, repo).createGithubAppClient() " + + "instead of GitHubClient.createGithubAppClient()")); } /** From 532d23d5173223d48017cbbf421c1cfe37be05d1 Mon Sep 17 00:00:00 2001 From: Jonatan Dahl Date: Fri, 24 Oct 2025 08:45:59 -0400 Subject: [PATCH 4/4] try update maven enforcer --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index b8138ade..59bc50c5 100644 --- a/pom.xml +++ b/pom.xml @@ -457,7 +457,7 @@ maven-enforcer-plugin - 1.4.1 + 3.6.2 enforce