diff --git a/README.md b/README.md index c967471..1d77dde 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,10 @@ By default, this action will allow any valid Issue Key so long as it *could* be A specific Project Key to always check for +### `projectKeys` + +Multiple Project Keys to always check for + ### `separator` A specific separator to use. Defaults to a space character. @@ -65,6 +69,9 @@ A specific separator to use. Defaults to a space character. Allows the Jira Project Key, Issue # and separator to be anywhere in the title. Defaults to false. + +Note that `projectKey` and `projectKeys` works same under the hood. You can pass either one of them depending on the usecase. + ## Example Usage ``` @@ -81,6 +88,18 @@ Allows the Jira Project Key, Issue # and separator to be anywhere in the title. projectKey: 'AB' ``` +## Example Usage with a multiple Project Keys + +``` +- name: Enforce Jira Issue Key in Pull Request Title + uses: ryanvade/enforce-pr-title-style-action@v2 + with: + projectKeys: | + 'AB' + 'CD' + 'EF' +``` + ## Example Usage with a specific Project Key and a separator ``` diff --git a/__tests__/index.test.ts b/__tests__/index.test.ts index 31702ac..6d569e4 100644 --- a/__tests__/index.test.ts +++ b/__tests__/index.test.ts @@ -3,12 +3,15 @@ import { readFileSync } from "fs"; import { getPullRequestTitle, getRegex } from "../src/main"; const projectKeyInputName = "projectKey"; +const projectKeysInputName = 'projectKeys'; const separatorKeyInputName = "separator"; const keyAnywhereInTitle = "keyAnywhereInTitle"; const resetEnvironmentVariables = () => { process.env[`INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}`] = ""; + process.env[`INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}`] = + ""; process.env[ `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` ] = ""; @@ -47,98 +50,271 @@ describe("index", () => { describe("getRegex", () => { beforeEach(() => resetEnvironmentVariables()); - it("gets the default when no project key is provided", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = ""; - const regex = getRegex(); - const defaultRegex = - // eslint-disable-next-line no-useless-escape - /(?<=^|[a-z]-|[\s\p{Punct}&[^\-]])([A-Z][A-Z0-9_]*-\d+)(?![^\W_])(\s)+(.)+/; - expect(regex).toEqual(defaultRegex); - expect(regex.test("PR-4 this is valid")).toBe(true); - }); + describe("when projectKey is provided", () => { + it("uses a project key if it exists", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + const regex = getRegex(); + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(new RegExp(`(^AB-){1}(\\d)+(\\s)+(.)+`)); + expect(regex[0].test("AB-43 stuff and things")).toBe(true); + }); - it("uses a project key if it exists", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "AB"; - const regex = getRegex(); - expect(regex).toEqual(new RegExp(`(^AB-){1}(\\d)+(\\s)+(.)+`)); - expect(regex.test("AB-43 stuff and things")).toBe(true); - }); + it("throws an exception if the provided project key is not valid", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "aB"; + expect(getRegex).toThrow('Project Key "aB" is invalid'); + }); - it("throws an exception if the provided project key is not valid", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "aB"; - expect(getRegex).toThrow('Project Key "aB" is invalid'); - }); + it("uses a project key and a colon separator if they exist", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ":"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "false"; + const regex = getRegex(); + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(new RegExp(`(^AB-){1}(\\d)+(:)+(\\S)+(.)+`)); + expect(regex[0].test("AB-43: stuff and things")).toBe(false); + expect(regex[0].test("AB-123: PR Title")).toBe(false); + expect(regex[0].test("AB-43:stuff and things")).toBe(true); + expect(regex[0].test("AB-123:PR Title")).toBe(true); + }); - it("uses a project key and a colon separator if they exist", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "AB"; - process.env[ - `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = ":"; - process.env[ - `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` - ] = "false"; - const regex = getRegex(); - expect(regex).toEqual(new RegExp(`(^AB-){1}(\\d)+(:)+(\\S)+(.)+`)); - expect(regex.test("AB-43: stuff and things")).toBe(false); - expect(regex.test("AB-123: PR Title")).toBe(false); - expect(regex.test("AB-43:stuff and things")).toBe(true); - expect(regex.test("AB-123:PR Title")).toBe(true); - }); + it("uses a project key and an underscore separator if they exist", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "_"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "false"; + const regex = getRegex(); + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(new RegExp(`(^AB-){1}(\\d)+(_)+(\\S)+(.)+`)); + expect(regex[0].test("AB-43_stuff and things")).toBe(true); + expect(regex[0].test("AB-123_PR Title")).toBe(true); + }); - it("uses a project key and an underscore separator if they exist", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "AB"; - process.env[ - `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "_"; - process.env[ - `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` - ] = "false"; - const regex = getRegex(); - expect(regex).toEqual(new RegExp(`(^AB-){1}(\\d)+(_)+(\\S)+(.)+`)); - expect(regex.test("AB-43_stuff and things")).toBe(true); - expect(regex.test("AB-123_PR Title")).toBe(true); - }); + it("uses a project key if it exists anywhere in the title", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regex = getRegex(); + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(new RegExp(`(.)*(AB-){1}(\\d)+(\\s)+(.)+`)); + expect(regex[0].test("other words AB-43 stuff and things")).toBe(true); + }); - it("uses a project key if it exists anywhere in the title", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "AB"; - process.env[ - `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` - ] = "true"; - const regex = getRegex(); - expect(regex).toEqual(new RegExp(`(.)*(AB-){1}(\\d)+(\\s)+(.)+`)); - expect(regex.test("other words AB-43 stuff and things")).toBe(true); + it("uses a project key and a colon separator if they exist anywhere in the title", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ":"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regex = getRegex(); + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(new RegExp(`(.)*(AB-){1}(\\d)+(:)+(\\S)+(.)+`)); + expect(regex[0].test("other words AB-43: stuff and things")).toBe(false); + expect(regex[0].test("other words AB-123: PR Title")).toBe(false); + expect(regex[0].test("other words AB-43:stuff and things")).toBe(true); + expect(regex[0].test("other words AB-123:PR Title")).toBe(true); + expect(regex[0].test("AB-43:stuff and things")).toBe(true); + expect(regex[0].test("AB-123:PR Title")).toBe(true); + }); }); - it("uses a project key and a colon separator if they exist anywhere in the title", () => { - process.env[ - `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = "AB"; - process.env[ - `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` - ] = ":"; - process.env[ - `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` - ] = "true"; - const regex = getRegex(); - expect(regex).toEqual(new RegExp(`(.)*(AB-){1}(\\d)+(:)+(\\S)+(.)+`)); - expect(regex.test("other words AB-43: stuff and things")).toBe(false); - expect(regex.test("other words AB-123: PR Title")).toBe(false); - expect(regex.test("other words AB-43:stuff and things")).toBe(true); - expect(regex.test("other words AB-123:PR Title")).toBe(true); - expect(regex.test("AB-43:stuff and things")).toBe(true); - expect(regex.test("AB-123:PR Title")).toBe(true); + describe("when projectKeys are provided", () => { + it("uses a project key if it exists in projectKeys", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + const regexes = getRegex(); + expect(regexes.length).toEqual(1); + expect(regexes[0]).toEqual(new RegExp(`(^AB-){1}(\\d)+(\\s)+(.)+`)); + expect(regexes[0].test("AB-43 stuff and things")).toBe(true); + }); + + it("throws an exception if the provided project key is not valid", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "aB"; + expect(getRegex).toThrow('Project Key "aB" is invalid'); + }); + + it("throws an exception if one of the provided project key is not valid", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB\naB\nCD"; + expect(getRegex).toThrow('Project Key "aB" is invalid'); + }); + + it("uses a project key and a colon separator if they exist", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ":"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "false"; + const regexes = getRegex(); + expect(regexes.length).toEqual(1); + const regex = regexes[0]; + expect(regex).toEqual(new RegExp(`(^AB-){1}(\\d)+(:)+(\\S)+(.)+`)); + expect(regex.test("AB-43: stuff and things")).toBe(false); + expect(regex.test("AB-123: PR Title")).toBe(false); + expect(regex.test("AB-43:stuff and things")).toBe(true); + expect(regex.test("AB-123:PR Title")).toBe(true); + }); + + it("uses a project key and an underscore separator if they exist", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "_"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "false"; + const regexes = getRegex(); + expect(regexes.length).toEqual(1); + const regex = regexes[0]; + expect(regex).toEqual(new RegExp(`(^AB-){1}(\\d)+(_)+(\\S)+(.)+`)); + expect(regex.test("AB-43_stuff and things")).toBe(true); + expect(regex.test("AB-123_PR Title")).toBe(true); + }); + + it("uses a project key if it exists anywhere in the title", () => { + const projectNames: string[] = ["AB", "CD", "EF"]; + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB\nCD\nEF"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regexes = getRegex(); + expect(regexes.length).not.toEqual(1); + regexes.forEach((regex: RegExp, index: number) => { + expect(regex).toEqual(new RegExp(`(.)*(${projectNames[index]}-){1}(\\d)+(\\s)+(.)+`)); + expect(regex.test(`other words ${projectNames[index]}-43 stuff and things`)).toBe(true); + }) + }); + + it("uses a project key and a colon separator if they exist anywhere in the title", () => { + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ":"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regexes = getRegex(); + expect(regexes.length).toEqual(1); + const regex = regexes[0]; + expect(regex).toEqual(new RegExp(`(.)*(AB-){1}(\\d)+(:)+(\\S)+(.)+`)); + expect(regex.test("other words AB-43: stuff and things")).toBe(false); + expect(regex.test("other words AB-123: PR Title")).toBe(false); + expect(regex.test("other words AB-43:stuff and things")).toBe(true); + expect(regex.test("other words AB-123:PR Title")).toBe(true); + expect(regex.test("AB-43:stuff and things")).toBe(true); + expect(regex.test("AB-123:PR Title")).toBe(true); + }); + + it("uses a project key and a colon separator if they exist", () => { + const projectNames: string[] = ["AB", "CD", "EF", "GH"]; + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB\nCD\nEF\nGH"; + process.env[ + `INPUT_${separatorKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ":"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "false"; + const regexCollection = getRegex(); + regexCollection.forEach((regex: RegExp, index: number) => { + expect(regex).toEqual(new RegExp(`(^${projectNames[index]}-){1}(\\d)+(:)+(\\S)+(.)+`)); + expect(regex.test(`${projectNames[index]}-43: stuff and things`)).toBe(false); + expect(regex.test(`${projectNames[index]}-123: PR Title`)).toBe(false); + expect(regex.test(`${projectNames[index]}-43:stuff and things`)).toBe(true); + expect(regex.test(`${projectNames[index]}-123:PR Title`)).toBe(true); + }) + }); }); + + describe("when projectKey and projectKeys both are provided", () => { + + it("gets the default when no project key is provided", () => { + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = ""; + const regex = getRegex(); + const defaultRegex = + // eslint-disable-next-line no-useless-escape + /(?<=^|[a-z]-|[\s\p{Punct}&[^\-]])([A-Z][A-Z0-9_]*-\d+)(?![^\W_])(\s)+(.)+/; + expect(regex.length).toEqual(1); + expect(regex[0]).toEqual(defaultRegex); + expect(regex[0].test("PR-4 this is valid")).toBe(true); + }); + + it("uses project key if it exists anywhere in the title", () => { + const projectNames: string[] = ["AB", "CD", "EF", "GH"]; + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB\nCD\nEF"; + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "GH"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regexes = getRegex(); + expect(regexes.length).not.toEqual(1); + regexes.forEach((regex: RegExp, index: number) => { + expect(regex).toEqual(new RegExp(`(.)*(${projectNames[index]}-){1}(\\d)+(\\s)+(.)+`)); + expect(regex.test(`other words ${projectNames[index]}-43 stuff and things`)).toBe(true); + }) + }); + + it("multiple project keys are present", () => { + const projectNames: string[] = ["AB", "CD"]; + process.env[ + `INPUT_${projectKeysInputName.replace(/ /g, "_").toUpperCase()}` + ] = "AB\n"; + process.env[ + `INPUT_${projectKeyInputName.replace(/ /g, "_").toUpperCase()}` + ] = "CD"; + process.env[ + `INPUT_${keyAnywhereInTitle.replace(/ /g, "_").toUpperCase()}` + ] = "true"; + const regexes = getRegex(); + expect(regexes.length).not.toEqual(1); + regexes.forEach((regex: RegExp, index: number) => { + expect(regex).toEqual(new RegExp(`(.)*(${projectNames[index]}-){1}(\\d)+(\\s)+(.)+`)); + expect(regex.test(`other words ${projectNames[index]}-43 CD-43 stuff and things`)).toBe(true); + expect(regex.test(`other words CD-43 ${projectNames[index]}-43 stuff and things`)).toBe(true); + }) + }); + }) }); }); diff --git a/src/main.ts b/src/main.ts index 8dd0999..fa1fa12 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,47 +5,76 @@ export const run = async () => { try { core.debug("Starting PR Title check for Jira Issue Key"); const title = getPullRequestTitle(); - const regex = getRegex(); + const allPossibleRegex = getRegex(); core.debug(title); - core.debug(regex.toString()); + core.debug(allPossibleRegex.toString()); - if (!regex.test(title)) { - core.debug(`Regex ${regex} failed with title ${title}`); - core.info("Title Failed"); - core.setFailed("PullRequest title does not start with a Jira Issue key."); - return; + for (const regex of allPossibleRegex) { + if (regex.test(title)) { + core.info("Title Passed"); + return; + } } - core.info("Title Passed"); + core.debug(`Regex ${allPossibleRegex} failed with title ${title}`); + core.info("Title Failed"); + core.setFailed("PullRequest title does not start with any Jira Issue key."); + return; // eslint-disable-next-line @typescript-eslint/no-explicit-any } catch (error: any) { core.setFailed(error.message); } }; -export const getRegex = () => { - const projectKey = core.getInput("projectKey", { required: false }); +export const getRegex = (): RegExp[] => { + const projectKeyInput = core.getInput("projectKey", { required: false }); + let projectKeysInput = core.getInput("projectKeys", { required: false }); const separator = core.getInput("separator", { required: false }); const keyAnywhereInTitle = core.getBooleanInput("keyAnywhereInTitle", { required: false, }); - core.debug(`Project Key ${projectKey}`); + core.debug(`Project Key ${projectKeyInput}`); + core.debug(`Project Keys ${projectKeysInput}`); core.debug(`Separator ${separator}`); core.debug(`Key Anywhere In Title ${keyAnywhereInTitle}`); - if (!projectKey || projectKey === "") return getDefaultJiraIssueRegex(); + if ((!projectKeysInput || projectKeysInput === "") && (!projectKeyInput || projectKeyInput === "")) return [getDefaultJiraIssueRegex()]; - if (!isValidProjectKey(projectKey)) - throw new Error(`Project Key "${projectKey}" is invalid`); + const projectKeys: string[] = []; + // if there is any input in projectKeys. + // input separated by multiple lines: split and consume. + projectKeysInput = projectKeysInput.trim(); // remove extra spaces. + if (projectKeysInput.length > 0) { + projectKeysInput.split('\n').forEach((project: string) => { + projectKeys.push(project.trim()); + }); + } + if (projectKeyInput) { + projectKeys.push(projectKeyInput); + } - if (!separator || separator === "") - return getRegexWithProjectKey(projectKey, keyAnywhereInTitle); + projectKeys.forEach((projectName: string) => { + if (!isValidProjectKey(projectName)) + throw new Error(`Project Key "${projectName}" is invalid`); + }); - return getRegexWithProjectKeyAndSeparator( - projectKey, - separator, - keyAnywhereInTitle, - ); + const allPossibleRegex: RegExp[] = []; + + if (!separator || separator === "") { + projectKeys.forEach((projectName: string) => { + allPossibleRegex.push(getRegexWithProjectKey(projectName, keyAnywhereInTitle)); + }); + return allPossibleRegex; + } + + projectKeys.forEach((projectName: string) => { + allPossibleRegex.push(getRegexWithProjectKeyAndSeparator( + projectName, + separator, + keyAnywhereInTitle, + )); + }); + return allPossibleRegex; }; export const getPullRequestTitle = () => { const pull_request = github.context.payload.pull_request;