Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ This action automates a couple of processes connected with the management of Git

The linkage and estimation checks are retried every 15 seconds for 2 minutes so that the user can set them up after the pull request is created without this action failing.

The action skips pull requests that come from external forks, that do not target the repository's default branch, or whose creator is not a member of any Product Engineering team. Teams listed in `SKIP_LINKING_AND_ESTIMATE_CHECKS_FOR_TEAMS` in [`src/consts.ts`](src/consts.ts) are exempt from the linking and estimate checks.
The action skips pull requests that come from external forks, that are closed, that do not target the repository's default branch, or whose creator is not a member of any Product Engineering team. Teams listed in `SKIP_LINKING_AND_ESTIMATE_CHECKS_FOR_TEAMS` in [`src/consts.ts`](src/consts.ts) are exempt from the linking and estimate checks.

## Action input

Expand Down
6 changes: 6 additions & 0 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ export async function main({
await pullRequestToolkit.closeIssuesMentionedInPullRequestBody();
}

// A closed pull request belongs in "Closed", the status set below would move it back to "Pull Request".
if (await pullRequestToolkit.isClosed()) {
core.info('Pull request is closed. Skipping toolkit action.');
return;
}

// Skip when the pull request is not into the default branch. We don't want to run this on releases or pull request chains.
if (!(await pullRequestToolkit.isToDefaultBranch())) {
core.info(`Skipping toolkit action for pull request not into the default branch.`);
Expand Down
9 changes: 9 additions & 0 deletions src/pull_request_toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,15 @@ export class PullRequestToolkit {
return !!pullRequest.merged;
}

/**
* Checks whether the pull request is closed, including merged ones:
* the REST API has no merged state, a merged pull request is `state: 'closed'` with `merged: true`.
*/
public async isClosed(): Promise<boolean> {
const pullRequest = await this.getPullRequest();
return pullRequest.state === 'closed';
}
Comment on lines +84 to +91

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

so if it's merged instead of closed, how does this one behave please?

@markubra markubra Sep 30, 2026 •

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Merged PRs have state: closed in the REST API, so this returns true for both. Issue closing for merged PRs runs before this check

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, can you please extend the docstring so it explains this?


/**
* Finds the human creator of the pull request, falling back to a human assignee if it was created by a bot.
*/
Expand Down
44 changes: 43 additions & 1 deletion tests/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { describe, expect, test, vi } from 'vitest';
import { afterEach, describe, expect, test, vi } from 'vitest';

import { GitHubModel } from '../src/github_model.ts';
import { main } from '../src/main.ts';
import { PullRequestToolkit } from '../src/pull_request_toolkit.ts';
import type { Context, Core, GetOctokitFunction } from '../src/types.ts';

function makeContext(pullRequest: Record<string, unknown>) {
Expand All @@ -17,6 +19,10 @@ function makePullRequest(creatorLogin: string) {
}

describe('main', () => {
afterEach(() => {
vi.restoreAllMocks();
});

test('skips pull requests from Dependabot, which run without access to the Actions secrets', async () => {
const core = { info: vi.fn(), error: vi.fn(), setFailed: vi.fn() } as unknown as Core;
const getOctokit = vi.fn() as unknown as GetOctokitFunction;
Expand All @@ -35,4 +41,40 @@ describe('main', () => {

expect(core.setFailed).toHaveBeenCalledWith('Missing org-github-token input!');
});

test('does not put a closed pull request back on the team board when the check is re-run', async () => {
// Re-runs replay the original event payload, so only the freshly fetched pull request shows it is closed.
vi.spyOn(GitHubModel.prototype, 'getPullRequest').mockResolvedValue({
state: 'closed',
merged: true,
draft: false,
user: { login: 'VojtaM39' },
base: { ref: 'master', repo: { default_branch: 'master' } },
} as unknown as Awaited<ReturnType<GitHubModel['getPullRequest']>>);
vi.spyOn(PullRequestToolkit.prototype, 'isPullRequestToolkitRequiredForRepo').mockResolvedValue(true);
vi.spyOn(PullRequestToolkit.prototype, 'linkIssuesMentionedInPullRequestBody').mockResolvedValue();
const closeIssues = vi
.spyOn(PullRequestToolkit.prototype, 'closeIssuesMentionedInPullRequestBody')
.mockResolvedValue();
vi.spyOn(PullRequestToolkit.prototype, 'findUsersProductEngineeringChildTeamName').mockResolvedValue(
'Infrastructure',
);
vi.spyOn(PullRequestToolkit.prototype, 'isTested').mockResolvedValue(false);
vi.spyOn(PullRequestToolkit.prototype, 'assignCreator').mockResolvedValue();
vi.spyOn(PullRequestToolkit.prototype, 'getTeamLabels').mockResolvedValue(['t-infra']);
const findProject = vi.spyOn(PullRequestToolkit.prototype, 'findProjectForTeam').mockResolvedValue(null);
const core = { info: vi.fn(), error: vi.fn(), setFailed: vi.fn() } as unknown as Core;
const getOctokit = vi.fn() as unknown as GetOctokitFunction;

await main({
getOctokit,
context: makeContext(makePullRequest('VojtaM39')),
core,
input: { 'org-github-token': 'token', 'apify-api-token': 'token' },
});

expect(closeIssues).toHaveBeenCalled();
expect(findProject).not.toHaveBeenCalled();
expect(core.setFailed).not.toHaveBeenCalled();
});
});