Skip to content

Commit

Permalink
Add Regex support
Browse files Browse the repository at this point in the history
  • Loading branch information
mheap committed Feb 11, 2024
1 parent d1635a6 commit 3aed3ee
Show file tree
Hide file tree
Showing 4 changed files with 134 additions and 10 deletions.
14 changes: 13 additions & 1 deletion README.md
Expand Up @@ -8,13 +8,14 @@ This action has three required inputs; `labels`, `mode` and `count`

| Name | Description | Required | Default |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | -------- | ------------------- |
| `labels` | Comma or new line separated list of labels to match | true |
| `labels` | New line 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 |
| `use_regex` | Evaluate the values in `labels` as regular expressions | 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.

Expand Down Expand Up @@ -104,6 +105,17 @@ The following tokens are available for use in custom messages:
labels: "community-reviewed, team-reviewed, codeowner-reviewed"
```

### Use regular expressions

```yaml
- uses: mheap/github-action-required-labels@v5
with:
mode: exactly
count: 1
labels: "semver:.*"
use_regex: true
```

### Preventing comment collisions

This action uses a combination of the workflow name/path, job ID, and step ID to add an invisible "match token" to the beginning of any comments it creates. That way, it can later know which comments it owns when modifying them, while supporting multiple "instances" of this action to be run at the same time within a repo.
Expand Down
5 changes: 4 additions & 1 deletion action.yml
Expand Up @@ -15,7 +15,7 @@ inputs:
default: ${{ github.token }}
required: false
labels:
description: "Comma separated list of labels to match"
description: "Comma or new line separated list of labels to match"
required: true
mode:
description: "The mode of comparison to use. One of: exactly, minimum, maximum"
Expand All @@ -34,3 +34,6 @@ inputs:
exit_type:
description: "The exit type of the action. One of: failure, success"
required: false
use_regex:
description: Evaluate the values in `labels` as regular expressions
default: "false"
38 changes: 30 additions & 8 deletions index.js
Expand Up @@ -20,16 +20,29 @@ async function action() {
// Process inputs for use later
const mode = core.getInput("mode", { required: true });
const count = parseInt(core.getInput("count", { required: true }), 10);
const providedLabels = core
.getInput("labels", { required: true })
.split("\n")
.join(",")
.split(",")
.map((l) => l.trim())
.filter((r) => r);

const exitType = core.getInput("exit_type") || "failure";
const shouldAddComment = core.getInput("add_comment") == "true";
const labelsAreRegex = core.getInput("use_regex") == "true";

let providedLabels = core.getInput("labels", { required: true });

providedLabels = core.getInput("labels", { required: true });
if (labelsAreRegex) {
// If labels are regex they must be provided as new line delimited
providedLabels = providedLabels.split("\n");
} else {
// Comma separated are allowed for exact string matches
// This may be removed in the next major version
providedLabels = providedLabels
.split("\n")
.join(",")
.split(",")
.map((l) => l.trim());
}

// Remove any empty labels
providedLabels = providedLabels.filter((r) => r);

const allowedModes = ["exactly", "minimum", "maximum"];
if (!allowedModes.includes(mode)) {
Expand Down Expand Up @@ -70,7 +83,16 @@ async function action() {
const appliedLabels = labels.map((label) => label.name);

// How many labels overlap?
let intersection = providedLabels.filter((x) => appliedLabels.includes(x));
let intersection = [];
if (labelsAreRegex) {
intersection = appliedLabels.filter((appliedLabel) =>
providedLabels.some((providedLabel) =>
new RegExp(providedLabel).test(appliedLabel)
)
);
} else {
intersection = providedLabels.filter((x) => appliedLabels.includes(x));
}

// Is there an error?
let errorMode;
Expand Down
87 changes: 87 additions & 0 deletions index.test.js
Expand Up @@ -172,6 +172,55 @@ describe("Required Labels", () => {
expect(core.setOutput).toBeCalledWith("status", "success");
expect(core.setOutput).toBeCalledWith("labels", "enhancement,bug");
});

it("exactly (regex)", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhance.*",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
INPUT_USE_REGEX: "true",
});
mockLabels(["enhancement"]);

await action();

expect(core.setOutput).toBeCalledTimes(2);
expect(core.setOutput).toBeCalledWith("status", "success");
expect(core.setOutput).toBeCalledWith("labels", "enhancement");
});

it("at least X (regex)", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhance.*\nbug\ntriage",
INPUT_MODE: "minimum",
INPUT_COUNT: "2",
INPUT_USE_REGEX: "true",
});
mockLabels(["enhancement", "bug"]);

await action();

expect(core.setOutput).toBeCalledTimes(2);
expect(core.setOutput).toBeCalledWith("status", "success");
expect(core.setOutput).toBeCalledWith("labels", "enhancement,bug");
});

it("at most X (regex)", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhance.*\nbug\ntriage",
INPUT_MODE: "maximum",
INPUT_COUNT: "2",
INPUT_USE_REGEX: "true",
});

mockLabels(["enhancement", "bug"]);

await action();

expect(core.setOutput).toBeCalledTimes(2);
expect(core.setOutput).toBeCalledWith("status", "success");
expect(core.setOutput).toBeCalledWith("labels", "enhancement,bug");
});
});

describe("failure", () => {
Expand All @@ -193,6 +242,44 @@ describe("Required Labels", () => {
);
});

it("exact count (regex)", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhance.*\nbug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
INPUT_USE_REGEX: "true",
});
mockLabels(["enhancement", "bug"]);

await action();

expect(core.setOutput).toBeCalledTimes(1);
expect(core.setOutput).toBeCalledWith("status", "failure");
expect(core.setFailed).toBeCalledTimes(1);
expect(core.setFailed).toBeCalledWith(
"Label error. Requires exactly 1 of: enhance.*, bug. Found: enhancement, bug"
);
});

it("fails when regex are provided as comma delimited", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhance.*, bug",
INPUT_MODE: "exactly",
INPUT_COUNT: "1",
INPUT_USE_REGEX: "true",
});
mockLabels(["enhancement", "bug"]);

await action();

expect(core.setOutput).toBeCalledTimes(1);
expect(core.setOutput).toBeCalledWith("status", "failure");
expect(core.setFailed).toBeCalledTimes(1);
expect(core.setFailed).toBeCalledWith(
"Label error. Requires exactly 1 of: enhance.*, bug. Found: enhancement, bug"
);
});

it("at least X", async () => {
restoreTest = mockPr({
INPUT_LABELS: "enhancement,bug,triage",
Expand Down

0 comments on commit 3aed3ee

Please sign in to comment.