diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index 85fe7053..3c62ef99 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -51,7 +51,8 @@ const DefaultProcessorOptions: IssueProcessorOptions = Object.freeze({ removeStaleWhenUpdated: false, ascending: false, skipStaleIssueMessage: false, - skipStalePrMessage: false + skipStalePrMessage: false, + deleteBranch: false }); test('empty issue list results in 1 operation', async () => { @@ -89,6 +90,7 @@ test('processing an issue with no label will make it stale and close it, if it i expect(processor.staleIssues.length).toEqual(1); expect(processor.closedIssues.length).toEqual(1); + expect(processor.deletedBranchIssues.length).toEqual(0); }); test('processing an issue with no label will make it stale and not close it if days-before-close is set to > 0', async () => { @@ -895,3 +897,59 @@ test('not providing stalePrMessage takes precedence over skipStalePrMessage', as expect(processor.removedLabelIssues.length).toEqual(0); expect(processor.staleIssues.length).toEqual(0); }); + +test('git branch is deleted when option is enabled', async () => { + const opts = {...DefaultProcessorOptions, deleteBranch: true}; + const isPullRequest = true; + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should have its branch deleted', + '2020-01-01T17:00:00Z', + isPullRequest, + ['Stale'] + ) + ]; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + await processor.processIssues(1); + + expect(processor.closedIssues.length).toEqual(1); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(0); + expect(processor.deletedBranchIssues.length).toEqual(1); +}); + +test('git branch is not deleted when issue is not pull request', async () => { + const opts = {...DefaultProcessorOptions, deleteBranch: true}; + const isPullRequest = false; + const TestIssueList: Issue[] = [ + generateIssue( + 1, + 'An issue that should not have its branch deleted', + '2020-01-01T17:00:00Z', + isPullRequest, + ['Stale'] + ) + ]; + + const processor = new IssueProcessor( + opts, + async p => (p == 1 ? TestIssueList : []), + async (num, dt) => [], + async (issue, label) => new Date().toDateString() + ); + + await processor.processIssues(1); + + expect(processor.closedIssues.length).toEqual(1); + expect(processor.removedLabelIssues.length).toEqual(0); + expect(processor.staleIssues.length).toEqual(0); + expect(processor.deletedBranchIssues.length).toEqual(0); +}); diff --git a/action.yml b/action.yml index 9b6cf437..ea6c6d19 100644 --- a/action.yml +++ b/action.yml @@ -56,6 +56,9 @@ inputs: skip-stale-issue-message: description: 'Skip adding stale message when marking an issue as stale.' default: false + delete-branch: + description: 'Delete the git branch after closing a stale pull request.' + default: false runs: using: 'node12' main: 'dist/index.js' diff --git a/src/IssueProcessor.ts b/src/IssueProcessor.ts index 77f3b09f..7572a5e4 100644 --- a/src/IssueProcessor.ts +++ b/src/IssueProcessor.ts @@ -14,6 +14,13 @@ export interface Issue { locked: boolean; } +export interface PullRequest { + number: number; + head: { + ref: string; + }; +} + export interface User { type: string; login: string; @@ -54,6 +61,7 @@ export interface IssueProcessorOptions { ascending: boolean; skipStaleIssueMessage: boolean; skipStalePrMessage: boolean; + deleteBranch: boolean; } /*** @@ -66,6 +74,7 @@ export class IssueProcessor { readonly staleIssues: Issue[] = []; readonly closedIssues: Issue[] = []; + readonly deletedBranchIssues: Issue[] = []; readonly removedLabelIssues: Issue[] = []; constructor( @@ -250,6 +259,14 @@ export class IssueProcessor { `Closing ${issueType} because it was last updated on ${issue.updated_at}` ); await this.closeIssue(issue, closeMessage, closeLabel); + + if (this.options.deleteBranch && issue.pull_request) { + core.info( + `Deleting branch for #${issue.number} as delete-branch option was specified` + ); + await this.deleteBranch(issue); + this.deletedBranchIssues.push(issue); + } } else { core.info( `Stale ${issueType} is not old enough to close yet (hasComments? ${issueHasComments}, hasUpdate? ${issueHasUpdate})` @@ -432,6 +449,61 @@ export class IssueProcessor { } } + private async getPullRequest( + pullNumber: number + ): Promise { + this.operationsLeft -= 1; + + try { + const pullRequest = await this.client.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: pullNumber + }); + + return pullRequest.data; + } catch (error) { + core.error(`Error getting pull request ${pullNumber}: ${error.message}`); + } + } + + // Delete the branch on closed pull request + private async deleteBranch(issue: Issue): Promise { + core.info( + `Delete branch from closed issue #${issue.number} - ${issue.title}` + ); + + if (this.options.debugOnly) { + return; + } + + const pullRequest = await this.getPullRequest(issue.number); + + if (!pullRequest) { + core.info( + `Not deleting branch as pull request not found for issue ${issue.number}` + ); + return; + } + + const branch = pullRequest.head.ref; + core.info(`Deleting branch ${branch} from closed issue #${issue.number}`); + + this.operationsLeft -= 1; + + try { + await this.client.git.deleteRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `heads/${branch}` + }); + } catch (error) { + core.error( + `Error deleting branch ${branch} from issue #${issue.number}: ${error.message}` + ); + } + } + // Remove a label from an issue private async removeLabel(issue: Issue, label: string): Promise { core.info(`Removing label from issue #${issue.number}`); diff --git a/src/main.ts b/src/main.ts index b1e0dc3a..1cd914ab 100644 --- a/src/main.ts +++ b/src/main.ts @@ -42,7 +42,8 @@ function getAndValidateArgs(): IssueProcessorOptions { debugOnly: core.getInput('debug-only') === 'true', ascending: core.getInput('ascending') === 'true', skipStalePrMessage: core.getInput('skip-stale-pr-message') === 'true', - skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true' + skipStaleIssueMessage: core.getInput('skip-stale-issue-message') === 'true', + deleteBranch: core.getInput('delete-branch') === 'true' }; for (const numberInput of [