Skip to content

Commit

Permalink
feat: Toggle pull-request draft status (#485)
Browse files Browse the repository at this point in the history
Close #436

## Overview

My proposal about a REST API to toggle pull-request draft status ([pulls.update: Change pull-request draft status · Issue #69 · octokit/rest.js](octokit/rest.js#69)) has been rejected since the GitHub API v4 (GraphQL API) has the ability to do that.

Unfortunately, an auto-generated access token called `GITHUB_TOKEN` seems not to have full control permission of the GraphQL API. We got the error `Resource not accessible by integration` with the GitHub API v4 and `GITHUB_TOKEN`. This problem may be related to the following thread.

- [API v4 - Unable to Retrieve Email - Resource not accessible by integration - GitHub Ecosystem - GitHub Support Community](https://github.community/t/api-v4-unable-to-retrieve-email-resource-not-accessible-by-integration/13831/3)

This pull-request uses a personal access token with a `public_repo` scope (for a public repository) as a workaround.

## References

- [convertPullRequestToDraft - Mutations - GitHub Docs](https://docs.github.com/en/graphql/reference/mutations#convertpullrequesttodraft)
- [markPullRequestReadyForReview - Mutations - GitHub Docs](https://docs.github.com/en/graphql/reference/mutations#markpullrequestreadyforreview)
- https://github.com/actions/toolkit/blob/%40actions/tool-cache%401.1.1/packages/github/README.md?plain=1#L32
	- [octokit/graphql.js: GitHub GraphQL API client for browsers and Node](https://github.com/octokit/graphql.js)
	- [octokit/graphql-schema: GitHub’s GraphQL Schema with validation. Automatically updated.](https://github.com/octokit/graphql-schema)
  • Loading branch information
peaceiris authored Aug 11, 2021
1 parent 1c813ca commit 2756d58
Show file tree
Hide file tree
Showing 16 changed files with 506 additions and 291 deletions.
1 change: 1 addition & 0 deletions .github/label-commenter-config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ labels:
pr:
body: Thank you @{{ pull_request.user.login }} for suggesting this. Please follow the pull request templates.
action: close
draft: true
unlabeled:
issue:
body: Thank you for following the template. The repository owner will reply.
Expand Down
13 changes: 7 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,12 @@ all:
release:
bash ./scripts/release.sh

.PHONY: pre-release
pre-release:
.PHONY: create-pre-release
create-pre-release:
bash ./scripts/pre-release.sh
git rev-parse HEAD

.PHONY: remove-pre-release
remove-pre-release:
git rm -f ./lib/index.js
git commit -m "chore: remove lib/index.js"
.PHONY: pre-release
pre-release: create-pre-release
git rm -f ./lib/*
git commit -m "chore(release): Remove build assets [skip ci]"
331 changes: 184 additions & 147 deletions __tests__/classes/action-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ const commentBody = `hello`;
const githubClient = getOctokit('token');
const issueMock: Issue = {
githubClient: githubClient,
id: 'MDExOlB1bGxSZXF1ZXN0NzA2MTE5NTg0',
number: 1,
locked: false,
setLocked: jest.fn(),
createComment: jest.fn(),
updateState: jest.fn(),
lock: jest.fn(),
unlock: jest.fn()
unlock: jest.fn(),
markPullRequestReadyForReview: jest.fn(),
convertPullRequestToDraft: jest.fn()
};
const tests = ['issue', 'pr'];

// beforeAll(() => {
// });
Expand All @@ -25,157 +29,190 @@ afterEach(() => {
jest.resetModules();
});

describe('issue', () => {
test('Comment and close', async () => {
const config: IConfig = {
parentFieldName: 'labels.invalid.labeled.issue',
labelIndex: '0',
action: 'close',
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Comment and close', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.invalid.labeled.${t}`,
labelIndex: '0',
action: 'close',
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Comment, close, and lock without lockReason', async () => {
const config: IConfig = {
parentFieldName: 'labels.locked (resolved).labeled.issue',
labelIndex: '0',
action: 'close',
locking: 'lock',
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Comment, close, and lock without lockReason', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.locked (resolved).labeled.${t}`,
labelIndex: '0',
action: 'close',
locking: 'lock',
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Comment, close, and lock with lockReason', async () => {
const config: IConfig = {
parentFieldName: 'labels.locked (spam).labeled.issue',
labelIndex: '0',
action: 'close',
locking: 'lock',
lockReason: 'spam'
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.lock).toBeCalledWith('spam');
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Comment, close, and lock with lockReason', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.locked (spam).labeled.${t}`,
labelIndex: '0',
action: 'close',
locking: 'lock',
lockReason: 'spam'
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.lock).toBeCalledWith('spam');
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Unlock, open and comment', async () => {
const config: IConfig = {
parentFieldName: 'labels.locked (heated).labeled.issue',
labelIndex: '0',
action: 'open',
locking: 'unlock',
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(1);
});
describe('Unlock, open and comment', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.locked (heated).labeled.${t}`,
labelIndex: '0',
action: 'open',
locking: 'unlock',
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(1);
});
}
});

test('Comment and open', async () => {
const config: IConfig = {
parentFieldName: 'labels.invalid.labeled.issue',
labelIndex: '0',
action: 'open',
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Comment and open', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.invalid.labeled.${t}`,
labelIndex: '0',
action: 'open',
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(1);
expect(issueMock.createComment).toBeCalledWith(commentBody);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Open without comment if the issue is locked', async () => {
const config: IConfig = {
parentFieldName: 'labels.invalid.labeled.issue',
labelIndex: '0',
action: 'open',
locking: undefined,
lockReason: undefined
};
const issueMock: Issue = {
githubClient: githubClient,
number: 1,
locked: true,
setLocked: jest.fn(),
createComment: jest.fn(),
updateState: jest.fn(),
lock: jest.fn(),
unlock: jest.fn()
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Open without comment if the issue is locked', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.invalid.labeled.${t}`,
labelIndex: '0',
action: 'open',
locking: undefined,
lockReason: undefined
};
const issueMock: Issue = {
githubClient: githubClient,
id: 'MDExOlB1bGxSZXF1ZXN0NzA2MTE5NTg0',
number: 1,
locked: true,
setLocked: jest.fn(),
createComment: jest.fn(),
updateState: jest.fn(),
lock: jest.fn(),
unlock: jest.fn(),
markPullRequestReadyForReview: jest.fn(),
convertPullRequestToDraft: jest.fn()
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('open');
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Skip all actions for a label that has no configuration', async () => {
const config: IConfig = {
parentFieldName: 'labels.unknown.labeled.issue',
labelIndex: '',
action: undefined,
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(0);
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Skip all actions for a label that has no configuration', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.unknown.labeled.${t}`,
labelIndex: '',
action: undefined,
locking: undefined,
lockReason: undefined
};
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(0);
expect(issueMock.lock).toBeCalledTimes(0);
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});

test('Skip comment if body is empty', async () => {
const config: IConfig = {
parentFieldName: 'labels.spam.labeled.issue',
labelIndex: '1',
action: 'close',
locking: 'lock',
lockReason: 'spam'
};
const commentBody = '';
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.lock).toBeCalledWith('spam');
expect(issueMock.unlock).toBeCalledTimes(0);
});
describe('Skip comment if body is empty', () => {
for (const t of tests) {
test(`${t}`, async () => {
const config: IConfig = {
parentFieldName: `labels.spam.labeled.${t}`,
labelIndex: '1',
action: 'close',
locking: 'lock',
lockReason: 'spam'
};
const commentBody = '';
const actionProcessor = new ActionProcessor(config, commentBody, issueMock);
await actionProcessor.process();
expect(issueMock.createComment).toBeCalledTimes(0);
expect(issueMock.updateState).toBeCalledTimes(1);
expect(issueMock.updateState).toBeCalledWith('closed');
expect(issueMock.lock).toBeCalledTimes(1);
expect(issueMock.lock).toBeCalledWith('spam');
expect(issueMock.unlock).toBeCalledTimes(0);
});
}
});
Loading

0 comments on commit 2756d58

Please sign in to comment.