diff --git a/README.md b/README.md index 9e0cb12..41c39f5 100644 --- a/README.md +++ b/README.md @@ -6,15 +6,15 @@ This action allows you to fail the build if/unless a certain combination of labe This action has three required inputs; `labels`, `mode` and `count` -| Name | Description | Required | Default | -| ------------- | ----------------------------------------------------------------------------------------------------------- | -------- | ------------------- | -| `labels` | Comma separated list of labels to match | true | -| `mode` | The mode of comparison to use. One of: exactly, minimum, maximum | true | -| `count` | The required number of labels to match | true | -| `token` | The GitHub token to use when calling the API | false | ${{ github.token }} | -| `message` | The message to log and to add to the PR (if add_comment is true). See the README for available placeholders | false | -| `add_comment` | Add a comment to the PR if required labels are missing | false | false | -| `exit_type` | The exit type of the action. One of: failure, success | false | +| Name | Description | Required | Default | +| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------- | +| `labels` | Comma separated list of labels to match | true | +| `mode` | The mode of comparison to use. One of: exactly, minimum, maximum | true | +| `count` | The required number of labels to match | true | +| `token` | The GitHub token to use when calling the API | false | ${{ github.token }} | +| `message` | The message to log and to add to the PR (if add_comment is true). See the README for available placeholders | false | +| `add_comment` | Add a comment to the PR if required labels are missing. If a comment already exists, it will be updated. When the action passes, the comment will be deleted | false | false | +| `exit_type` | The exit type of the action. One of: failure, success | false | This action calls the GitHub API to fetch labels for a PR rather than reading `event.json`. This allows the action to run as intended when an earlier step adds a label. It will use `github.token` by default, and you can set the `token` input to provide alternative authentication. @@ -31,10 +31,10 @@ jobs: label: runs-on: ubuntu-latest permissions: - issues: read - pull-requests: read + issues: write + pull-requests: write steps: - - uses: mheap/github-action-required-labels@v3 + - uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 1 @@ -44,7 +44,7 @@ jobs: ### Prevent merging if a label exists ```yaml -- uses: mheap/github-action-required-labels@v3 +- uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 0 @@ -58,7 +58,7 @@ You can choose to add a comment to the PR when the action fails. The default for > Label error. Requires {{ errorString }} {{ count }} of: {{ provided }}. Found: {{ applied }} ```yaml -- uses: mheap/github-action-required-labels@v3 +- uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 1 @@ -66,12 +66,14 @@ You can choose to add a comment to the PR when the action fails. The default for add_comment: true ``` +If a comment already exists, it will be updated. When the action passes, the comment will be deleted. + ### Customising the failure message / comment You can also customise the message used by providing the `message` input: ```yaml -- uses: mheap/github-action-required-labels@v3 +- uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 1 @@ -93,7 +95,7 @@ The following tokens are available for use in custom messages: ### Require multiple labels ```yaml -- uses: mheap/github-action-required-labels@v3 +- uses: mheap/github-action-required-labels@v4 with: mode: minimum count: 2 @@ -105,7 +107,7 @@ The following tokens are available for use in custom messages: You can set `exit_type` to success then inspect `outputs.status` to see if the action passed or failed. This is useful when you want to perform additional actions if a label is not present, but not fail the entire build. ```yaml -- uses: mheap/github-action-required-labels@v3 +- uses: mheap/github-action-required-labels@v4 with: mode: minimum count: 2 @@ -126,13 +128,13 @@ jobs: label: runs-on: ubuntu-latest permissions: - issues: read - pull-requests: read + issues: write + pull-requests: write outputs: status: ${{ steps.check-labels.outputs.status }} steps: - id: check-labels - uses: mheap/github-action-required-labels@v3 + uses: mheap/github-action-required-labels@v4 with: mode: exactly count: 1 diff --git a/index.js b/index.js index 573f1f5..1e5cce9 100644 --- a/index.js +++ b/index.js @@ -1,6 +1,7 @@ const core = require("@actions/core"); const github = require("@actions/github"); +const matchToken = ``; async function action() { try { const token = core.getInput("token", { required: true }); @@ -84,6 +85,24 @@ async function action() { return; } + // Remove the comment if it exists + if (shouldAddComment) { + const { data: existing } = await octokit.rest.issues.listComments({ + ...github.context.repo, + issue_number: github.context.issue.number, + }); + + const generatedComment = existing.find((c) => + c.body.includes(matchToken) + ); + if (generatedComment) { + await octokit.rest.issues.deleteComment({ + ...github.context.repo, + comment_id: generatedComment.id, + }); + } + } + core.setOutput("status", "success"); } catch (e) { core.setFailed(e.message); @@ -98,11 +117,27 @@ function tmpl(t, o) { async function exitWithError(exitType, octokit, shouldAddComment, message) { if (shouldAddComment) { - await octokit.rest.issues.createComment({ + // Is there an existing comment? + const { data: existing } = await octokit.rest.issues.listComments({ ...github.context.repo, issue_number: github.context.issue.number, - body: message, }); + + const generatedComment = existing.find((c) => c.body.includes(matchToken)); + + const params = { + ...github.context.repo, + issue_number: github.context.issue.number, + body: `${matchToken}${message}`, + }; + + // If so, update it + let method = "createComment"; + if (generatedComment) { + method = "updateComment"; + params.comment_id = generatedComment.id; + } + await octokit.rest.issues[method](params); } core.setOutput("status", "failure"); diff --git a/index.test.js b/index.test.js index 1190848..1b22771 100644 --- a/index.test.js +++ b/index.test.js @@ -6,6 +6,8 @@ const mockedEnv = require("mocked-env"); const nock = require("nock"); nock.disableNetConnect(); +const matchToken = ``; + describe("Required Labels", () => { let restore; let restoreTest; @@ -90,14 +92,35 @@ describe("Required Labels", () => { mockLabels(["bug"]); + mockListComments([]); + nock("https://api.github.com") .post("/repos/mheap/missing-repo/issues/28/comments", { - body: "Label error. Requires exactly 1 of: enhancement. Found: bug", + body: `${matchToken}Label error. Requires exactly 1 of: enhancement. Found: bug`, }) .reply(201); await action(); }); + + it("deletes a comment when passing", async () => { + restoreTest = mockPr({ + INPUT_LABELS: "bug", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + INPUT_ADD_COMMENT: "true", + GITHUB_TOKEN: "mock-token-here-abc", + }); + + mockLabels(["bug"]); + mockListComments([{ id: "12345", body: `${matchToken}This` }]); + + nock("https://api.github.com") + .delete("/repos/mheap/missing-repo/issues/comments/12345") + .reply(200); + + await action(); + }); }); describe("success", () => { @@ -369,9 +392,58 @@ describe("Required Labels", () => { mockLabels(["enhancement", "bug"]); + mockListComments([]); + + nock("https://api.github.com") + .post("/repos/mheap/missing-repo/issues/28/comments", { + body: `${matchToken}This is a static comment`, + }) + .reply(201); + + await action(); + }); + + it("updates an existing comment when one is found", async () => { + restoreTest = mockPr({ + GITHUB_TOKEN: "abc123", + INPUT_LABELS: "enhancement,bug", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + INPUT_ADD_COMMENT: "true", + INPUT_MESSAGE: "This is a static comment", + }); + + mockLabels(["enhancement", "bug"]); + + mockListComments([{ id: "12345", body: `${matchToken}This` }]); + + nock("https://api.github.com") + .patch("/repos/mheap/missing-repo/issues/comments/12345", { + issue_number: 28, + body: `${matchToken}This is a static comment`, + }) + .reply(200); + + await action(); + }); + + it("creates when comments exist but don't match", async () => { + restoreTest = mockPr({ + GITHUB_TOKEN: "abc123", + INPUT_LABELS: "enhancement,bug", + INPUT_MODE: "exactly", + INPUT_COUNT: "1", + INPUT_ADD_COMMENT: "true", + INPUT_MESSAGE: "This is a static comment", + }); + + mockLabels(["enhancement", "bug"]); + + mockListComments([{ id: "12345", body: `No Match` }]); + nock("https://api.github.com") .post("/repos/mheap/missing-repo/issues/28/comments", { - body: "This is a static comment", + body: `${matchToken}This is a static comment`, }) .reply(201); @@ -392,9 +464,10 @@ describe("Required Labels", () => { mockLabels(["enhancement", "bug"]); + mockListComments([]); nock("https://api.github.com") .post("/repos/mheap/missing-repo/issues/28/comments", { - body: "Mode: exactly, Count: 1, Error String: exactly, Provided: enhancement, bug, Applied: enhancement, bug", + body: `${matchToken}Mode: exactly, Count: 1, Error String: exactly, Provided: enhancement, bug, Applied: enhancement, bug`, }) .reply(201); @@ -428,6 +501,17 @@ function mockLabels(labels) { ); } +function mockListComments(comments) { + nock("https://api.github.com") + .get("/repos/mheap/missing-repo/issues/28/comments") + .reply( + 200, + comments.map((c) => { + return { body: c.body, id: c.id }; + }) + ); +} + function mockEvent(eventName, mockPayload, additionalParams = {}) { github.context.payload = mockPayload;