From e5edf3bdc0379d41f74ce10aaa240f55e6812757 Mon Sep 17 00:00:00 2001 From: David Losert Date: Sun, 1 Sep 2024 19:45:51 +0200 Subject: [PATCH] feat: Allows multiple selections for the comment-on option --- .github/workflows/test.yml | 2 +- README.md | 24 ++++----- action.yml | 2 +- src/index.ts | 87 +++++++++++++++++++++------------ src/inputs/getCommentOn.test.ts | 73 +++++++++++++++++++++++++++ src/inputs/getCommentOn.ts | 43 ++++++++++++++++ src/inputs/options.ts | 14 ++---- 7 files changed, 192 insertions(+), 53 deletions(-) create mode 100644 src/inputs/getCommentOn.test.ts create mode 100644 src/inputs/getCommentOn.ts diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c0375f6..e651c7c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -70,4 +70,4 @@ jobs: with: file-coverage-mode: "all" json-summary-compare-path: coverage-main/coverage-summary.json - comment-on: 'commit' + comment-on: 'pr,commit' diff --git a/README.md b/README.md index 7b72e65..961229a 100644 --- a/README.md +++ b/README.md @@ -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` | -| `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 | workspace}.{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.
If in the context of a triggered workflow, the PR of the triggering workflow.
If no PR context is found, it defaults to `undefined` | -| `comment-on` | Select whether you want a comment to appear on a pull-request (if it exists) or on the commit in which context the action was ran. | `pr` | +| 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 | workspace}.{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.
If in the context of a triggered workflow, the PR of the triggering workflow.
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 diff --git a/action.yml b/action.yml index 41344dc..d98560c 100644 --- a/action.yml +++ b/action.yml @@ -38,7 +38,7 @@ inputs: default: '' comment-on: required: false - description: 'Select wether you want a comment to appear on the PR (if it exists) or on the commit. Uses "pr" by default.' + description: '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. Uses "pr" by default.' default: pr runs: using: 'node20' diff --git a/src/index.ts b/src/index.ts index 9a8d841..dada21a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,12 +3,12 @@ import * as github from "@actions/github"; import { RequestError } from "@octokit/request-error"; import { FileCoverageMode } from "./inputs/FileCoverageMode.js"; import { getPullChanges } from "./inputs/getPullChanges.js"; -import { readOptions } from "./inputs/options.js"; +import { type Options, readOptions } from "./inputs/options.js"; import { parseVitestJsonFinal, parseVitestJsonSummary, } from "./inputs/parseJsonReports.js"; -import { createOctokit } from "./octokit.js"; +import { createOctokit, type Octokit } from "./octokit.js"; import { generateCommitSHAUrl } from "./report/generateCommitSHAUrl.js"; import { generateFileCoverageHtml } from "./report/generateFileCoverageHtml.js"; import { generateHeadline } from "./report/generateHeadline.js"; @@ -16,6 +16,9 @@ import { generateSummaryTableHtml } from "./report/generateSummaryTableHtml.js"; import type { JsonSummary } from "./types/JsonSummary.js"; import { writeSummaryToCommit } from "./writeSummaryToComment.js"; import { writeSummaryToPR } from "./writeSummaryToPR.js"; +import { aw } from "vitest/dist/chunks/reporters.C_zwCd4j.js"; + +type GitHubSummary = typeof core.summary; const run = async () => { const octokit = createOctokit(); @@ -72,48 +75,72 @@ const run = async () => { `Generated in workflow #${github.context.runNumber} for commit ${options.commitSHA.substring(0, 7)} by the Vitest Coverage Report Action`, ); + if (options.commentOn.includes("pr")) { + await commentOnPR(octokit, summary, options); + } + + if (options.commentOn.includes("commit")) { + await commentOnCommit(octokit, summary, options); + } + + await summary.write(); +}; + +async function commentOnPR( + octokit: Octokit, + summary: GitHubSummary, + options: Options, +) { try { - if (options.commentOn === "pr") { - await writeSummaryToPR({ - octokit, - summary, - markerPostfix: getMarkerPostfix({ - name: options.name, - workingDirectory: options.workingDirectory, - }), - prNumber: options.prNumber, - }); + await writeSummaryToPR({ + octokit, + summary, + markerPostfix: getMarkerPostfix({ + name: options.name, + workingDirectory: options.workingDirectory, + }), + prNumber: options.prNumber, + }); + } catch (error) { + if ( + error instanceof RequestError && + (error.status === 404 || error.status === 403) + ) { + core.warning( + `Couldn't write a comment to the pull request. Please make sure your job has the permission 'pull-request: write'. + Original Error was: [${error.name}] - ${error.message}`, + ); + } else { + throw error; } + } +} - if (options.commentOn === "commit") { - await writeSummaryToCommit({ - octokit, - summary, - commitSha: options.commitSHA, - }); - } +async function commentOnCommit( + octokit: Octokit, + summary: GitHubSummary, + options: Options, +) { + try { + await writeSummaryToCommit({ + octokit, + summary, + commitSha: options.commitSHA, + }); } catch (error) { if ( error instanceof RequestError && (error.status === 404 || error.status === 403) ) { - const item = options.commentOn === "pr" ? "pull request" : "commit"; - const requiredPermission = - options.commentOn === "pr" ? "pull-request: write" : "contents: read"; - core.warning( - `Couldn't write a comment to the ${item}. Please make sure your job has the permission '${requiredPermission}'. - Original Error was: [${error.name}] - ${error.message} - `, + `Couldn't write a comment to the commit. Please make sure your job has the permission 'contents: read'. + Original Error was: [${error.name}] - ${error.message}`, ); } else { - // Rethrow to handle it in the catch block of the run()-call. throw error; } } - - await summary.write(); -}; +} function getMarkerPostfix({ name, diff --git a/src/inputs/getCommentOn.test.ts b/src/inputs/getCommentOn.test.ts new file mode 100644 index 0000000..936ed9c --- /dev/null +++ b/src/inputs/getCommentOn.test.ts @@ -0,0 +1,73 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import * as core from "@actions/core"; +import { getCommentOn, type CommentOn } from "./getCommentOn"; + +vi.mock("@actions/core"); + +describe("getCommentOn()", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns the default value ['pr'] if no valid values are provided", () => { + vi.spyOn(core, "getInput").mockReturnValue("invalid1, invalid2"); + + const result = getCommentOn(); + expect(result).toEqual(["pr"]); + expect(core.warning).toHaveBeenCalledWith( + 'No valid options for comment-on found. Falling back to default value "pr".', + ); + }); + + it("logs invalid values", () => { + vi.spyOn(core, "getInput").mockReturnValue("pr, invalid, commit"); + + const result = getCommentOn(); + expect(result).toEqual(["pr", "commit"]); + expect(core.warning).toHaveBeenCalledWith( + 'Invalid options for comment-on: invalid. Valid options are "pr" and "commit".', + ); + }); + + it("returns valid values correctly", () => { + vi.spyOn(core, "getInput").mockReturnValue("pr, commit"); + + const result = getCommentOn(); + expect(result).toEqual(["pr", "commit"]); + expect(core.warning).not.toHaveBeenCalled(); + }); + + it("trims whitespace from the input", () => { + vi.spyOn(core, "getInput").mockReturnValue("pr, commit"); + + const result = getCommentOn(); + + expect(result).toEqual(["pr", "commit"]); + + expect(core.warning).not.toHaveBeenCalled(); + }); + + it("returns valid values and logs invalid values", () => { + vi.spyOn(core, "getInput").mockReturnValue( + "pr, invalid, commit, anotherInvalid", + ); + + const result = getCommentOn(); + expect(result).toEqual(["pr", "commit"]); + expect(core.warning).toHaveBeenCalledWith( + 'Invalid options for comment-on: invalid, anotherInvalid. Valid options are "pr" and "commit".', + ); + }); + + it("for value 'none', returns empty array", () => { + vi.spyOn(core, "getInput").mockReturnValue("none"); + + const result = getCommentOn(); + + expect(result).toEqual([]); + }); +}); diff --git a/src/inputs/getCommentOn.ts b/src/inputs/getCommentOn.ts new file mode 100644 index 0000000..93da54b --- /dev/null +++ b/src/inputs/getCommentOn.ts @@ -0,0 +1,43 @@ +import * as core from "@actions/core"; + +type CommentOn = "pr" | "commit" | "none"; + +function getCommentOn(): CommentOn[] { + const commentOnInput = core.getInput("comment-on"); + if (commentOnInput === "none") { + return []; + } + + const commentOnList = commentOnInput.split(",").map((item) => item.trim()); + + let validCommentOnValues: Array = []; + const invalidCommentOnValues: string[] = []; + + for (const value of commentOnList) { + if (value === "pr" || value === "commit") { + validCommentOnValues.push(value as CommentOn); + } else { + invalidCommentOnValues.push(value); + } + } + + if (validCommentOnValues.length === 0) { + core.warning( + `No valid options for comment-on found. Falling back to default value "pr".`, + ); + validCommentOnValues = ["pr"]; + return validCommentOnValues; + } + + if (invalidCommentOnValues.length > 0) { + core.warning( + `Invalid options for comment-on: ${invalidCommentOnValues.join(", ")}. Valid options are "pr" and "commit".`, + ); + } + + return validCommentOnValues; +} + +export { getCommentOn }; + +export type { CommentOn }; diff --git a/src/inputs/options.ts b/src/inputs/options.ts index 918649a..34714ca 100644 --- a/src/inputs/options.ts +++ b/src/inputs/options.ts @@ -7,6 +7,7 @@ import { getCommitSHA } from "./getCommitSHA"; import { getPullRequestNumber } from "./getPullRequestNumber"; import { getViteConfigPath } from "./getViteConfigPath"; import { parseCoverageThresholds } from "./parseCoverageThresholds"; +import { getCommentOn, type CommentOn } from "./getCommentOn"; type Options = { fileCoverageMode: FileCoverageMode; @@ -18,7 +19,7 @@ type Options = { workingDirectory: string; prNumber: number | undefined; commitSHA: string; - commentOn: "pr" | "commit"; + commentOn: Array; }; async function readOptions(octokit: Octokit): Promise { @@ -49,14 +50,7 @@ async function readOptions(octokit: Octokit): Promise { const name = core.getInput("name"); - let commentOn = core.getInput("comment-on") as Options["commentOn"]; - - if (commentOn !== "pr" && commentOn !== "commit") { - core.warning( - `Invalid option for comment-on: ${commentOn}. Falling back to default value "pr".`, - ); - commentOn = "pr"; - } + const commentOn = getCommentOn(); // ViteConfig is optional, as it is only required for thresholds. If no vite config is provided, we will not include thresholds in the final report. const viteConfigPath = await getViteConfigPath( @@ -87,3 +81,5 @@ async function readOptions(octokit: Octokit): Promise { } export { readOptions }; + +export type { Options };