diff --git a/README.md b/README.md index e001e02..b006048 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,17 @@ To force an API call, set the `GITHUB_TOKEN` environment variable like so: labels: "do not merge" ``` +### Post a comment when the check fails + +```yaml +- uses: mheap/github-action-required-labels@v2 + with: + mode: exactly + count: 1 + labels: "semver:patch, semver:minor, semver:major" + add_comment: true +``` + ### Require multiple labels ```yaml diff --git a/action.yml b/action.yml index b8b0637..858b3b7 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,10 @@ inputs: count: description: "The required number of labels to match" required: true + comment: + description: "Add a comment to the PR if required labels are missing" + default: "false" + required: false exit_type: description: "The exit type of the action. One of: failure, success, neutral" required: false diff --git a/index.js b/index.js index c000bd2..6a5333a 100644 --- a/index.js +++ b/index.js @@ -55,8 +55,9 @@ Toolkit.run(async (tools) => { let intersection = allowedLabels.filter((x) => appliedLabels.includes(x)); if (mode === "exactly" && intersection.length !== count) { - tools.outputs.status = "failure"; - tools.exit[exitType]( + await exitWithError( + tools, + exitType, `Label error. Requires exactly ${count} of: ${allowedLabels.join( ", " )}. Found: ${appliedLabels.join(", ")}` @@ -65,8 +66,9 @@ Toolkit.run(async (tools) => { } if (mode === "minimum" && intersection.length < count) { - tools.outputs.status = "failure"; - tools.exit[exitType]( + await exitWithError( + tools, + exitType, `Label error. Requires at least ${count} of: ${allowedLabels.join( ", " )}. Found: ${appliedLabels.join(", ")}` @@ -75,8 +77,9 @@ Toolkit.run(async (tools) => { } if (mode === "maximum" && intersection.length > count) { - tools.outputs.status = "failure"; - tools.exit[exitType]( + await exitWithError( + tools, + exitType, `Label error. Requires at most ${count} of: ${allowedLabels.join( ", " )}. Found: ${appliedLabels.join(", ")}` @@ -87,3 +90,20 @@ Toolkit.run(async (tools) => { tools.outputs.status = "success"; tools.exit.success("Complete"); }); + +async function exitWithError(tools, exitType, message) { + if (tools.inputs.add_comment) { + if (process.env.GITHUB_TOKEN) { + await tools.github.issues.createComment({ + ...tools.context.issue, + body: message, + }); + } else { + throw new Error( + "The GITHUB_TOKEN environment variable must be set to add a comment" + ); + } + } + tools.outputs.status = "failure"; + tools.exit[exitType](message); +} diff --git a/index.test.js b/index.test.js index c617977..b282c2c 100644 --- a/index.test.js +++ b/index.test.js @@ -1,6 +1,8 @@ const { Toolkit } = require("actions-toolkit"); const core = require("@actions/core"); const mockedEnv = require("mocked-env"); +const nock = require("nock"); +nock.disableNetConnect(); describe("Required Labels", () => { let action, tools; @@ -44,6 +46,82 @@ describe("Required Labels", () => { restore(); restoreTest(); jest.resetModules(); + + if (!nock.isDone()) { + throw new Error( + `Not all nock interceptors were used: ${JSON.stringify( + nock.pendingMocks() + )}` + ); + } + nock.cleanAll(); + }); + + describe("interacts with the API", () => { + it("fetches the labels from the API", async () => { + restoreTest = mockPr(tools, [], { + INPUT_LABELS: "enhancement", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + GITHUB_TOKEN: "mock-token-here-abc", + }); + + nock("https://api.github.com") + .get("/repos/mheap/missing-repo/issues/28/labels") + .reply(200, [{ name: "enhancement" }, { name: "bug" }]); + + await action(tools); + expect(core.setOutput).toBeCalledTimes(1); + expect(core.setOutput).toBeCalledWith("status", "success"); + expect(tools.exit.success).toBeCalledTimes(1); + expect(tools.exit.success).toBeCalledWith("Complete"); + }); + + it("fetches the labels from the API (and fails)", async () => { + restoreTest = mockPr(tools, ["enhancement"], { + INPUT_LABELS: "enhancement", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + GITHUB_TOKEN: "mock-token-here-abc", + }); + + nock("https://api.github.com") + .get("/repos/mheap/missing-repo/issues/28/labels") + .reply(200, [{ name: "bug" }]); + + await action(tools); + expect(core.setOutput).toBeCalledTimes(1); + expect(core.setOutput).toBeCalledWith("status", "failure"); + expect(tools.exit.failure).toBeCalledTimes(1); + expect(tools.exit.failure).toBeCalledWith( + "Label error. Requires exactly 1 of: enhancement. Found: bug" + ); + }); + + it("posts a comment when enabled", async () => { + restoreTest = mockPr(tools, ["enhancement"], { + INPUT_LABELS: "enhancement", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + INPUT_ADD_COMMENT: "true", + GITHUB_TOKEN: "mock-token-here-abc", + }); + + nock("https://api.github.com") + .get("/repos/mheap/missing-repo/issues/28/labels") + .reply(200, [{ name: "bug" }]); + + nock("https://api.github.com") + .post("/repos/mheap/missing-repo/issues/28/comments", { + body: "Label error. Requires exactly 1 of: enhancement. Found: bug", + }) + .reply(201); + + await action(tools); + expect(tools.exit.failure).toBeCalledWith( + "Label error. Requires exactly 1 of: enhancement. Found: bug" + ); + }); }); describe("success", () => { diff --git a/package-lock.json b/package-lock.json index 0a489d5..4729d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ }, "devDependencies": { "jest": "^28.1.1", + "nock": "^13.2.9", "prettier": "^2.7.1" } }, @@ -4474,6 +4475,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "node_modules/json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -4544,6 +4551,12 @@ "node": ">=4" } }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4672,6 +4685,21 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "node_modules/nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + }, + "engines": { + "node": ">= 10.13" + } + }, "node_modules/node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -5042,6 +5070,15 @@ "node": ">= 6" } }, + "node_modules/propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, "node_modules/pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", @@ -9021,6 +9058,12 @@ "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", "dev": true }, + "json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==", + "dev": true + }, "json5": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz", @@ -9070,6 +9113,12 @@ "path-exists": "^3.0.0" } }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "dev": true + }, "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -9173,6 +9222,18 @@ "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==" }, + "nock": { + "version": "13.2.9", + "resolved": "https://registry.npmjs.org/nock/-/nock-13.2.9.tgz", + "integrity": "sha512-1+XfJNYF1cjGB+TKMWi29eZ0b82QOvQs2YoLNzbpWGqFMtRQHTa57osqdGj4FrFPgkO4D4AZinzUJR9VvW3QUA==", + "dev": true, + "requires": { + "debug": "^4.1.0", + "json-stringify-safe": "^5.0.1", + "lodash": "^4.17.21", + "propagate": "^2.0.0" + } + }, "node-fetch": { "version": "2.6.7", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", @@ -9439,6 +9500,12 @@ "sisteransi": "^1.0.5" } }, + "propagate": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/propagate/-/propagate-2.0.1.tgz", + "integrity": "sha512-vGrhOavPSTz4QVNuBNdcNXePNdNMaO1xj9yBeH1ScQPjk/rhg9sSlCXPhMkFuaNNW/syTvYqsnbIJxMBfRbbag==", + "dev": true + }, "pump": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", diff --git a/package.json b/package.json index 4824d3e..223af2c 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "devDependencies": { "jest": "^28.1.1", + "nock": "^13.2.9", "prettier": "^2.7.1" }, "license": "MIT"