Skip to content

Commit bc81a15

Browse files
authored
Merge pull request #74 from peter-evans/dev
Support triage and maintain permission levels
2 parents 0495b44 + d07bfe3 commit bc81a15

File tree

10 files changed

+170
-26
lines changed

10 files changed

+170
-26
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,7 @@ This action also features [advanced configuration](docs/advanced-configuration.m
7575
| `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` |
7676
| `reactions` | Add reactions. :eyes: = seen, :rocket: = dispatched | `true` |
7777
| `commands` | (**required**) A comma or newline separated list of commands. | |
78-
| `permission` | The repository permission level required by the user to dispatch commands. (`none`, `read`, `write`, `admin`) | `write` |
78+
| `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` |
7979
| `issue-type` | The issue type required for commands. (`issue`, `pull-request`, `both`) | `both` |
8080
| `allow-edits` | Allow edited comments to trigger command dispatches. | `false` |
8181
| `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
109109
build-docs
110110
```
111111

112+
#### `permission`
113+
114+
This input sets the repository permission level required by the user to dispatch commands.
115+
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`.
116+
From the least to greatest permission level they are `none`, `read`, `triage`, `write`, `maintain` and `admin`.
117+
118+
Setting `write` as the required permission level means that any user with `write`, `maintain` or `admin` will be able to execute commands.
119+
120+
Note that `read`, `triage` and `maintain` only make sense for organization repositories.
121+
For repositories owned by a user account there are only two permission levels, the repository owner (`admin`) and collaborators (`write`).
122+
112123
#### `dispatch-type`
113124

114125
By default, the action creates [repository_dispatch](https://developer.github.com/v3/repos/#create-a-repository-dispatch-event) events.

__test__/command-helper.test.ts renamed to __test__/command-helper.unit.test.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,13 +206,16 @@ describe('command-helper tests', () => {
206206

207207
test('actor does not have permission', async () => {
208208
expect(actorHasPermission('none', 'read')).toBeFalsy()
209-
expect(actorHasPermission('read', 'write')).toBeFalsy()
210-
expect(actorHasPermission('write', 'admin')).toBeFalsy()
209+
expect(actorHasPermission('read', 'triage')).toBeFalsy()
210+
expect(actorHasPermission('triage', 'write')).toBeFalsy()
211+
expect(actorHasPermission('write', 'maintain')).toBeFalsy()
212+
expect(actorHasPermission('maintain', 'admin')).toBeFalsy()
211213
})
212214

213215
test('actor has permission', async () => {
214216
expect(actorHasPermission('read', 'none')).toBeTruthy()
215-
expect(actorHasPermission('write', 'read')).toBeTruthy()
217+
expect(actorHasPermission('triage', 'read')).toBeTruthy()
218+
expect(actorHasPermission('write', 'triage')).toBeTruthy()
216219
expect(actorHasPermission('admin', 'write')).toBeTruthy()
217220
expect(actorHasPermission('write', 'write')).toBeTruthy()
218221
})

__test__/github-helper.int.test.ts

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import {GitHubHelper} from '../lib/github-helper'
2+
3+
const token: string = process.env['REPO_SCOPED_PAT'] || 'not set'
4+
5+
describe('github-helper tests', () => {
6+
it('tests getActorPermission returns "none" for non-existent collaborators', async () => {
7+
const githubHelper = new GitHubHelper(token)
8+
const actorPermission = await githubHelper.getActorPermission(
9+
{owner: 'peter-evans', repo: 'slash-command-dispatch'},
10+
'collaborator-does-not-exist'
11+
)
12+
expect(actorPermission).toEqual('none')
13+
})
14+
15+
it('tests getActorPermission returns "admin"', async () => {
16+
const githubHelper = new GitHubHelper(token)
17+
const actorPermission = await githubHelper.getActorPermission(
18+
{owner: 'peter-evans', repo: 'slash-command-dispatch'},
19+
'peter-evans'
20+
)
21+
expect(actorPermission).toEqual('admin')
22+
})
23+
24+
it('tests getActorPermission returns "write"', async () => {
25+
const githubHelper = new GitHubHelper(token)
26+
const actorPermission = await githubHelper.getActorPermission(
27+
{owner: 'peter-evans', repo: 'slash-command-dispatch'},
28+
'actions-bot'
29+
)
30+
expect(actorPermission).toEqual('write')
31+
})
32+
33+
it('tests getActorPermission returns "triage" for an org repository collaborator', async () => {
34+
const githubHelper = new GitHubHelper(token)
35+
const actorPermission = await githubHelper.getActorPermission(
36+
{owner: 'slash-command-dispatch', repo: 'integration-test-fixture'},
37+
'test-case-machine-user'
38+
)
39+
expect(actorPermission).toEqual('triage')
40+
})
41+
})

dist/index.js

Lines changed: 41 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1052,7 +1052,7 @@ function getCommandsConfigFromJson(json) {
10521052
exports.getCommandsConfigFromJson = getCommandsConfigFromJson;
10531053
function configIsValid(config) {
10541054
for (const command of config) {
1055-
if (!['none', 'read', 'write', 'admin'].includes(command.permission)) {
1055+
if (!['none', 'read', 'triage', 'write', 'maintain', 'admin'].includes(command.permission)) {
10561056
core.setFailed(`'${command.permission}' is not a valid 'permission'.`);
10571057
return false;
10581058
}
@@ -1072,8 +1072,10 @@ function actorHasPermission(actorPermission, commandPermission) {
10721072
const permissionLevels = Object.freeze({
10731073
none: 1,
10741074
read: 2,
1075-
write: 3,
1076-
admin: 4
1075+
triage: 3,
1076+
write: 4,
1077+
maintain: 5,
1078+
admin: 6
10771079
});
10781080
core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`);
10791081
core.debug(`Command permission level: ${permissionLevels[commandPermission]}`);
@@ -4720,13 +4722,19 @@ Object.defineProperty(exports, "__esModule", { value: true });
47204722
exports.GitHubHelper = void 0;
47214723
const core = __importStar(__webpack_require__(470));
47224724
const octokit_client_1 = __webpack_require__(921);
4725+
const util_1 = __webpack_require__(669);
47234726
class GitHubHelper {
47244727
constructor(token) {
47254728
const options = {};
47264729
if (token) {
47274730
options.auth = `${token}`;
47284731
}
47294732
this.octokit = new octokit_client_1.Octokit(options);
4733+
this.graphqlClient = octokit_client_1.graphql.defaults({
4734+
headers: {
4735+
authorization: `token ${token}`
4736+
}
4737+
});
47304738
}
47314739
parseRepository(repository) {
47324740
const [owner, repo] = repository.split('/');
@@ -4737,8 +4745,23 @@ class GitHubHelper {
47374745
}
47384746
getActorPermission(repo, actor) {
47394747
return __awaiter(this, void 0, void 0, function* () {
4740-
const { data: { permission } } = yield this.octokit.repos.getCollaboratorPermissionLevel(Object.assign(Object.assign({}, repo), { username: actor }));
4741-
return permission;
4748+
// https://docs.github.com/en/graphql/reference/enums#repositorypermission
4749+
// https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge
4750+
// Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN'
4751+
const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) {
4752+
repository(owner:$owner, name:$repo) {
4753+
collaborators(query: $collaborator) {
4754+
edges {
4755+
permission
4756+
}
4757+
}
4758+
}
4759+
}`;
4760+
const collaboratorPermission = yield this.graphqlClient(query, Object.assign(Object.assign({}, repo), { collaborator: actor }));
4761+
core.debug(`CollaboratorPermission: ${util_1.inspect(collaboratorPermission.repository.collaborators.edges)}`);
4762+
return collaboratorPermission.repository.collaborators.edges.length > 0
4763+
? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase()
4764+
: 'none';
47424765
});
47434766
}
47444767
tryAddReaction(repo, commentId, reaction) {
@@ -6327,13 +6350,16 @@ Object.defineProperty(exports, '__esModule', { value: true });
63276350
var request = __webpack_require__(753);
63286351
var universalUserAgent = __webpack_require__(796);
63296352

6330-
const VERSION = "4.5.2";
6353+
const VERSION = "4.5.3";
63316354

63326355
class GraphqlError extends Error {
63336356
constructor(request, response) {
63346357
const message = response.data.errors[0].message;
63356358
super(message);
63366359
Object.assign(this, response.data);
6360+
Object.assign(this, {
6361+
headers: response.headers
6362+
});
63376363
this.name = "GraphqlError";
63386364
this.request = request; // Maintains proper stack trace (only available on V8)
63396365

@@ -6366,7 +6392,14 @@ function graphql(request, query, options) {
63666392
}, {});
63676393
return request(requestOptions).then(response => {
63686394
if (response.data.errors) {
6395+
const headers = {};
6396+
6397+
for (const key of Object.keys(response.headers)) {
6398+
headers[key] = response.headers[key];
6399+
}
6400+
63696401
throw new GraphqlError(requestOptions, {
6402+
headers,
63706403
data: response.data
63716404
});
63726405
}
@@ -6420,6 +6453,8 @@ const core_1 = __webpack_require__(448);
64206453
const plugin_paginate_rest_1 = __webpack_require__(299);
64216454
const plugin_rest_endpoint_methods_1 = __webpack_require__(842);
64226455
exports.Octokit = core_1.Octokit.plugin(plugin_paginate_rest_1.paginateRest, plugin_rest_endpoint_methods_1.restEndpointMethods);
6456+
var graphql_1 = __webpack_require__(898);
6457+
Object.defineProperty(exports, "graphql", { enumerable: true, get: function () { return graphql_1.graphql; } });
64236458

64246459

64256460
/***/ }),

docs/advanced-configuration.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,7 @@ Advanced configuration requires a combination of yaml based inputs and JSON conf
100100
| `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` |
101101
| `reactions` | | Add reactions. :eyes: = seen, :rocket: = dispatched | `true` |
102102
| | `command` | (**required**) The slash command. | |
103-
| | `permission` | The repository permission level required by the user to dispatch the command. (`none`, `read`, `write`, `admin`) | `write` |
103+
| | `permission` | The repository permission level required by the user to dispatch the command. (`none`, `read`, `triage`, `write`, `maintain`, `admin`) | `write` |
104104
| | `issue_type` | The issue type required for the command. (`issue`, `pull-request`, `both`) | `both` |
105105
| | `allow_edits` | Allow edited comments to trigger command dispatches. | `false` |
106106
| | `repository` | The full name of the repository to send the dispatch events. | Current repository |

package-lock.json

Lines changed: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@
99
"format": "prettier --write '**/*.ts'",
1010
"format-check": "prettier --check '**/*.ts'",
1111
"lint": "eslint src/**/*.ts",
12-
"test": "jest"
12+
"test": "jest unit",
13+
"test:int": "jest int"
1314
},
1415
"repository": {
1516
"type": "git",
@@ -30,6 +31,7 @@
3031
"@actions/core": "1.2.4",
3132
"@actions/github": "4.0.0",
3233
"@octokit/core": "3.1.1",
34+
"@octokit/graphql": "4.5.3",
3335
"@octokit/plugin-paginate-rest": "2.2.3",
3436
"@octokit/plugin-rest-endpoint-methods": "4.1.0",
3537
"@octokit/types": "5.1.0"

src/command-helper.ts

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,7 +156,11 @@ export function getCommandsConfigFromJson(json: string): Command[] {
156156

157157
export function configIsValid(config: Command[]): boolean {
158158
for (const command of config) {
159-
if (!['none', 'read', 'write', 'admin'].includes(command.permission)) {
159+
if (
160+
!['none', 'read', 'triage', 'write', 'maintain', 'admin'].includes(
161+
command.permission
162+
)
163+
) {
160164
core.setFailed(`'${command.permission}' is not a valid 'permission'.`)
161165
return false
162166
}
@@ -181,8 +185,10 @@ export function actorHasPermission(
181185
const permissionLevels = Object.freeze({
182186
none: 1,
183187
read: 2,
184-
write: 3,
185-
admin: 4
188+
triage: 3,
189+
write: 4,
190+
maintain: 5,
191+
admin: 6
186192
})
187193
core.debug(`Actor permission level: ${permissionLevels[actorPermission]}`)
188194
core.debug(`Command permission level: ${permissionLevels[commandPermission]}`)

src/github-helper.ts

Lines changed: 50 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
import * as core from '@actions/core'
2-
import {Octokit, OctokitOptions, PullsGetResponseData} from './octokit-client'
2+
import {
3+
graphql,
4+
Graphql,
5+
Octokit,
6+
OctokitOptions,
7+
PullsGetResponseData
8+
} from './octokit-client'
39
import {Command, SlashCommandPayload} from './command-helper'
10+
import {inspect} from 'util'
411

512
type ReposCreateDispatchEventParamsClientPayload = {
613
[key: string]: ReposCreateDispatchEventParamsClientPayloadKeyString
@@ -23,15 +30,33 @@ interface Repository {
2330
repo: string
2431
}
2532

33+
type CollaboratorPermission = {
34+
repository: {
35+
collaborators: {
36+
edges: [
37+
{
38+
permission: string
39+
}
40+
]
41+
}
42+
}
43+
}
44+
2645
export class GitHubHelper {
2746
private octokit: InstanceType<typeof Octokit>
47+
private graphqlClient: Graphql
2848

2949
constructor(token: string) {
3050
const options: OctokitOptions = {}
3151
if (token) {
3252
options.auth = `${token}`
3353
}
3454
this.octokit = new Octokit(options)
55+
this.graphqlClient = graphql.defaults({
56+
headers: {
57+
authorization: `token ${token}`
58+
}
59+
})
3560
}
3661

3762
private parseRepository(repository: string): Repository {
@@ -43,13 +68,32 @@ export class GitHubHelper {
4368
}
4469

4570
async getActorPermission(repo: Repository, actor: string): Promise<string> {
46-
const {
47-
data: {permission}
48-
} = await this.octokit.repos.getCollaboratorPermissionLevel({
71+
// https://docs.github.com/en/graphql/reference/enums#repositorypermission
72+
// https://docs.github.com/en/graphql/reference/objects#repositorycollaboratoredge
73+
// Returns 'READ', 'TRIAGE', 'WRITE', 'MAINTAIN', 'ADMIN'
74+
const query = `query CollaboratorPermission($owner: String!, $repo: String!, $collaborator: String) {
75+
repository(owner:$owner, name:$repo) {
76+
collaborators(query: $collaborator) {
77+
edges {
78+
permission
79+
}
80+
}
81+
}
82+
}`
83+
const collaboratorPermission = await this.graphqlClient<
84+
CollaboratorPermission
85+
>(query, {
4986
...repo,
50-
username: actor
87+
collaborator: actor
5188
})
52-
return permission
89+
core.debug(
90+
`CollaboratorPermission: ${inspect(
91+
collaboratorPermission.repository.collaborators.edges
92+
)}`
93+
)
94+
return collaboratorPermission.repository.collaborators.edges.length > 0
95+
? collaboratorPermission.repository.collaborators.edges[0].permission.toLowerCase()
96+
: 'none'
5397
}
5498

5599
async tryAddReaction(

src/octokit-client.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {Octokit as Core} from '@octokit/core'
22
import {paginateRest} from '@octokit/plugin-paginate-rest'
33
import {restEndpointMethods} from '@octokit/plugin-rest-endpoint-methods'
4+
45
export {RestEndpointMethodTypes} from '@octokit/plugin-rest-endpoint-methods'
56
export {OctokitOptions} from '@octokit/core/dist-types/types'
6-
77
export const Octokit = Core.plugin(paginateRest, restEndpointMethods)
8-
98
export {PullsGetResponseData} from '@octokit/types'
9+
10+
export {graphql} from '@octokit/graphql'
11+
export {graphql as Graphql} from '@octokit/graphql/dist-types/types'

0 commit comments

Comments
 (0)