Skip to content

Commit

Permalink
feat: Add support for vanity URLs (#1017)
Browse files Browse the repository at this point in the history
Signed-off-by: Ian Lewis <[email protected]>
  • Loading branch information
Ian Lewis authored Nov 9, 2023
1 parent 3c12e3f commit a1c5d2d
Show file tree
Hide file tree
Showing 13 changed files with 8,846 additions and 73 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
Basic](https://learn.microsoft.com/en-us/dotnet/visual-basic/) was added.
- Support for recognizing multi-line comments only at the beginning of a line
(Ruby, Perl) was added.
- Added support for [vanity urls](actions/issue-reopener/README.md#vanityurls)
to the issue-reopener action.

## [0.6.0] - 2023-09-23

Expand Down
31 changes: 26 additions & 5 deletions actions/issue-reopener/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,12 +44,33 @@ jobs:
## Inputs
| Name | Required | Default | Description |
| ------- | -------- | ------------------ | -------------------------------------------------------------------------- |
| path | No | `github.workspace` | The root path of the source code to search. |
| token | No | `github.token` | The GitHub token to use. This token must have `issues: write` permissions. |
| dry-run | No | false | If true, issues are only output to logs and not actually reopened. |
| Name | Required | Default | Description |
| ----------- | -------- | ---------------------------- | -------------------------------------------------------------------------- |
| path | No | `github.workspace` | The root path of the source code to search. |
| token | No | `github.token` | The GitHub token to use. This token must have `issues: write` permissions. |
| dry-run | No | `false` | If true, issues are only output to logs and not actually reopened. |
| config-path | No | `.github/issue-reopener.yml` | Path to an optional [config file](#configuration). |

## Outputs

There are currently no outputs.

## Configuration

An optional configuration file in YAML format can added to your repository.

### vanityURLs

Some projects use a custom vanity URL for issues. `vanityURLs` is a list of
[`RegExp`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp)
values used to match URLs and extract a GitHub issue number. The issue number
must be extracted with a
[named capturing group](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Regular_expressions/Named_capturing_group)
named "id".

Example:

```yaml
vanityURLs:
- "^\\s*(https?://)?golang.org/issues/(?<id>[0-9]+)\\s*$",
```
12 changes: 9 additions & 3 deletions actions/issue-reopener/__tests__/action.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,17 @@ describe("runAction", () => {

const workspacePath = "/home/user";
const githubToken = "deadbeef";
const configPath = ".todos.yml";
const dryRun = false;

process.env.INPUT_PATH = workspacePath;
process.env.INPUT_TOKEN = githubToken;
process.env["INPUT_CONFIG-PATH"] = configPath;
process.env["INPUT_DRY-RUN"] = String(dryRun);

await action.runAction();

expect(reopener.getTODOIssues).toBeCalledWith(workspacePath);
expect(reopener.getTODOIssues).toBeCalledWith(workspacePath, {});
expect(reopener.reopenIssues).toBeCalledWith(
workspacePath,
[],
Expand All @@ -63,15 +65,17 @@ describe("runAction", () => {

const workspacePath = "/home/user";
const githubToken = "deadbeef";
const configPath = ".todos.yml";
const dryRun = false;

process.env.INPUT_PATH = workspacePath;
process.env.INPUT_TOKEN = githubToken;
process.env["INPUT_CONFIG-PATH"] = configPath;
process.env["INPUT_DRY-RUN"] = String(dryRun);

await action.runAction();

expect(reopener.getTODOIssues).toBeCalledWith(workspacePath);
expect(reopener.getTODOIssues).toBeCalledWith(workspacePath, {});

expect(process.exitCode).not.toBe(0);
});
Expand All @@ -82,15 +86,17 @@ describe("runAction", () => {

const workspacePath = "/home/user";
const githubToken = "deadbeef";
const configPath = ".todos.yml";
const dryRun = false;

process.env.INPUT_PATH = workspacePath;
process.env.INPUT_TOKEN = githubToken;
process.env["INPUT_CONFIG-PATH"] = configPath;
process.env["INPUT_DRY-RUN"] = String(dryRun);

await action.runAction();

expect(reopener.getTODOIssues).toBeCalledWith(workspacePath);
expect(reopener.getTODOIssues).toBeCalledWith(workspacePath, {});
expect(reopener.reopenIssues).toBeCalledWith(
workspacePath,
[],
Expand Down
63 changes: 63 additions & 0 deletions actions/issue-reopener/__tests__/config.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright 2023 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

import * as fs from "fs";
import * as os from "os";
import * as path from "path";

import YAML from "yaml";

import * as config from "../src/config";

describe("readConfig", () => {
it("handles non-existant config", async () => {
let c = await config.readConfig("not-exists.yml");
expect(c).toEqual({});
});

it("handles empty config", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "readConfig"));
const configPath = path.join(tmpDir, "empty.yml");
fs.writeFileSync(configPath, "");

let c = await config.readConfig(configPath);

expect(c).toEqual({});
});

it("handles basic config", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "readConfig"));
const configPath = path.join(tmpDir, "todos.yml");
fs.writeFileSync(
configPath,
'vanityURLs:\n - "(https?://)?golang.org/issues/(?<id>[0-9]+)"',
);

let c = await config.readConfig(configPath);

expect(c).toEqual({
vanityURLs: ["(https?://)?golang.org/issues/(?<id>[0-9]+)"],
});
});

it("handles invalid config", async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "readConfig"));
const configPath = path.join(tmpDir, "todos.yml");
fs.writeFileSync(configPath, "vanityURLs:\n- ,");

expect(() => config.readConfig(configPath)).rejects.toThrow(
YAML.YAMLParseError,
);
});
});
109 changes: 89 additions & 20 deletions actions/issue-reopener/__tests__/reopener.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,9 +79,9 @@ describe("getTODOIssues", () => {
stderr: "",
});

await expect(reopener.getTODOIssues(workspacePath)).resolves.toHaveLength(
0,
);
await expect(
reopener.getTODOIssues(workspacePath, {}),
).resolves.toHaveLength(0);
});

it("skips non-match", async () => {
Expand All @@ -107,9 +107,9 @@ describe("getTODOIssues", () => {
stderr: "",
});

await expect(reopener.getTODOIssues(workspacePath)).resolves.toHaveLength(
0,
);
await expect(
reopener.getTODOIssues(workspacePath, {}),
).resolves.toHaveLength(0);
});

it("skips links to other repos", async () => {
Expand All @@ -135,9 +135,9 @@ describe("getTODOIssues", () => {
stderr: "",
});

await expect(reopener.getTODOIssues(workspacePath)).resolves.toHaveLength(
0,
);
await expect(
reopener.getTODOIssues(workspacePath, {}),
).resolves.toHaveLength(0);
});

it("handles malformed url", async () => {
Expand All @@ -163,9 +163,9 @@ describe("getTODOIssues", () => {
stderr: "",
});

await expect(reopener.getTODOIssues(workspacePath)).resolves.toHaveLength(
0,
);
await expect(
reopener.getTODOIssues(workspacePath, {}),
).resolves.toHaveLength(0);
});

it("matches issue number only", async () => {
Expand All @@ -191,7 +191,7 @@ describe("getTODOIssues", () => {
stderr: "",
});

let p = reopener.getTODOIssues(workspacePath);
let p = reopener.getTODOIssues(workspacePath, {});
await expect(p).resolves.toHaveLength(1);
let issues = await p;

Expand Down Expand Up @@ -223,7 +223,7 @@ describe("getTODOIssues", () => {
stderr: "",
});

let p = reopener.getTODOIssues(workspacePath);
let p = reopener.getTODOIssues(workspacePath, {});
await expect(p).resolves.toHaveLength(1);
let issues = await p;

Expand Down Expand Up @@ -255,7 +255,7 @@ describe("getTODOIssues", () => {
stderr: "",
});

let p = reopener.getTODOIssues(workspacePath);
let p = reopener.getTODOIssues(workspacePath, {});
await expect(p).resolves.toHaveLength(1);
let issues = await p;

Expand Down Expand Up @@ -287,7 +287,7 @@ describe("getTODOIssues", () => {
stderr: "",
});

let p = reopener.getTODOIssues(workspacePath);
let p = reopener.getTODOIssues(workspacePath, {});
await expect(p).resolves.toHaveLength(2);
let issues = await p;

Expand Down Expand Up @@ -323,9 +323,9 @@ describe("getTODOIssues", () => {
stderr: "ERROR",
});

await expect(reopener.getTODOIssues(workspacePath)).rejects.toBeInstanceOf(
reopener.ReopenError,
);
await expect(
reopener.getTODOIssues(workspacePath, {}),
).rejects.toBeInstanceOf(reopener.ReopenError);
});

it("handles checkout in sub-dir", async () => {
Expand Down Expand Up @@ -353,7 +353,7 @@ describe("getTODOIssues", () => {
});

await expect(
reopener.getTODOIssues(path.join(repoRoot, "path/to")),
reopener.getTODOIssues(path.join(repoRoot, "path/to"), {}),
).resolves.toHaveLength(1);

expect(exec.getExecOutput).toBeCalledWith(
Expand Down Expand Up @@ -562,3 +562,72 @@ describe("reopenIssues", () => {
expect(issues.update).toHaveBeenCalledTimes(2);
});
});

describe("labelMatch", () => {
it("github url", async () => {
let num = reopener.matchLabel(
"https://github.com/owner/repo/issues/123",
{},
);
expect(num).toBe(123);
});

it("github url with spaces", async () => {
let num = reopener.matchLabel(
" \thttps://github.com/owner/repo/issues/123 ",
{},
);
expect(num).toBe(123);
});

it("github url no scheme", async () => {
let num = reopener.matchLabel("github.com/owner/repo/issues/123", {});
expect(num).toBe(123);
});

it("github url different repo", async () => {
let num = reopener.matchLabel("github.com/owner/other/issues/123", {});
expect(num).toBe(-1);
});

it("github url different repo", async () => {
let num = reopener.matchLabel("github.com/owner/other/issues/123", {});
expect(num).toBe(-1);
});

it("num only", async () => {
let num = reopener.matchLabel("123", {});
expect(num).toBe(123);
});

it("num with #", async () => {
let num = reopener.matchLabel("#123", {});
expect(num).toBe(123);
});

it("no match", async () => {
let num = reopener.matchLabel("no match", {});
expect(num).toBe(-1);
});

it("vanity url", async () => {
let num = reopener.matchLabel("golang.org/issues/123", {
vanityURLs: ["^golang.org/issues/(?<id>[0-9]+)$"],
});
expect(num).toBe(123);
});

it("vanity url no match", async () => {
let num = reopener.matchLabel("golang.org/issues", {
vanityURLs: ["^golang.org/issues/(?<id>[0-9]+)$"],
});
expect(num).toBe(-1);
});

it("vanity url error", async () => {
let num = reopener.matchLabel("golang.org/issues/123", {
vanityURLs: ["^golang.org/issues/(?<id>[0-9]+$"],
});
expect(num).toBe(-1);
});
});
4 changes: 4 additions & 0 deletions actions/issue-reopener/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@
name: "TODO Issue Reopener"
description: "Reopen issues that are referenced by TODOs"
inputs:
config-path:
description: "The path to the config file."
required: false
default: ".github/issue-reopener.yml"
path:
description: "Base path to search."
required: false
Expand Down
Loading

0 comments on commit a1c5d2d

Please sign in to comment.