Repository: hub4j/github-api
Pull request #1492 (2022-07-27 21:10:10)

Add support for repositories body parameter when creating installation access tokens
# Description

This PR adds support for the `repositories` body parameter as specified in https://docs.github.com/en/rest/apps/apps#create-an-installation-access-token-for-an-app to `GHAppCreateTokenBuilder`.

Originally, if you wanted to generate an installation access token limited to a specific repository, the `/app/installations/{installation_id}/access_tokens` endpoint required you to pass a `repository_ids` body parameter. That parameter is a bit awkward because generally you will need to make an additional API call to retrieve the ID of a repository given its name.

At some point (I could not find a changelog) GitHub added a `repositories` body parameter that works like `repository_ids`, but accepts repository names rather than IDs, which is a nice improvement.

# Before submitting a PR:

- [x] Changes must not break binary backwards compatibility. If you are unclear on how to make the change you think is needed while maintaining backward compatibility, [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [x] Add JavaDocs and other comments as appropriate. Consider including links in comments to relevant documentation on https://docs.github.com/en/rest .  
- [x] Add tests that cover any added or changed code. This generally requires capturing snapshot test data. See [CONTRIBUTING.md](CONTRIBUTING.md) for details.
- [x] Run `mvn -D enable-ci clean install site` locally. If this command doesn't succeed, your change will not pass CI.
- [x] Push your changes to a branch other than `main`. You will create your PR from that branch.

# When creating a PR: 

- [x] Fill in the "Description" above with clear summary of the changes. This includes:
  - [ ] If this PR fixes one or more issues, include "Fixes #<issue number>" lines for each issue. 
  - [x] Provide links to relevant documentation on https://docs.github.com/en/rest where possible.
- [x] All lines of new code should be covered by tests as reported by code coverage. Any lines that are not covered must have PR comments explaining why they cannot be covered. For example, "Reaching this particular exception is hard and is not a particular common scenario."
- [x] Enable "Allow edits from maintainers".



Patch:
diff --git a/src/main/java/org/kohsuke/github/GHAppCreateTokenBuilder.java b/src/main/java/org/kohsuke/github/GHAppCreateTokenBuilder.java
index 9222ca7fe..44b49e012 100644
--- a/src/main/java/org/kohsuke/github/GHAppCreateTokenBuilder.java
+++ b/src/main/java/org/kohsuke/github/GHAppCreateTokenBuilder.java
@@ -46,6 +46,20 @@ public class GHAppCreateTokenBuilder extends GitHubInteractiveObject {
         return this;
     }
 
+    /**
+     * By default the installation token has access to all repositories that the installation can access. To restrict
+     * the access to specific repositories, you can provide repository names when creating the token.
+     *
+     * @param repositories
+     *            Array containing the repository names
+     * @return a GHAppCreateTokenBuilder
+     */
+    @BetaApi
+    public GHAppCreateTokenBuilder repositories(List<String> repositories) {
+        this.builder.with("repositories", repositories);
+        return this;
+    }
+
     /**
      * Set the permissions granted to the access token. The permissions object includes the permission names and their
      * access type.


Test patch:
diff --git a/src/test/java/org/kohsuke/github/GHAppTest.java b/src/test/java/org/kohsuke/github/GHAppTest.java
index c3ebb3c6b..29e0da23c 100644
--- a/src/test/java/org/kohsuke/github/GHAppTest.java
+++ b/src/test/java/org/kohsuke/github/GHAppTest.java
@@ -146,6 +146,27 @@ public class GHAppTest extends AbstractGitHubWireMockTest {
         assertThat(installationToken2.getRepositories(), nullValue());;
     }
 
+    @Test
+    public void createTokenWithRepositories() throws IOException {
+        GHApp app = gitHub.getApp();
+        GHAppInstallation installation = app.getInstallationByUser("bogus");
+
+        // Create token specifying repositories (not repository_ids!)
+        GHAppInstallationToken installationToken = installation.createToken()
+                .repositories(Collections.singletonList("bogus"))
+                .create();
+
+        assertThat(installationToken.getToken(), is("bogus"));
+        assertThat(installationToken.getPermissions().entrySet(), hasSize(4));
+        assertThat(installationToken.getRepositorySelection(), is(GHRepositorySelection.SELECTED));
+        assertThat(installationToken.getExpiresAt(), is(GitHubClient.parseDate("2022-07-27T21:38:33Z")));
+
+        GHRepository repository = installationToken.getRepositories().get(0);
+        assertThat(installationToken.getRepositories().size(), is(1));
+        assertThat(repository.getId(), is((long) 11111111));
+        assertThat(repository.getName(), is("bogus"));
+    }
+
     private void testAppInstallation(GHAppInstallation appInstallation) throws IOException {
         Map<String, GHPermissionType> appPermissions = appInstallation.getPermissions();
         GHUser appAccount = appInstallation.getAccount();
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app-1.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app-1.json
new file mode 100644
index 000000000..98f64483b
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app-1.json
@@ -0,0 +1,41 @@
+{
+  "id": 11111111,
+  "slug": "bogus-testing",
+  "node_id": "asdfasdfasdf",
+  "owner": {
+    "login": "bogus",
+    "id": 11111111,
+    "node_id": "asdfasfasdf",
+    "avatar_url": "https://avatars.githubusercontent.com/u/11111111?v=4",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/bogus",
+    "html_url": "https://github.com/bogus",
+    "followers_url": "https://api.github.com/users/bogus/followers",
+    "following_url": "https://api.github.com/users/bogus/following{/other_user}",
+    "gists_url": "https://api.github.com/users/bogus/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/bogus/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/bogus/subscriptions",
+    "organizations_url": "https://api.github.com/users/bogus/orgs",
+    "repos_url": "https://api.github.com/users/bogus/repos",
+    "events_url": "https://api.github.com/users/bogus/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/bogus/received_events",
+    "type": "User",
+    "site_admin": false
+  },
+  "name": "bogus-testing",
+  "description": "description here",
+  "external_url": "https://github.com/bogus",
+  "html_url": "https://github.com/apps/bogus-testing",
+  "created_at": "2022-07-15T17:51:32Z",
+  "updated_at": "2022-07-15T17:51:32Z",
+  "permissions": {
+    "contents": "write",
+    "metadata": "read",
+    "organization_plan": "read",
+    "plan": "read",
+    "pull_requests": "read",
+    "statuses": "write"
+  },
+  "events": [],
+  "installations_count": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app_installations_27419505_access_tokens-3.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app_installations_27419505_access_tokens-3.json
new file mode 100644
index 000000000..e2ed7454d
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/app_installations_27419505_access_tokens-3.json
@@ -0,0 +1,112 @@
+{
+  "token": "bogus",
+  "expires_at": "2022-07-27T21:38:33Z",
+  "permissions": {
+    "contents": "write",
+    "metadata": "read",
+    "pull_requests": "read",
+    "statuses": "write"
+  },
+  "repository_selection": "selected",
+  "repositories": [
+    {
+      "id": 11111111,
+      "node_id": "asdfasdf",
+      "name": "bogus",
+      "full_name": "bogus/bogus",
+      "private": true,
+      "owner": {
+        "login": "bogus",
+        "id": 11111111,
+        "node_id": "asdfasdf",
+        "avatar_url": "https://avatars.githubusercontent.com/u/11111111?v=4",
+        "gravatar_id": "",
+        "url": "https://api.github.com/users/bogus",
+        "html_url": "https://github.com/bogus",
+        "followers_url": "https://api.github.com/users/bogus/followers",
+        "following_url": "https://api.github.com/users/bogus/following{/other_user}",
+        "gists_url": "https://api.github.com/users/bogus/gists{/gist_id}",
+        "starred_url": "https://api.github.com/users/bogus/starred{/owner}{/repo}",
+        "subscriptions_url": "https://api.github.com/users/bogus/subscriptions",
+        "organizations_url": "https://api.github.com/users/bogus/orgs",
+        "repos_url": "https://api.github.com/users/bogus/repos",
+        "events_url": "https://api.github.com/users/bogus/events{/privacy}",
+        "received_events_url": "https://api.github.com/users/bogus/received_events",
+        "type": "User",
+        "site_admin": false
+      },
+      "html_url": "https://github.com/bogus/bogus",
+      "description": null,
+      "fork": false,
+      "url": "https://api.github.com/repos/bogus/bogus",
+      "forks_url": "https://api.github.com/repos/bogus/bogus/forks",
+      "keys_url": "https://api.github.com/repos/bogus/bogus/keys{/key_id}",
+      "collaborators_url": "https://api.github.com/repos/bogus/bogus/collaborators{/collaborator}",
+      "teams_url": "https://api.github.com/repos/bogus/bogus/teams",
+      "hooks_url": "https://api.github.com/repos/bogus/bogus/hooks",
+      "issue_events_url": "https://api.github.com/repos/bogus/bogus/issues/events{/number}",
+      "events_url": "https://api.github.com/repos/bogus/bogus/events",
+      "assignees_url": "https://api.github.com/repos/bogus/bogus/assignees{/user}",
+      "branches_url": "https://api.github.com/repos/bogus/bogus/branches{/branch}",
+      "tags_url": "https://api.github.com/repos/bogus/bogus/tags",
+      "blobs_url": "https://api.github.com/repos/bogus/bogus/git/blobs{/sha}",
+      "git_tags_url": "https://api.github.com/repos/bogus/bogus/git/tags{/sha}",
+      "git_refs_url": "https://api.github.com/repos/bogus/bogus/git/refs{/sha}",
+      "trees_url": "https://api.github.com/repos/bogus/bogus/git/trees{/sha}",
+      "statuses_url": "https://api.github.com/repos/bogus/bogus/statuses/{sha}",
+      "languages_url": "https://api.github.com/repos/bogus/bogus/languages",
+      "stargazers_url": "https://api.github.com/repos/bogus/bogus/stargazers",
+      "contributors_url": "https://api.github.com/repos/bogus/bogus/contributors",
+      "subscribers_url": "https://api.github.com/repos/bogus/bogus/subscribers",
+      "subscription_url": "https://api.github.com/repos/bogus/bogus/subscription",
+      "commits_url": "https://api.github.com/repos/bogus/bogus/commits{/sha}",
+      "git_commits_url": "https://api.github.com/repos/bogus/bogus/git/commits{/sha}",
+      "comments_url": "https://api.github.com/repos/bogus/bogus/comments{/number}",
+      "issue_comment_url": "https://api.github.com/repos/bogus/bogus/issues/comments{/number}",
+      "contents_url": "https://api.github.com/repos/bogus/bogus/contents/{+path}",
+      "compare_url": "https://api.github.com/repos/bogus/bogus/compare/{base}...{head}",
+      "merges_url": "https://api.github.com/repos/bogus/bogus/merges",
+      "archive_url": "https://api.github.com/repos/bogus/bogus/{archive_format}{/ref}",
+      "downloads_url": "https://api.github.com/repos/bogus/bogus/downloads",
+      "issues_url": "https://api.github.com/repos/bogus/bogus/issues{/number}",
+      "pulls_url": "https://api.github.com/repos/bogus/bogus/pulls{/number}",
+      "milestones_url": "https://api.github.com/repos/bogus/bogus/milestones{/number}",
+      "notifications_url": "https://api.github.com/repos/bogus/bogus/notifications{?since,all,participating}",
+      "labels_url": "https://api.github.com/repos/bogus/bogus/labels{/name}",
+      "releases_url": "https://api.github.com/repos/bogus/bogus/releases{/id}",
+      "deployments_url": "https://api.github.com/repos/bogus/bogus/deployments",
+      "created_at": "2018-08-30T17:18:24Z",
+      "updated_at": "2022-06-14T22:15:20Z",
+      "pushed_at": "2022-07-15T20:04:26Z",
+      "git_url": "git://github.com/bogus/bogus.git",
+      "ssh_url": "git@github.com:bogus/bogus.git",
+      "clone_url": "https://github.com/bogus/bogus.git",
+      "svn_url": "https://github.com/bogus/bogus",
+      "homepage": null,
+      "size": 36,
+      "stargazers_count": 0,
+      "watchers_count": 0,
+      "language": "Groovy",
+      "has_issues": true,
+      "has_projects": true,
+      "has_downloads": true,
+      "has_wiki": true,
+      "has_pages": false,
+      "forks_count": 0,
+      "mirror_url": null,
+      "archived": false,
+      "disabled": false,
+      "open_issues_count": 0,
+      "license": null,
+      "allow_forking": true,
+      "is_template": false,
+      "web_commit_signoff_required": false,
+      "topics": [],
+      "visibility": "private",
+      "forks": 0,
+      "open_issues": 0,
+      "watchers": 0,
+      "default_branch": "master"
+    }
+  ]
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/users_bogus_installation-2.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/users_bogus_installation-2.json
new file mode 100644
index 000000000..07b4061b6
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/__files/users_bogus_installation-2.json
@@ -0,0 +1,45 @@
+{
+  "id": 11111111,
+  "account": {
+    "login": "bogus",
+    "id": 11111111,
+    "node_id": "asdfasdfasdf=",
+    "avatar_url": "https://avatars2.githubusercontent.com/u/111111111?v=4",
+    "gravatar_id": "",
+    "url": "https://api.github.com/users/bogus",
+    "html_url": "https://github.com/bogus",
+    "followers_url": "https://api.github.com/users/bogus/followers",
+    "following_url": "https://api.github.com/users/bogus/following{/other_user}",
+    "gists_url": "https://api.github.com/users/bogus/gists{/gist_id}",
+    "starred_url": "https://api.github.com/users/bogus/starred{/owner}{/repo}",
+    "subscriptions_url": "https://api.github.com/users/bogus/subscriptions",
+    "organizations_url": "https://api.github.com/users/bogus/orgs",
+    "repos_url": "https://api.github.com/users/bogus/repos",
+    "events_url": "https://api.github.com/users/bogus/events{/privacy}",
+    "received_events_url": "https://api.github.com/users/bogus/received_events",
+    "type": "Organization",
+    "site_admin": false
+  },
+  "repository_selection": "all",
+  "access_tokens_url": "https://api.github.com/app/installations/11111111/access_tokens",
+  "repositories_url": "https://api.github.com/installation/repositories",
+  "html_url": "https://github.com/settings/installations/11111111",
+  "app_id": 111111,
+  "app_slug": "bogus-testing",
+  "target_id": 11111111,
+  "target_type": "Organization",
+  "permissions": {
+    "checks": "write",
+    "pull_requests": "write",
+    "contents": "read",
+    "metadata": "read"
+  },
+  "events": [],
+  "created_at": "2022-07-15T17:55:36.000Z",
+  "updated_at": "2022-07-15T19:15:12.000Z",
+  "single_file_name": null,
+  "has_multiple_single_files": false,
+  "single_file_paths": [],
+  "suspended_by": null,
+  "suspended_at": null
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app-1.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app-1.json
new file mode 100644
index 000000000..06417be60
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app-1.json
@@ -0,0 +1,39 @@
+{
+  "id": "32a6d293-46e2-48e0-a7f5-cf9ec061cd02",
+  "name": "app",
+  "request": {
+    "url": "/app",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.machine-man-preview+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "app-1.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 27 Jul 2022 20:38:33 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "public, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"139628cbc6b3b04a0aa89130c8d2d7eb6935386aeaa396cbef97166394e38723\"",
+      "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "F861:6983:622AD5:115FE5B:62E1A249"
+    }
+  },
+  "uuid": "32a6d293-46e2-48e0-a7f5-cf9ec061cd02",
+  "persistent": true,
+  "insertionIndex": 1
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app_installations_27419505_access_tokens-3.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app_installations_27419505_access_tokens-3.json
new file mode 100644
index 000000000..cc68198f8
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/app_installations_27419505_access_tokens-3.json
@@ -0,0 +1,46 @@
+{
+  "id": "9e9f33d9-1b63-4f67-81a3-82ec45bd0afa",
+  "name": "app_installations_27419505_access_tokens",
+  "request": {
+    "url": "/app/installations/11111111/access_tokens",
+    "method": "POST",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.machine-man-preview+json"
+      }
+    },
+    "bodyPatterns": [
+      {
+        "equalToJson": "{\"repositories\":[\"bogus\"]}",
+        "ignoreArrayOrder": true,
+        "ignoreExtraElements": false
+      }
+    ]
+  },
+  "response": {
+    "status": 201,
+    "bodyFileName": "app_installations_27419505_access_tokens-3.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 27 Jul 2022 20:38:33 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "public, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "\"6cfb04630611056af08d2652a018429d37ca8e0df7d861b918a985d5e646e2de\"",
+      "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "F863:32FC:5F7C97:10FD23D:62E1A249"
+    }
+  },
+  "uuid": "9e9f33d9-1b63-4f67-81a3-82ec45bd0afa",
+  "persistent": true,
+  "insertionIndex": 3
+}
\ No newline at end of file
diff --git a/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/users_bogus_installation-2.json b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/users_bogus_installation-2.json
new file mode 100644
index 000000000..2f2f27a70
--- /dev/null
+++ b/src/test/resources/org/kohsuke/github/GHAppTest/wiremock/createTokenWithRepositories/mappings/users_bogus_installation-2.json
@@ -0,0 +1,39 @@
+{
+  "id": "b3d0192b-f564-437c-b705-d1662ba3f8c5",
+  "name": "users_bogus_installation",
+  "request": {
+    "url": "/users/bogus/installation",
+    "method": "GET",
+    "headers": {
+      "Accept": {
+        "equalTo": "application/vnd.github.machine-man-preview+json"
+      }
+    }
+  },
+  "response": {
+    "status": 200,
+    "bodyFileName": "users_bogus_installation-2.json",
+    "headers": {
+      "Server": "GitHub.com",
+      "Date": "Wed, 27 Jul 2022 20:38:33 GMT",
+      "Content-Type": "application/json; charset=utf-8",
+      "Cache-Control": "public, max-age=60, s-maxage=60",
+      "Vary": [
+        "Accept",
+        "Accept-Encoding, Accept, X-Requested-With"
+      ],
+      "ETag": "W/\"c95438f7909eebbaa54f451239b617b063a83ab1193afa1e180c144dd6295033\"",
+      "X-GitHub-Media-Type": "github.v3; param=machine-man-preview; format=json",
+      "Strict-Transport-Security": "max-age=31536000; includeSubdomains; preload",
+      "X-Frame-Options": "deny",
+      "X-Content-Type-Options": "nosniff",
+      "X-XSS-Protection": "0",
+      "Referrer-Policy": "origin-when-cross-origin, strict-origin-when-cross-origin",
+      "Content-Security-Policy": "default-src 'none'",
+      "X-GitHub-Request-Id": "F862:0322:510DC3:B06CF0:62E1A249"
+    }
+  },
+  "uuid": "b3d0192b-f564-437c-b705-d1662ba3f8c5",
+  "persistent": true,
+  "insertionIndex": 2
+}
\ No newline at end of file