Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ target
.idea
*.iml

## VS Code
.vscode

## Logs
*.log

Expand All @@ -29,4 +32,4 @@ pom.xml.releaseBackup
release.properties

# macOS
.DS_Store
.DS_Store
2 changes: 1 addition & 1 deletion pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -457,7 +457,7 @@
</plugin>
<plugin>
<artifactId>maven-enforcer-plugin</artifactId>
<version>1.4.1</version>
<version>3.6.2</version>
<executions>
<execution>
<id>enforce</id>
Expand Down
54 changes: 52 additions & 2 deletions src/main/java/com/spotify/github/v3/checks/App.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -112,4 +113,53 @@ public interface App {
* @return the optional count
*/
Optional<Integer> installationsCount();

/**
* The client ID of the GitHub App.
*
* @return the optional client ID
*/
Optional<String> clientId();

/**
* The name of the single file the GitHub App can access (if applicable).
*
* @return the optional single file name
*/
Optional<String> singleFileName();

/**
* Whether the GitHub App has access to multiple single files.
*
* @return the optional boolean
*/
Optional<Boolean> hasMultipleSingleFiles();

/**
* The list of single file paths the GitHub App can access.
*
* @return the optional list of file paths
*/
Optional<List<String>> singleFilePaths();

/**
* The slug name of the GitHub App.
*
* @return the optional app slug
*/
Optional<String> appSlug();

/**
* The date the App was suspended.
*
* @return the optional suspended date
*/
Optional<ZonedDateTime> suspendedAt();

/**
* The user who suspended the App.
*
* @return the optional user
*/
Optional<User> suspendedBy();
}
13 changes: 12 additions & 1 deletion src/main/java/com/spotify/github/v3/clients/GitHubClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -1017,7 +1026,9 @@ private CompletableFuture<String> 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");
}

/**
Expand Down
58 changes: 49 additions & 9 deletions src/main/java/com/spotify/github/v3/clients/GithubAppClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand All @@ -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";
Expand All @@ -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<String> maybeOwner;
private final Optional<String> maybeRepo;

private final Map<String, String> extraHeaders =
Expand All @@ -59,16 +61,38 @@ 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();
}

/**
* 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()"));
}
Copy link
Member Author

@javoire javoire Oct 23, 2025

Choose a reason for hiding this comment

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

another option is another Class (GithubAppClient?). But then I'd prob rename this class to GithubAppInstallationClient which would be a breaking change

at least this way its hard to accidentally call one of the existing methods without the owner


/**
* List Installations of an app.
*
Expand Down Expand Up @@ -99,29 +123,32 @@ public CompletableFuture<Installation> getInstallation(final Integer installatio

/**
* Get an installation of a repo
*
* @return an Installation
*/
private CompletableFuture<Installation> getRepoInstallation(final String repo) {
return github.request(
String.format(GET_INSTALLATION_REPO_URL, owner, repo), Installation.class);
String.format(GET_INSTALLATION_REPO_URL, requireOwner(), repo), Installation.class);
}

/**
* Get an installation of an org
*
* @return an Installation
*/
private CompletableFuture<Installation> getOrgInstallation() {
return github.request(
String.format(GET_INSTALLATION_ORG_URL, owner), Installation.class);
String.format(GET_INSTALLATION_ORG_URL, requireOwner()), Installation.class);
}

/**
/**
* Get an installation of a user
*
* @return an Installation
*/
public CompletableFuture<Installation> getUserInstallation() {
return github.request(
String.format(GET_INSTALLATION_USER_URL, owner), Installation.class);
String.format(GET_INSTALLATION_USER_URL, requireOwner()), Installation.class);
}

/**
Expand All @@ -146,4 +173,17 @@ public CompletableFuture<InstallationRepositoriesResponse> listAccessibleReposit
return GitHubClient.scopeForInstallationId(github, installationId)
.request(LIST_ACCESSIBLE_REPOS_URL, InstallationRepositoriesResponse.class, extraHeaders);
}

/**
* Get the authenticated GitHub App.
*
* <p>Returns the authenticated app. You must use a JWT to access this endpoint.
*
* <p>see https://docs.github.com/en/rest/apps/apps#get-the-authenticated-app
*
* @return the authenticated App
*/
public CompletableFuture<App> getAuthenticatedApp() {
return github.request(GET_AUTHENTICATED_APP_URL, App.class);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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;
Expand Down Expand Up @@ -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));
Expand Down Expand Up @@ -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));
}
}
Original file line number Diff line number Diff line change
@@ -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-----"
}