Skip to content

Commit

Permalink
feat: Add option pr-number: "auto" to automatically find a pr-numbe…
Browse files Browse the repository at this point in the history
…r for push-events (#424)

Previously, one would have to pass in the concrete PR Number via the `pr-number` option. Now you can set this option 'auto' to automatically find the pull-request associated with the SHA of the cuerrent push-event.
  • Loading branch information
davelosert authored Sep 30, 2024
1 parent d8bc1a0 commit 001cef1
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 23 deletions.
22 changes: 11 additions & 11 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -77,18 +77,18 @@ This action requires the `pull-request: write` permission to add a comment to yo

### Options

| Option | Description | Default |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `working-directory` | The main path to search for coverage- and configuration files (adjusting this is especially useful in monorepos). | `./` |
| `json-summary-path` | The path to the json summary file. | `${working-directory}/coverage/coverage-summary.json` |
| `json-final-path` | The path to the json final file. | `${working-directory}/coverage/coverage-final.json` |
| Option | Description | Default |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `working-directory` | The main path to search for coverage- and configuration files (adjusting this is especially useful in monorepos). | `./` |
| `json-summary-path` | The path to the json summary file. | `${working-directory}/coverage/coverage-summary.json` |
| `json-final-path` | The path to the json final file. | `${working-directory}/coverage/coverage-final.json` |
| `vite-config-path` | The path to the vite config file. Will check the same paths as vite and vitest | Checks pattern `${working-directory}/vite[st].config.{t\|mt\|ct\|j\|mj\|cj}s` |
| `github-token` | A GitHub access token with permissions to write to issues (defaults to `secrets.GITHUB_TOKEN`). | `${{ github.token }}` |
| `file-coverage-mode` | Defines how file-based coverage is reported. Possible values are `all`, `changes` or `none`. | `changes` |
| `name` | Give the report a custom name. This is useful if you want multiple reports for different test suites within the same PR. Needs to be unique. | '' |
| `json-summary-compare-path` | The path to the json summary file to compare against. If given, will display a trend indicator and the difference in the summary. Respects the `working-directory` option. | undefined |
| `pr-number` | The number of the PR to post a comment to (if any) | If in the context of a PR, the number of that PR.<br/> If in the context of a triggered workflow, the PR of the triggering workflow. <br/>If no PR context is found, it defaults to `undefined` |
| `comment-on` | Specify where you want a comment to appear: "pr" for pull-request (if one can be found), "commit" for the commit in which context the action was run, or "none" for no comments. You can provide a comma-separated list of "pr" and "commit" to comment on both. | `pr` |
| `github-token` | A GitHub access token with permissions to write to issues (defaults to `secrets.GITHUB_TOKEN`). | `${{ github.token }}` |
| `file-coverage-mode` | Defines how file-based coverage is reported. Possible values are `all`, `changes` or `none`. | `changes` |
| `name` | Give the report a custom name. This is useful if you want multiple reports for different test suites within the same PR. Needs to be unique. | '' |
| `json-summary-compare-path` | The path to the json summary file to compare against. If given, will display a trend indicator and the difference in the summary. Respects the `working-directory` option. | undefined |
| `pr-number` | The number of the PR to post a comment to. When using the `push` trigger, you can set this option to "auto" to make the action automaticaly search of a PR with a matching `sha` value and comment on it. | If in the context of a PR, the number of that PR.<br/> If in the context of a triggered workflow, the PR of the triggering workflow. <br/>If no PR context is found, it defaults to `undefined` |
| `comment-on` | Specify where you want a comment to appear: "pr" for pull-request (if one can be found), "commit" for the commit in which context the action was run, or "none" for no comments. You can provide a comma-separated list of "pr" and "commit" to comment on both. | `pr` |

#### File Coverage Mode

Expand Down
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ inputs:
default: ''
pr-number:
required: false
description: 'An optional, user-defined pull request number to comment on.'
description: 'An optional, user-defined pull request number to comment on. When using "push" events, use "auto" to try to automatically find a matching PR.'
default: ''
comment-on:
required: false
Expand Down
46 changes: 46 additions & 0 deletions src/inputs/getPullChanges.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Mock, beforeEach, describe, expect, it, vi } from "vitest";
import type { Octokit } from "../octokit";
import { RequestError } from "@octokit/request-error";
import { FileCoverageMode } from "./FileCoverageMode";
import { getPullChanges } from "./getPullChanges";

Expand Down Expand Up @@ -66,4 +67,49 @@ describe("getPullChanges()", () => {
});
expect(result).toEqual(["file1.ts", "file2.ts"]);
});

it("handles RequestError with status 404 gracefully", async () => {
mockOctokit.paginate.iterator = vi.fn().mockImplementation(async function* () {
throw new RequestError("Not Found", 404, {
request: { headers: {}, method: "GET", url: "" },
});
});

const result = await getPullChanges({
fileCoverageMode: FileCoverageMode.Changes,
prNumber: 123,
octokit: mockOctokit,
});

expect(result).toEqual([]);
});

it("handles RequestError with status 403 gracefully", async () => {
mockOctokit.paginate.iterator = vi.fn().mockImplementation(async function* () {
throw new RequestError("Forbidden", 403, {
request: { headers: {}, method: "GET", url: "" },
});
});

const result = await getPullChanges({
fileCoverageMode: FileCoverageMode.Changes,
prNumber: 123,
octokit: mockOctokit,
});

expect(result).toEqual([]);
});

it("throws an error for other exceptions", async () => {
mockOctokit.paginate.iterator = vi.fn().mockImplementation(async function* () {
throw new Error("Unexpected error");
});
await expect(
getPullChanges({
fileCoverageMode: FileCoverageMode.Changes,
prNumber: 123,
octokit: mockOctokit,
}),
).rejects.toThrow("Unexpected error");
});
});
125 changes: 122 additions & 3 deletions src/inputs/getPullRequestNumber.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ describe("getPullRequestNumber()", () => {
pulls: {
list: vi.fn(),
},
repos: {
listPullRequestsAssociatedWithCommit: vi.fn(),
},
},
} as unknown as Octokit;
});
Expand All @@ -59,14 +62,21 @@ describe("getPullRequestNumber()", () => {
vi.unstubAllEnvs();
});

it("returns undefined if the input 'pr-number' is set to 'none'", async () => {
vi.stubEnv("INPUT_PR-NUMBER", "none");

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBeUndefined();
});

it("returns the PR number from the input 'pr-number' if valid ", async () => {
vi.stubEnv("INPUT_PR-NUMBER", "123");

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBe(123);
});

it("returns the PR number from payload.pull_request context if found", async () => {
it("in context of pull-request, returns the PR number from the payload.pull_request.", async () => {
mockContext.payload = {
pull_request: {
number: 456,
Expand All @@ -77,7 +87,7 @@ describe("getPullRequestNumber()", () => {
expect(result).toBe(456);
});

it("returns the PR number from payload.workflow_run context if found", async () => {
it("in context of a workflow_run, returns the PR number from payload.workflow_run if found", async () => {
mockContext.eventName = "workflow_run";
mockContext.payload = {
workflow_run: {
Expand All @@ -90,7 +100,7 @@ describe("getPullRequestNumber()", () => {
expect(result).toBe(789);
});

it("calls the API to find PR number by the head_sha of the payload.workflow_run when called from a fork", async () => {
it("in context of a workflow_run from a fork, calls the API to find PR number by the head_sha of the payload.workflow_run when called from a fork", async () => {
mockContext.eventName = "workflow_run";
mockContext.payload = {
workflow_run: {
Expand All @@ -109,6 +119,115 @@ describe("getPullRequestNumber()", () => {
expect(result).toBe(101);
});

it("in context of a push event, when option PR-Number is undefined, returns undefined", async () => {
mockContext.eventName = "push";
mockContext.payload = {
head_commit: {
id: "test-sha",
},
};

(
mockOctokit.rest.repos
.listPullRequestsAssociatedWithCommit as unknown as Mock
).mockResolvedValue({
data: [
{
number: 101,
},
],
});

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBeUndefined();
});

it("in context of a push event, when option PR-Number is set to auto, returns the number of a PR found through the 'listPullRequestsAssociatedWithCommit' endpoint.", async () => {
vi.stubEnv("INPUT_PR-NUMBER", "auto");

mockContext.eventName = "push";
mockContext.payload = {
head_commit: {
id: "test-sha",
},
};

(
mockOctokit.rest.repos
.listPullRequestsAssociatedWithCommit as unknown as Mock
).mockResolvedValue({
data: [
{
number: 101,
},
],
});

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBe(101);
});
it("in context of a push event, when option PR-Number is set to auto, and the 'listPullRequestsAssociatedWithCommits' endpoint returns no PRs, returns a PR found in the 'pulls.list' endpoint.", async () => {
vi.stubEnv("INPUT_PR-NUMBER", "auto");

mockContext.eventName = "push";
mockContext.payload = {
head_commit: {
id: "testsha",
},
};

(
mockOctokit.rest.repos
.listPullRequestsAssociatedWithCommit as unknown as Mock
).mockResolvedValue({
data: [],
});

(mockOctokit.paginate.iterator as Mock).mockReturnValue([
{
data: [{ number: 101, head: { sha: "testsha" } }],
},
]);

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBe(101);
});

it("in context of a push event, when option PR-Number is set to auto, and the 'listPullRequestsAssociatedWithCommits' endpoint returns no PRs, and the 'pulls.list' endpoint returns only non-matchin PRs, returns undefined.", async () => {
vi.stubEnv("INPUT_PR-NUMBER", "auto");
mockContext.eventName = "push";
mockContext.payload = {
head_commit: {
id: "testsha",
},
};

(
mockOctokit.rest.repos
.listPullRequestsAssociatedWithCommit as unknown as Mock
).mockResolvedValue({
data: [],
});

(mockOctokit.paginate.iterator as Mock).mockReturnValue([
{
data: [
{
number: 101,
head: { sha: "not-testsha" },
},
{
number: 102,
head: { sha: "not-testsha2" },
},
],
},
]);

const result = await getPullRequestNumber(mockOctokit);
expect(result).toBeUndefined();
});

it("returns undefined if no pr number is found", async () => {
mockContext.payload = {};
const result = await getPullRequestNumber(mockOctokit);
Expand Down
Loading

0 comments on commit 001cef1

Please sign in to comment.