Skip to content

feat: ✨ Automatically delete branches when issues are closed #883

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,19 @@ the following is true for your app configuration:
- Your branch name contains the string `issue-` (case insensitive) followed by
the issue number, for example: `Project-A-Issue-123-Rewrite_in_Clojure`

## Automatically delete issue branch after closing an issue

This app can delete issue branches for you when a related issue is closed. You
can enable this feature with:

```yaml
autoDeleteBranch: true
```

Be aware that the app needs to be able to find the issue number in the branch
name, otherwise this feature will not work. See also the explanation in [this
section](#Automatically-close-issues-after-a-pull-request-merge).

## Default source branch

You can override the source branch (by default
Expand Down
2 changes: 2 additions & 0 deletions src/entities/Config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ export interface Config {
branchName: string;
autoLinkIssue: boolean;
autoCloseIssue: boolean;
autoDeleteBranch: boolean;
defaultBranch?: string;
branches: Array<BranchConfig>;
copyIssueLabelsToPR: boolean;
Expand Down Expand Up @@ -40,6 +41,7 @@ export function getDefaultConfig(): Config {
branchName: 'full',
autoLinkIssue: false,
autoCloseIssue: false,
autoDeleteBranch: false,
branches: [],
copyIssueLabelsToPR: false,
copyIssueAssigneeToPR: false,
Expand Down
13 changes: 13 additions & 0 deletions src/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,19 @@ export async function branchExists(ctx: Context<any>, branchName: string) {
}
}

export async function deleteBranch(ctx: Context<any>, branchName: string) {
const owner = getRepoOwnerLogin(ctx)
const repo = getRepoName(ctx)
try {
await ctx.octokit.git.deleteRef({
owner: owner, repo: repo, ref: `heads/${branchName}`
})
return true
} catch (err) {
return false
}
}

export function getSourceBranch(ctx: Context<any>, config: Config) {
const branchConfig = getIssueBranchConfig(ctx, config)
if (branchConfig && branchConfig.name) {
Expand Down
4 changes: 4 additions & 0 deletions src/probot.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {gitDate, gitSha, version} from "./version";
import {isRunningInGitHubActions, logMemoryUsage} from "./utils";
import {MongoDbService} from "./services/MongoDbService";
import {WebhookEvent} from "./entities/WebhookEvent";
import {issueClosed} from "./webhooks/issue-closed";


export default (app: Probot, {getRouter}: ApplicationFunctionOptions) => {
Expand All @@ -34,6 +35,9 @@ function setupEventHandlers(app: Probot) {
app.on('issues.assigned', async ctx => {
await issueAssigned(app, ctx);
});
app.on('issues.closed', async ctx => {
await issueClosed(app, ctx);
});
app.on('issue_comment.created', async ctx => {
const comment = ctx.payload.comment.body;
await commentCreated(app, ctx, comment);
Expand Down
28 changes: 28 additions & 0 deletions src/webhooks/issue-closed.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import {Context, Probot} from "probot";
import {loadConfig} from "../config";
import {logMemoryUsage} from "../utils";
import {Config} from "../entities/Config";
import {branchExists, deleteBranch, getBranchNameFromIssue} from "../github";

export async function issueClosed(app: Probot, ctx: Context<any>) {
const config = await loadConfig(ctx);
if (config) {
if (!config.autoDeleteBranch) {
return;
}
await handle(app, ctx, config);
logMemoryUsage(app);
}
}

async function handle(app: Probot, ctx: Context<any>, config: Config) {
const branchName = await getBranchNameFromIssue(ctx, config)
if (await branchExists(ctx, branchName)) {
const result = await deleteBranch(ctx, branchName);
if (result) {
app.log.info(`Deleted branch ${branchName}`);
} else {
app.log.error(`Failed to delete branch ${branchName}`);
}
}
}
2 changes: 1 addition & 1 deletion tests/webhooks/issue-assigned.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,5 @@ test('do nothing if configured to skip', async () => {
"description": "Something isn't working"
}] as any;

await probot.receive({id: '', name: 'issues', payload: issueAssignedPayload as any});
await probot.receive({id: '', name: 'issues', payload: payload as any});
});
40 changes: 40 additions & 0 deletions tests/webhooks/issue-closed.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {Probot} from "probot";
import issueOpenedPayload from "../test-fixtures/issues.opened.json";
import {initNock, initProbot, nockConfig, nockEmptyConfig, nockExistingBranch} from "../test-helpers";

let probot: Probot

beforeAll(() => {
initNock()
})

beforeEach(() => {
probot = initProbot()
})

test('do nothing if not configured', async () => {
nockEmptyConfig();
const payload = issueOpenedPayload;
payload.action = 'closed';
const deleteRef = jest.fn()
// @ts-ignore
probot.state.octokit.git.deleteRef = deleteRef

await probot.receive({id: '', name: 'issues', payload: payload as any});

expect(deleteRef).toHaveBeenCalledTimes(0);
});

test('do delete branch', async () => {
nockConfig('autoDeleteBranch: true');
nockExistingBranch('issue-1-Test_issue', 'abcd1234');
const payload = issueOpenedPayload;
payload.action = 'closed';
const deleteRef = jest.fn()
// @ts-ignore
probot.state.octokit.git.deleteRef = deleteRef

await probot.receive({id: '', name: 'issues', payload: payload as any});

expect(deleteRef).toHaveBeenCalledTimes(1);
});
Loading