diff --git a/README.md b/README.md index bafe515..0858ffa 100644 --- a/README.md +++ b/README.md @@ -75,7 +75,7 @@ This action also features [advanced configuration](docs/advanced-configuration.m | `reaction-token` | `GITHUB_TOKEN` or a `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). See [reaction-token](#reaction-token) for further details. | `GITHUB_TOKEN` | | `reactions` | Add reactions. :eyes: = seen, :rocket: = dispatched | `true` | | `commands` | (**required**) A comma or newline separated list of commands. | | -| `permission` | The repository permission level required by the user to dispatch commands. (`none`, `read`, `write`, `admin`) | `write` | +| `permission` | The repository permission level required by the user to dispatch commands. See [permission](#permission) for further details. (`none`, `read`, `triage`, `write`, `maintain`, `admin`) | `write` | | `issue-type` | The issue type required for commands. (`issue`, `pull-request`, `both`) | `both` | | `allow-edits` | Allow edited comments to trigger command dispatches. | `false` | | `repository` | The full name of the repository to send the dispatch events. | Current repository | @@ -109,6 +109,17 @@ You can use a [PAT](https://help.github.com/en/github/authenticating-to-github/c build-docs ``` +#### `permission` + +This input sets the repository permission level required by the user to dispatch commands. +It expects one of the [five repository permission levels](https://docs.github.com/en/github/setting-up-and-managing-organizations-and-teams/repository-permission-levels-for-an-organization#permission-levels-for-repositories-owned-by-an-organization), or `none`. +From the least to greatest permission level they are `none`, `read`, `triage`, `write`, `maintain` and `admin`. + +Setting `write` as the required permission level means that any user with `write`, `maintain` or `admin` will be able to execute commands. + +Note that `read`, `triage` and `maintain` only make sense for organization repositories. +For repositories owned by a user account there are only two permission levels, the repository owner (`admin`) and collaborators (`write`). + #### `dispatch-type` By default, the action creates [repository_dispatch](https://developer.github.com/v3/repos/#create-a-repository-dispatch-event) events. diff --git a/__test__/command-helper.test.ts b/__test__/command-helper.unit.test.ts similarity index 96% rename from __test__/command-helper.test.ts rename to __test__/command-helper.unit.test.ts index de6ac88..0785b51 100644 --- a/__test__/command-helper.test.ts +++ b/__test__/command-helper.unit.test.ts @@ -206,13 +206,16 @@ describe('command-helper tests', () => { test('actor does not have permission', async () => { expect(actorHasPermission('none', 'read')).toBeFalsy() - expect(actorHasPermission('read', 'write')).toBeFalsy() - expect(actorHasPermission('write', 'admin')).toBeFalsy() + expect(actorHasPermission('read', 'triage')).toBeFalsy() + expect(actorHasPermission('triage', 'write')).toBeFalsy() + expect(actorHasPermission('write', 'maintain')).toBeFalsy() + expect(actorHasPermission('maintain', 'admin')).toBeFalsy() }) test('actor has permission', async () => { expect(actorHasPermission('read', 'none')).toBeTruthy() - expect(actorHasPermission('write', 'read')).toBeTruthy() + expect(actorHasPermission('triage', 'read')).toBeTruthy() + expect(actorHasPermission('write', 'triage')).toBeTruthy() expect(actorHasPermission('admin', 'write')).toBeTruthy() expect(actorHasPermission('write', 'write')).toBeTruthy() }) diff --git a/__test__/github-helper.int.test.ts b/__test__/github-helper.int.test.ts new file mode 100644 index 0000000..4ae156d --- /dev/null +++ b/__test__/github-helper.int.test.ts @@ -0,0 +1,41 @@ +import {GitHubHelper} from '../lib/github-helper' + +const token: string = process.env['REPO_SCOPED_PAT'] || 'not set' + +describe('github-helper tests', () => { + it('tests getActorPermission returns "none" for non-existent collaborators', async () => { + const githubHelper = new GitHubHelper(token) + const actorPermission = await githubHelper.getActorPermission( + {owner: 'peter-evans', repo: 'slash-command-dispatch'}, + 'collaborator-does-not-exist' + ) + expect(actorPermission).toEqual('none') + }) + + it('tests getActorPermission returns "admin"', async () => { + const githubHelper = new GitHubHelper(token) + const actorPermission = await githubHelper.getActorPermission( + {owner: 'peter-evans', repo: 'slash-command-dispatch'}, + 'peter-evans' + ) + expect(actorPermission).toEqual('admin') + }) + + it('tests getActorPermission returns "write"', async () => { + const githubHelper = new GitHubHelper(token) + const actorPermission = await githubHelper.getActorPermission( + {owner: 'peter-evans', repo: 'slash-command-dispatch'}, + 'actions-bot' + ) + expect(actorPermission).toEqual('write') + }) + + it('tests getActorPermission returns "triage" for an org repository collaborator', async () => { + const githubHelper = new GitHubHelper(token) + const actorPermission = await githubHelper.getActorPermission( + {owner: 'slash-command-dispatch', repo: 'integration-test-fixture'}, + 'test-case-machine-user' + ) + expect(actorPermission).toEqual('triage') + }) +}) diff --git a/dist/index.js b/dist/index.js index 7891726..34cb47c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1052,7 +1052,7 @@ function getCommandsConfigFromJson(json) { exports.getCommandsConfigFromJson = getCommandsConfigFromJson; function configIsValid(config) { for (const command of config) { - if (!['none', 'read', 'write', 'admin'].includes(command.permission)) { + if (!['none', 'read', 'triage', 'write', 'maintain', 'admin'].includes(command.permission)) { core.setFailed(`'${command.permission}' is not a valid 'permission'.`); return false; } @@ -1072,8 +1072,10 @@ function actorHasPermission(actorPermission, commandPermission) { const permissionLevels = Object.freeze({ none: 1, read: 2, - write: 3, - admin: 4 + triage: 3, + write: 4, + maintain: 5, + admin: 6 }); core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`); core.debug(`Command permission level: ${permissionLevels[commandPermission]}`); @@ -4720,6 +4722,7 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.GitHubHelper = void 0; const core = __importStar(__webpack_require__(470)); const octokit_client_1 = __webpack_require__(921); +const util_1 = __webpack_require__(669); class GitHubHelper { constructor(token) { const options = {}; @@ -4727,6 +4730,11 @@ class GitHubHelper { options.auth = `${token}`; } this.octokit = new octokit_client_1.Octokit(options); + this.graphqlClient = octokit_client_1.graphql.defaults({ + headers: { + authorization: `token ${token}` + } + }); } parseRepository(repository) { const [owner, repo] = repository.split('/'); @@ -4737,8 +4745,23 @@ class GitHubHelper { } getActorPermission(repo, actor) { return __awaiter(this, void 0, void 0, function* () { - const { data: { permission } } = yield this.octokit.repos.getCollaboratorPermissionLevel(Object.assign(Object.assign({}, repo), { username: actor })); - return permission; + // https://docs.github.com/en/graphql/reference/enums#repositorypermission + // https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge + // Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN' + const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) { + repository(owner:$owner, name:$repo) { + collaborators(query: $collaborator) { + edges { + permission + } + } + } + }`; + const collaboratorPermission = yield this.graphqlClient(query, Object.assign(Object.assign({}, repo), { collaborator: actor })); + core.debug(`CollaboratorPermission: ${util_1.inspect(collaboratorPermission.repository.collaborators.edges)}`); + return collaboratorPermission.repository.collaborators.edges.length > 0 + ? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase() + : 'none'; }); } tryAddReaction(repo, commentId, reaction) { @@ -6327,13 +6350,16 @@ Object.defineProperty(exports, '__esModule', { value: true }); var request = __webpack_require__(753); var universalUserAgent = __webpack_require__(796); -const VERSION = "4.5.2"; +const VERSION = "4.5.3"; class GraphqlError extends Error { constructor(request, response) { const message = response.data.errors[0].message; super(message); Object.assign(this, response.data); + Object.assign(this, { + headers: response.headers + }); this.name = "GraphqlError"; this.request = request; // Maintains proper stack trace (only available on V8) @@ -6366,7 +6392,14 @@ function graphql(request, query, options) { }, {}); return request(requestOptions).then(response => { if (response.data.errors) { + const headers = {}; + + for (const key of Object.keys(response.headers)) { + headers[key] = response.headers[key]; + } + throw new GraphqlError(requestOptions, { + headers, data: response.data }); } @@ -6420,6 +6453,8 @@ const core_1 = __webpack_require__(448); const plugin_paginate_rest_1 = __webpack_require__(299); const plugin_rest_endpoint_methods_1 = __webpack_require__(842); exports.Octokit = core_1.Octokit.plugin(plugin_paginate_rest_1.paginateRest, plugin_rest_endpoint_methods_1.restEndpointMethods); +var graphql_1 = __webpack_require__(898); +Object.defineProperty(exports, "graphql", { enumerable: true, get: function () { return graphql_1.graphql; } }); /***/ }), diff --git a/docs/advanced-configuration.md b/docs/advanced-configuration.md index 7fcb516..c509283 100644 --- a/docs/advanced-configuration.md +++ b/docs/advanced-configuration.md @@ -100,7 +100,7 @@ Advanced configuration requires a combination of yaml based inputs and JSON conf | `reaction-token` | | `GITHUB_TOKEN` or a `repo` scoped [Personal Access Token (PAT)](https://help.github.com/en/github/authenticating-to-github/creating-a-personal-access-token-for-the-command-line). See [reaction-token](https://github.com/peter-evans/slash-command-dispatch#reaction-token) for further details. | `GITHUB_TOKEN` | | `reactions` | | Add reactions. :eyes: = seen, :rocket: = dispatched | `true` | | | `command` | (**required**) The slash command. | | -| | `permission` | The repository permission level required by the user to dispatch the command. (`none`, `read`, `write`, `admin`) | `write` | +| | `permission` | The repository permission level required by the user to dispatch the command. (`none`, `read`, `triage`, `write`, `maintain`, `admin`) | `write` | | | `issue_type` | The issue type required for the command. (`issue`, `pull-request`, `both`) | `both` | | | `allow_edits` | Allow edited comments to trigger command dispatches. | `false` | | | `repository` | The full name of the repository to send the dispatch events. | Current repository | diff --git a/package-lock.json b/package-lock.json index 5f33216..e550a8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -946,9 +946,9 @@ } }, "@octokit/graphql": { - "version": "4.5.2", - "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.2.tgz", - "integrity": "sha512-SpB/JGdB7bxRj8qowwfAXjMpICUYSJqRDj26MKJAryRQBqp/ZzARsaO2LEFWzDaps0FLQoPYVGppS0HQXkBhdg==", + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-4.5.3.tgz", + "integrity": "sha512-JyYvi3j2tOb5ofASEpcg1Advs07H+Ag+I+ez7buuZfNVAmh1IYcDTuxd4gnYH8S2PSGu+f5IdDGxMmkK+5zsdA==", "requires": { "@octokit/request": "^5.3.0", "@octokit/types": "^5.0.0", diff --git a/package.json b/package.json index 99f4f6d..f1954a3 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,8 @@ "format": "prettier --write '**/*.ts'", "format-check": "prettier --check '**/*.ts'", "lint": "eslint src/**/*.ts", - "test": "jest" + "test": "jest unit", + "test:int": "jest int" }, "repository": { "type": "git", @@ -30,6 +31,7 @@ "@actions/core": "1.2.4", "@actions/github": "4.0.0", "@octokit/core": "3.1.1", + "@octokit/graphql": "4.5.3", "@octokit/plugin-paginate-rest": "2.2.3", "@octokit/plugin-rest-endpoint-methods": "4.1.0", "@octokit/types": "5.1.0" diff --git a/src/command-helper.ts b/src/command-helper.ts index e5cac6e..18b3258 100644 --- a/src/command-helper.ts +++ b/src/command-helper.ts @@ -156,7 +156,11 @@ export function getCommandsConfigFromJson(json: string): Command[] { export function configIsValid(config: Command[]): boolean { for (const command of config) { - if (!['none', 'read', 'write', 'admin'].includes(command.permission)) { + if ( + !['none', 'read', 'triage', 'write', 'maintain', 'admin'].includes( + command.permission + ) + ) { core.setFailed(`'${command.permission}' is not a valid 'permission'.`) return false } @@ -181,8 +185,10 @@ export function actorHasPermission( const permissionLevels = Object.freeze({ none: 1, read: 2, - write: 3, - admin: 4 + triage: 3, + write: 4, + maintain: 5, + admin: 6 }) core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`) core.debug(`Command permission level: ${permissionLevels[commandPermission]}`) diff --git a/src/github-helper.ts b/src/github-helper.ts index a5d2d8d..c6ab59f 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -1,6 +1,13 @@ import * as core from '@actions/core' -import {Octokit, OctokitOptions, PullsGetResponseData} from './octokit-client' +import { + graphql, + Graphql, + Octokit, + OctokitOptions, + PullsGetResponseData +} from './octokit-client' import {Command, SlashCommandPayload} from './command-helper' +import {inspect} from 'util' type ReposCreateDispatchEventParamsClientPayload = { [key: string]: ReposCreateDispatchEventParamsClientPayloadKeyString @@ -23,8 +30,21 @@ interface Repository { repo: string } +type CollaboratorPermission = { + repository: { + collaborators: { + edges: [ + { + permission: string + } + ] + } + } +} + export class GitHubHelper { private octokit: InstanceType + private graphqlClient: Graphql constructor(token: string) { const options: OctokitOptions = {} @@ -32,6 +52,11 @@ export class GitHubHelper { options.auth = `${token}` } this.octokit = new Octokit(options) + this.graphqlClient = graphql.defaults({ + headers: { + authorization: `token ${token}` + } + }) } private parseRepository(repository: string): Repository { @@ -43,13 +68,32 @@ export class GitHubHelper { } async getActorPermission(repo: Repository, actor: string): Promise { - const { - data: {permission} - } = await this.octokit.repos.getCollaboratorPermissionLevel({ + // https://docs.github.com/en/graphql/reference/enums#repositorypermission + // https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge + // Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN' + const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) { + repository(owner:$owner, name:$repo) { + collaborators(query: $collaborator) { + edges { + permission + } + } + } + }` + const collaboratorPermission = await this.graphqlClient< + CollaboratorPermission + >(query, { ...repo, - username: actor + collaborator: actor }) - return permission + core.debug( + `CollaboratorPermission: ${inspect( + collaboratorPermission.repository.collaborators.edges + )}` + ) + return collaboratorPermission.repository.collaborators.edges.length > 0 + ? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase() + : 'none' } async tryAddReaction( diff --git a/src/octokit-client.ts b/src/octokit-client.ts index a13b1a6..efaa735 100644 --- a/src/octokit-client.ts +++ b/src/octokit-client.ts @@ -1,9 +1,11 @@ import {Octokit as Core} from '@octokit/core' import {paginateRest} from '@octokit/plugin-paginate-rest' import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods' + export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods' export {OctokitOptions} from '@octokit/core/dist-types/types' - export const Octokit = Core.plugin(paginateRest, restEndpointMethods) - export {PullsGetResponseData} from '@octokit/types' + +export {graphql} from '@octokit/graphql' +export {graphql as Graphql} from '@octokit/graphql/dist-types/types'