diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index d742df8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,16 +0,0 @@ -FROM node:10-alpine - -LABEL "com.github.actions.name"="Create an issue" -LABEL "com.github.actions.description"="Creates a new issue using a template with front matter." -LABEL "com.github.actions.icon"="alert-circle" -LABEL "com.github.actions.color"="gray-dark" - -LABEL "repository"="https://github.com/JasonEtco/create-an-issue" -LABEL "homepage"="https://github.com/JasonEtco/create-an-issue" -LABEL "maintainer"="Jason Etcovitch " - -COPY package*.json ./ -RUN npm ci -COPY . . - -ENTRYPOINT ["node", "/index.js"] diff --git a/codecov.yml b/codecov.yml index 477ee9f..beddfc0 100644 --- a/codecov.yml +++ b/codecov.yml @@ -8,4 +8,4 @@ coverage: default: threshold: 3 -comment: false \ No newline at end of file +comment: false diff --git a/package-lock.json b/package-lock.json index 359b423..f60f99d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,8 @@ "front-matter": "^4.0.2", "js-yaml": "^4.1.0", "nunjucks": "^3.2.3", - "nunjucks-date-filter": "^0.1.1" + "nunjucks-date-filter": "^0.1.1", + "zod": "^3.20.2" }, "devDependencies": { "@tsconfig/node12": "^1.0.11", @@ -5601,6 +5602,14 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.20.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.20.2.tgz", + "integrity": "sha512-1MzNQdAvO+54H+EaK5YpyEy0T+Ejo/7YLHS93G3RnYWh5gaotGHwGeN/ZO687qEDU2y4CdStQYXVHIgrUl5UVQ==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d994823..a8f8bf4 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "front-matter": "^4.0.2", "js-yaml": "^4.1.0", "nunjucks": "^3.2.3", - "nunjucks-date-filter": "^0.1.1" + "nunjucks-date-filter": "^0.1.1", + "zod": "^3.20.2" }, "devDependencies": { "@tsconfig/node12": "^1.0.11", diff --git a/src/action.ts b/src/action.ts index 3e7fae5..1319df9 100644 --- a/src/action.ts +++ b/src/action.ts @@ -1,113 +1,155 @@ -import * as core from '@actions/core' -import { Toolkit } from 'actions-toolkit' -import fm from 'front-matter' -import nunjucks from 'nunjucks' -// @ts-ignore -import dateFilter from 'nunjucks-date-filter' -import { FrontMatterAttributes, listToArray, setOutputs } from './helpers' - -function logError(tools: Toolkit, template: string, action: 'creating' | 'updating', err: any) { +import * as core from "@actions/core"; +import { Toolkit } from "actions-toolkit"; +import fm from "front-matter"; +import nunjucks from "nunjucks"; +// @ts-expect-error +import dateFilter from "nunjucks-date-filter"; +import { ZodError } from "zod"; +import { + FrontMatterAttributes, + frontmatterSchema, + listToArray, + setOutputs, +} from "./helpers"; + +function logError( + tools: Toolkit, + template: string, + action: "creating" | "updating" | "parsing", + err: any +) { // Log the error message - const errorMessage = `An error occurred while ${action} the issue. This might be caused by a malformed issue title, or a typo in the labels or assignees. Check ${template}!` - tools.log.error(errorMessage) - tools.log.error(err) + const errorMessage = `An error occurred while ${action} the issue. This might be caused by a malformed issue title, or a typo in the labels or assignees. Check ${template}!`; + tools.log.error(errorMessage); + tools.log.error(err); // The error might have more details - if (err.errors) tools.log.error(err.errors) + if (err.errors) tools.log.error(err.errors); // Exit with a failing status - core.setFailed(errorMessage + '\n\n' + err.message) - return tools.exit.failure() + core.setFailed(errorMessage + "\n\n" + err.message); + return tools.exit.failure(); } -export async function createAnIssue (tools: Toolkit) { - const template = tools.inputs.filename || '.github/ISSUE_TEMPLATE.md' - const assignees = tools.inputs.assignees +export async function createAnIssue(tools: Toolkit) { + const template = tools.inputs.filename || ".github/ISSUE_TEMPLATE.md"; + const assignees = tools.inputs.assignees; - let updateExisting: Boolean | null = null + let updateExisting: Boolean | null = null; if (tools.inputs.update_existing) { - if (tools.inputs.update_existing === 'true') { - updateExisting = true - } else if (tools.inputs.update_existing === 'false') { - updateExisting = false + if (tools.inputs.update_existing === "true") { + updateExisting = true; + } else if (tools.inputs.update_existing === "false") { + updateExisting = false; } else { - tools.exit.failure(`Invalid value update_existing=${tools.inputs.update_existing}, must be one of true or false`) + tools.exit.failure( + `Invalid value update_existing=${tools.inputs.update_existing}, must be one of true or false` + ); } } - const env = nunjucks.configure({ autoescape: false }) - env.addFilter('date', dateFilter) + const env = nunjucks.configure({ autoescape: false }); + env.addFilter("date", dateFilter); const templateVariables = { ...tools.context, repo: tools.context.repo, env: process.env, - date: Date.now() - } + date: Date.now(), + }; // Get the file - tools.log.debug('Reading from file', template) - const file = await tools.readFile(template) as string + tools.log.debug("Reading from file", template); + const file = (await tools.readFile(template)) as string; // Grab the front matter as JSON - const { attributes, body } = fm(file) - tools.log(`Front matter for ${template} is`, attributes) + const { attributes: rawAttributes, body } = fm(file); + + let attributes: FrontMatterAttributes; + try { + attributes = await frontmatterSchema.parseAsync(rawAttributes); + } catch (err) { + if (err instanceof ZodError) { + const formatted = err.format(); + return logError(tools, template, "parsing", formatted); + } + throw err; + } + + tools.log(`Front matter for ${template} is`, attributes); const templated = { body: env.renderString(body, templateVariables), - title: env.renderString(attributes.title, templateVariables) - } - tools.log.debug('Templates compiled', templated) + title: env.renderString(attributes.title, templateVariables), + }; + tools.log.debug("Templates compiled", templated); if (updateExisting !== null) { - tools.log.info(`Fetching issues with title "${templated.title}"`) + tools.log.info(`Fetching issues with title "${templated.title}"`); - let query = `is:issue repo:${process.env.GITHUB_REPOSITORY} in:title "${templated.title.replace(/['"]/g, "\\$&")}"` + let query = `is:issue repo:${ + process.env.GITHUB_REPOSITORY + } in:title "${templated.title.replace(/['"]/g, "\\$&")}"`; - const searchExistingType = tools.inputs.search_existing || 'open' - const allowedStates = ['open', 'closed'] + const searchExistingType = tools.inputs.search_existing || "open"; + const allowedStates = ["open", "closed"]; if (allowedStates.includes(searchExistingType)) { - query += ` is:${searchExistingType}` + query += ` is:${searchExistingType}`; } - const existingIssues = await tools.github.search.issuesAndPullRequests({ q: query }) - const existingIssue = existingIssues.data.items.find(issue => issue.title === templated.title) + const existingIssues = await tools.github.search.issuesAndPullRequests({ + q: query, + }); + const existingIssue = existingIssues.data.items.find( + (issue) => issue.title === templated.title + ); if (existingIssue) { if (updateExisting === false) { - tools.exit.success(`Existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url} found but not updated`) + tools.exit.success( + `Existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url} found but not updated` + ); } else { try { - tools.log.info(`Updating existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`) + tools.log.info( + `Updating existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}` + ); const issue = await tools.github.issues.update({ ...tools.context.repo, issue_number: existingIssue.number, - body: templated.body - }) - setOutputs(tools, issue.data) - tools.exit.success(`Updated issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`) + body: templated.body, + }); + setOutputs(tools, issue.data); + tools.exit.success( + `Updated issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}` + ); } catch (err: any) { - return logError(tools, template, 'updating', err) + return logError(tools, template, "updating", err); } } } else { - tools.log.info('No existing issue found to update') + tools.log.info("No existing issue found to update"); } } // Create the new issue - tools.log.info(`Creating new issue ${templated.title}`) + tools.log.info(`Creating new issue ${templated.title}`); try { const issue = await tools.github.issues.create({ ...tools.context.repo, ...templated, - assignees: assignees ? listToArray(assignees) : listToArray(attributes.assignees), + assignees: assignees + ? listToArray(assignees) + : listToArray(attributes.assignees), labels: listToArray(attributes.labels), - milestone: Number(tools.inputs.milestone || attributes.milestone) || undefined - }) - - setOutputs(tools, issue.data) - tools.log.success(`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`) + milestone: + Number(tools.inputs.milestone || attributes.milestone) || undefined, + }); + + setOutputs(tools, issue.data); + tools.log.success( + `Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}` + ); } catch (err: any) { - return logError(tools, template, 'creating', err) + return logError(tools, template, "creating", err); } } diff --git a/src/helpers.ts b/src/helpers.ts index 31134b4..8444a9c 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -1,18 +1,26 @@ -import { Toolkit } from 'actions-toolkit' +import { Toolkit } from "actions-toolkit"; +import { z } from "zod"; -export interface FrontMatterAttributes { - title: string - assignees?: string[] | string - labels?: string[] | string - milestone?: string | number -} +export const frontmatterSchema = z + .object({ + title: z.string(), + assignees: z.union([z.array(z.string()), z.string()]).optional(), + labels: z.union([z.array(z.string()), z.string()]).optional(), + milestone: z.union([z.string(), z.number()]).optional(), + }) + .strict(); + +export type FrontMatterAttributes = z.infer; -export function setOutputs (tools: Toolkit, issue: { number: number, html_url: string }) { - tools.outputs.number = String(issue.number) - tools.outputs.url = issue.html_url +export function setOutputs( + tools: Toolkit, + issue: { number: number; html_url: string } +) { + tools.outputs.number = String(issue.number); + tools.outputs.url = issue.html_url; } -export function listToArray (list?: string[] | string) { - if (!list) return [] - return Array.isArray(list) ? list : list.split(', ') -} \ No newline at end of file +export function listToArray(list?: string[] | string) { + if (!list) return []; + return Array.isArray(list) ? list : list.split(", "); +} diff --git a/src/index.ts b/src/index.ts index 0fdcadd..f78c0fc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,6 @@ -import { Toolkit } from 'actions-toolkit' -import { createAnIssue } from './action' +import { Toolkit } from "actions-toolkit"; +import { createAnIssue } from "./action"; Toolkit.run(createAnIssue, { - secrets: ['GITHUB_TOKEN'] -}) + secrets: ["GITHUB_TOKEN"], +}); diff --git a/tests/__snapshots__/index.test.ts.snap b/tests/__snapshots__/index.test.ts.snap index 5172e45..7340d64 100644 --- a/tests/__snapshots__/index.test.ts.snap +++ b/tests/__snapshots__/index.test.ts.snap @@ -232,6 +232,32 @@ exports[`create-an-issue logs a helpful error if creating an issue throws an err ] `; +exports[`create-an-issue logs a helpful error if the frontmatter is invalid 1`] = ` +[ + [ + "An error occurred while parsing the issue. This might be caused by a malformed issue title, or a typo in the labels or assignees. Check .github/invalid-frontmatter.md!", + ], + [ + { + "_errors": [ + "Unrecognized key(s) in object: 'name', 'not_a_thing'", + ], + "labels": { + "_errors": [ + "Expected array, received number", + "Expected string, received number", + ], + }, + "title": { + "_errors": [ + "Required", + ], + }, + }, + ], +] +`; + exports[`create-an-issue logs a helpful error if updating an issue throws an error with more errors 1`] = ` [ [ diff --git a/tests/fixtures/.github/invalid-frontmatter.md b/tests/fixtures/.github/invalid-frontmatter.md new file mode 100644 index 0000000..ab948e6 --- /dev/null +++ b/tests/fixtures/.github/invalid-frontmatter.md @@ -0,0 +1,6 @@ +--- +name: "Not a title" +labels: 123 +not_a_thing: "testing" +--- +Hi! \ No newline at end of file diff --git a/tests/fixtures/event.json b/tests/fixtures/event.json index d2bf5c4..85ef91e 100644 --- a/tests/fixtures/event.json +++ b/tests/fixtures/event.json @@ -1,6 +1,8 @@ { "repository": { - "owner": { "login": "JasonEtco" }, + "owner": { + "login": "JasonEtco" + }, "name": "waddup" } -} +} \ No newline at end of file diff --git a/tests/index.test.ts b/tests/index.test.ts index 2e95f2a..6db1df5 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -1,305 +1,338 @@ -import nock from 'nock' -import * as core from '@actions/core' -import { Toolkit } from 'actions-toolkit' -import { Signale } from 'signale' -import { createAnIssue } from '../src/action' +import nock from "nock"; +import * as core from "@actions/core"; +import { Toolkit } from "actions-toolkit"; +import { Signale } from "signale"; +import { createAnIssue } from "../src/action"; function generateToolkit() { const tools = new Toolkit({ - logger: new Signale({ disabled: true }) - }) + logger: new Signale({ disabled: true }), + }); - jest.spyOn(tools.log, 'info') - jest.spyOn(tools.log, 'error') - jest.spyOn(tools.log, 'success') + jest.spyOn(tools.log, "info"); + jest.spyOn(tools.log, "error"); + jest.spyOn(tools.log, "success"); // Turn core.setOutput into a mocked noop - jest.spyOn(core, 'setOutput') - .mockImplementation(() => {}) + jest.spyOn(core, "setOutput").mockImplementation(() => {}); // Turn core.setFailed into a mocked noop - jest.spyOn(core, 'setFailed') - .mockImplementation(() => {}) + jest.spyOn(core, "setFailed").mockImplementation(() => {}); - tools.exit.success = jest.fn() as any - tools.exit.failure = jest.fn() as any + tools.exit.success = jest.fn() as any; + tools.exit.failure = jest.fn() as any; - return tools + return tools; } -describe('create-an-issue', () => { - let tools: Toolkit - let params: any +describe("create-an-issue", () => { + let tools: Toolkit; + let params: any; beforeEach(() => { - nock('https://api.github.com') - .post(/\/repos\/.*\/.*\/issues/).reply(200, (_, body: any) => { - params = body + nock("https://api.github.com") + .post(/\/repos\/.*\/.*\/issues/) + .reply(200, (_, body: any) => { + params = body; return { title: body.title, number: 1, - html_url: 'www' - } - }) + html_url: "www", + }; + }); - tools = generateToolkit() + tools = generateToolkit(); // Ensure that the filename input isn't set at the start of a test - delete process.env.INPUT_FILENAME + delete process.env.INPUT_FILENAME; // Simulate an environment variable added for the action - process.env.EXAMPLE = 'foo' - }) + process.env.EXAMPLE = "foo"; + }); - it('creates a new issue', async () => { - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() + it("creates a new issue", async () => { + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); // Verify that the outputs were set - expect(core.setOutput).toHaveBeenCalledTimes(2) - expect(core.setOutput).toHaveBeenCalledWith('url', 'www') - expect(core.setOutput).toHaveBeenCalledWith('number', '1') - }) - - it('creates a new issue from a different template', async () => { - process.env.INPUT_FILENAME = '.github/different-template.md' - tools.context.payload = { repository: { owner: { login: 'JasonEtco' }, name: 'waddup' } } - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with some template variables', async () => { - process.env.INPUT_FILENAME = '.github/variables.md' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with the context.repo template variables', async () => { - process.env.INPUT_FILENAME = '.github/context-repo-template.md' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with assignees, labels and a milestone', async () => { - process.env.INPUT_FILENAME = '.github/kitchen-sink.md' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with assignees and labels as comma-delimited strings', async () => { - process.env.INPUT_FILENAME = '.github/split-strings.md' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with an assignee passed by input', async () => { - process.env.INPUT_ASSIGNEES = 'octocat' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with multiple assignees passed by input', async () => { - process.env.INPUT_ASSIGNEES = 'octocat, JasonEtco' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.success).toHaveBeenCalled() - expect((tools.log.success as any).mock.calls).toMatchSnapshot() - }) - - it('creates a new issue with a milestone passed by input', async () => { - process.env.INPUT_MILESTONE = '1' - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(params.milestone).toBe(1) - expect(tools.log.success).toHaveBeenCalled() - }) - - it('creates a new issue when updating existing issues is enabled but no issues with the same title exist', async () => { - nock.cleanAll() - nock('https://api.github.com') - .get(/\/search\/issues.*/).reply(200, { - items: [] + expect(core.setOutput).toHaveBeenCalledTimes(2); + expect(core.setOutput).toHaveBeenCalledWith("url", "www"); + expect(core.setOutput).toHaveBeenCalledWith("number", "1"); + }); + + it("creates a new issue from a different template", async () => { + process.env.INPUT_FILENAME = ".github/different-template.md"; + tools.context.payload = { + repository: { owner: { login: "JasonEtco" }, name: "waddup" }, + }; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with some template variables", async () => { + process.env.INPUT_FILENAME = ".github/variables.md"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with the context.repo template variables", async () => { + process.env.INPUT_FILENAME = ".github/context-repo-template.md"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with assignees, labels and a milestone", async () => { + process.env.INPUT_FILENAME = ".github/kitchen-sink.md"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with assignees and labels as comma-delimited strings", async () => { + process.env.INPUT_FILENAME = ".github/split-strings.md"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with an assignee passed by input", async () => { + process.env.INPUT_ASSIGNEES = "octocat"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with multiple assignees passed by input", async () => { + process.env.INPUT_ASSIGNEES = "octocat, JasonEtco"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.success).toHaveBeenCalled(); + expect((tools.log.success as any).mock.calls).toMatchSnapshot(); + }); + + it("creates a new issue with a milestone passed by input", async () => { + process.env.INPUT_MILESTONE = "1"; + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(params.milestone).toBe(1); + expect(tools.log.success).toHaveBeenCalled(); + }); + + it("creates a new issue when updating existing issues is enabled but no issues with the same title exist", async () => { + nock.cleanAll(); + nock("https://api.github.com") + .get(/\/search\/issues.*/) + .reply(200, { + items: [], }) - .post(/\/repos\/.*\/.*\/issues/).reply(200, (_, body: any) => { - params = body + .post(/\/repos\/.*\/.*\/issues/) + .reply(200, (_, body: any) => { + params = body; return { title: body.title, number: 1, - html_url: 'www' - } - }) - - process.env.INPUT_UPDATE_EXISTING = 'true' - - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.log.info).toHaveBeenCalledWith('No existing issue found to update') - expect(tools.log.success).toHaveBeenCalled() - }) - - it('updates an existing open issue with the same title', async () => { - nock.cleanAll() - nock('https://api.github.com') + html_url: "www", + }; + }); + + process.env.INPUT_UPDATE_EXISTING = "true"; + + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.log.info).toHaveBeenCalledWith( + "No existing issue found to update" + ); + expect(tools.log.success).toHaveBeenCalled(); + }); + + it("updates an existing open issue with the same title", async () => { + nock.cleanAll(); + nock("https://api.github.com") .get(/\/search\/issues.*/) - .query(parsedQuery => { - const q = parsedQuery['q'] - if (typeof(q) === 'string') { - const args = q.split(' ') - return (args.includes('is:open') || args.includes('is:closed')) - && args.includes('is:issue') + .query((parsedQuery) => { + const q = parsedQuery["q"]; + if (typeof q === "string") { + const args = q.split(" "); + return ( + (args.includes("is:open") || args.includes("is:closed")) && + args.includes("is:issue") + ); } else { - return false + return false; } }) .reply(200, { - items: [{ number: 1, title: 'Hello!' }] + items: [{ number: 1, title: "Hello!" }], }) - .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(200, {}) + .patch(/\/repos\/.*\/.*\/issues\/.*/) + .reply(200, {}); - process.env.INPUT_UPDATE_EXISTING = 'true' + process.env.INPUT_UPDATE_EXISTING = "true"; - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.exit.success).toHaveBeenCalled() - }) + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.exit.success).toHaveBeenCalled(); + }); - it('escapes quotes in the search query', async () => { - process.env.INPUT_FILENAME = '.github/quotes-in-title.md' + it("escapes quotes in the search query", async () => { + process.env.INPUT_FILENAME = ".github/quotes-in-title.md"; - nock.cleanAll() - nock('https://api.github.com') + nock.cleanAll(); + nock("https://api.github.com") .get(/\/search\/issues.*/) - .query(parsedQuery => { - const q = parsedQuery['q'] as string - return q.includes('"This title \\\"has quotes\\\""') + .query((parsedQuery) => { + const q = parsedQuery["q"] as string; + return q.includes('"This title \\"has quotes\\""'); }) .reply(200, { - items: [{ number: 1, title: 'Hello!' }] + items: [{ number: 1, title: "Hello!" }], }) - .post(/\/repos\/.*\/.*\/issues/).reply(200, {}) - - await createAnIssue(tools) - expect(tools.log.success).toHaveBeenCalled() - }) - - it('checks the value of update_existing', async () => { - process.env.INPUT_UPDATE_EXISTING = 'invalid' - - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.exit.failure).toHaveBeenCalledWith('Invalid value update_existing=invalid, must be one of true or false') - }) - - it('updates an existing closed issue with the same title', async () => { - nock.cleanAll() - nock('https://api.github.com') + .post(/\/repos\/.*\/.*\/issues/) + .reply(200, {}); + + await createAnIssue(tools); + expect(tools.log.success).toHaveBeenCalled(); + }); + + it("checks the value of update_existing", async () => { + process.env.INPUT_UPDATE_EXISTING = "invalid"; + + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.exit.failure).toHaveBeenCalledWith( + "Invalid value update_existing=invalid, must be one of true or false" + ); + }); + + it("updates an existing closed issue with the same title", async () => { + nock.cleanAll(); + nock("https://api.github.com") .get(/\/search\/issues.*/) - .query(parsedQuery => { - const q = parsedQuery['q'] - if (typeof(q) === 'string') { - const args = q.split(' ') - return !args.includes('is:all') && args.includes('is:issue') + .query((parsedQuery) => { + const q = parsedQuery["q"]; + if (typeof q === "string") { + const args = q.split(" "); + return !args.includes("is:all") && args.includes("is:issue"); } else { - return false + return false; } }) .reply(200, { - items: [{ number: 1, title: 'Hello!', html_url: '/issues/1' }] + items: [{ number: 1, title: "Hello!", html_url: "/issues/1" }], }) - .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(200, {}) - - process.env.INPUT_UPDATE_EXISTING = 'true' - process.env.INPUT_SEARCH_EXISTING = 'all' - - await createAnIssue(tools) - expect(tools.exit.success).toHaveBeenCalledWith('Updated issue Hello!#1: /issues/1') - }) + .patch(/\/repos\/.*\/.*\/issues\/.*/) + .reply(200, {}); - it('finds, but does not update an existing issue with the same title', async () => { - nock.cleanAll() - nock('https://api.github.com') - .get(/\/search\/issues.*/).reply(200, { - items: [{ number: 1, title: 'Hello!', html_url: '/issues/1' }] - }) - process.env.INPUT_UPDATE_EXISTING = 'false' - - await createAnIssue(tools) - expect(params).toMatchSnapshot() - expect(tools.exit.success).toHaveBeenCalledWith('Existing issue Hello!#1: /issues/1 found but not updated') - }) - - it('exits when updating an issue fails', async () => { - nock.cleanAll() - nock('https://api.github.com') - .get(/\/search\/issues.*/).reply(200, { - items: [{ number: 1, title: 'Hello!', html_url: '/issues/1' }] - }) - .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(500, { - message: 'Updating issue failed' - }) + process.env.INPUT_UPDATE_EXISTING = "true"; + process.env.INPUT_SEARCH_EXISTING = "all"; - await createAnIssue(tools) - expect(tools.exit.failure).toHaveBeenCalled() - }) + await createAnIssue(tools); + expect(tools.exit.success).toHaveBeenCalledWith( + "Updated issue Hello!#1: /issues/1" + ); + }); - it('logs a helpful error if creating an issue throws an error', async () => { - nock.cleanAll() - nock('https://api.github.com') - .get(/\/search\/issues.*/).reply(200, { items:[] }) - .post(/\/repos\/.*\/.*\/issues/).reply(500, { - message: 'Validation error' - }) - - await createAnIssue(tools) - expect(tools.log.error).toHaveBeenCalled() - expect((tools.log.error as any).mock.calls).toMatchSnapshot() - expect(tools.exit.failure).toHaveBeenCalled() - }) - - it('logs a helpful error if creating an issue throws an error with more errors', async () => { - nock.cleanAll() - nock('https://api.github.com') - .get(/\/search\/issues.*/).reply(200, { items:[] }) - .post(/\/repos\/.*\/.*\/issues/).reply(500, { - message: 'Validation error', - errors: [{ foo: true }] - }) - - await createAnIssue(tools) - expect(tools.log.error).toHaveBeenCalled() - expect((tools.log.error as any).mock.calls).toMatchSnapshot() - expect(tools.exit.failure).toHaveBeenCalled() - }) - - it('logs a helpful error if updating an issue throws an error with more errors', async () => { - nock.cleanAll() - nock('https://api.github.com') + it("finds, but does not update an existing issue with the same title", async () => { + nock.cleanAll(); + nock("https://api.github.com") + .get(/\/search\/issues.*/) + .reply(200, { + items: [{ number: 1, title: "Hello!", html_url: "/issues/1" }], + }); + process.env.INPUT_UPDATE_EXISTING = "false"; + + await createAnIssue(tools); + expect(params).toMatchSnapshot(); + expect(tools.exit.success).toHaveBeenCalledWith( + "Existing issue Hello!#1: /issues/1 found but not updated" + ); + }); + + it("exits when updating an issue fails", async () => { + nock.cleanAll(); + nock("https://api.github.com") .get(/\/search\/issues.*/) - .reply(200, { items: [{ number: 1, title: 'Hello!' }] }) - .patch(/\/repos\/.*\/.*\/issues\/.*/).reply(500, { - message: 'Validation error', - errors: [{ foo: true }] + .reply(200, { + items: [{ number: 1, title: "Hello!", html_url: "/issues/1" }], }) - - process.env.INPUT_UPDATE_EXISTING = 'true' - - await createAnIssue(tools) - expect(tools.log.error).toHaveBeenCalled() - expect((tools.log.error as any).mock.calls).toMatchSnapshot() - expect(tools.exit.failure).toHaveBeenCalled() - }) -}) + .patch(/\/repos\/.*\/.*\/issues\/.*/) + .reply(500, { + message: "Updating issue failed", + }); + + await createAnIssue(tools); + expect(tools.exit.failure).toHaveBeenCalled(); + }); + + it("logs a helpful error if creating an issue throws an error", async () => { + nock.cleanAll(); + nock("https://api.github.com") + .get(/\/search\/issues.*/) + .reply(200, { items: [] }) + .post(/\/repos\/.*\/.*\/issues/) + .reply(500, { + message: "Validation error", + }); + + await createAnIssue(tools); + expect(tools.log.error).toHaveBeenCalled(); + expect((tools.log.error as any).mock.calls).toMatchSnapshot(); + expect(tools.exit.failure).toHaveBeenCalled(); + }); + + it("logs a helpful error if creating an issue throws an error with more errors", async () => { + nock.cleanAll(); + nock("https://api.github.com") + .get(/\/search\/issues.*/) + .reply(200, { items: [] }) + .post(/\/repos\/.*\/.*\/issues/) + .reply(500, { + message: "Validation error", + errors: [{ foo: true }], + }); + + await createAnIssue(tools); + expect(tools.log.error).toHaveBeenCalled(); + expect((tools.log.error as any).mock.calls).toMatchSnapshot(); + expect(tools.exit.failure).toHaveBeenCalled(); + }); + + it("logs a helpful error if updating an issue throws an error with more errors", async () => { + nock.cleanAll(); + nock("https://api.github.com") + .get(/\/search\/issues.*/) + .reply(200, { items: [{ number: 1, title: "Hello!" }] }) + .patch(/\/repos\/.*\/.*\/issues\/.*/) + .reply(500, { + message: "Validation error", + errors: [{ foo: true }], + }); + + process.env.INPUT_UPDATE_EXISTING = "true"; + + await createAnIssue(tools); + expect(tools.log.error).toHaveBeenCalled(); + expect((tools.log.error as any).mock.calls).toMatchSnapshot(); + expect(tools.exit.failure).toHaveBeenCalled(); + }); + + it("logs a helpful error if the frontmatter is invalid", async () => { + process.env.INPUT_FILENAME = ".github/invalid-frontmatter.md"; + + await createAnIssue(tools); + expect(tools.log.error).toHaveBeenCalled(); + expect((tools.log.error as any).mock.calls).toMatchSnapshot(); + expect(tools.exit.failure).toHaveBeenCalled(); + }); +}); diff --git a/tests/setup.ts b/tests/setup.ts index 88e73ba..ec76ca9 100644 --- a/tests/setup.ts +++ b/tests/setup.ts @@ -1,8 +1,8 @@ -const path = require('path') +const path = require("path"); Object.assign(process.env, { - GITHUB_REPOSITORY: 'JasonEtco/waddup', - GITHUB_ACTION: 'create-an-issue', - GITHUB_EVENT_PATH: path.join(__dirname, 'fixtures', 'event.json'), - GITHUB_WORKSPACE: path.join(__dirname, 'fixtures') -}) + GITHUB_REPOSITORY: "JasonEtco/waddup", + GITHUB_ACTION: "create-an-issue", + GITHUB_EVENT_PATH: path.join(__dirname, "fixtures", "event.json"), + GITHUB_WORKSPACE: path.join(__dirname, "fixtures"), +});