Skip to content

Commit

Permalink
Merge pull request #144 from JasonEtco/zod
Browse files Browse the repository at this point in the history
Add Zod to validate front-matter
  • Loading branch information
JasonEtco committed Dec 21, 2022
2 parents d78f56c + 5218616 commit 8d42141
Show file tree
Hide file tree
Showing 12 changed files with 465 additions and 354 deletions.
16 changes: 0 additions & 16 deletions Dockerfile

This file was deleted.

2 changes: 1 addition & 1 deletion codecov.yml
Expand Up @@ -8,4 +8,4 @@ coverage:
default:
threshold: 3

comment: false
comment: false
11 changes: 10 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Expand Up @@ -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",
Expand Down
158 changes: 100 additions & 58 deletions 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<FrontMatterAttributes>(file)
tools.log(`Front matter for ${template} is`, attributes)
const { attributes: rawAttributes, body } = fm<FrontMatterAttributes>(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);
}
}
36 changes: 22 additions & 14 deletions 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<typeof frontmatterSchema>;

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(', ')
}
export function listToArray(list?: string[] | string) {
if (!list) return [];
return Array.isArray(list) ? list : list.split(", ");
}
8 changes: 4 additions & 4 deletions 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"],
});
26 changes: 26 additions & 0 deletions tests/__snapshots__/index.test.ts.snap
Expand Up @@ -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`] = `
[
[
Expand Down
6 changes: 6 additions & 0 deletions tests/fixtures/.github/invalid-frontmatter.md
@@ -0,0 +1,6 @@
---
name: "Not a title"
labels: 123
not_a_thing: "testing"
---
Hi!
6 changes: 4 additions & 2 deletions tests/fixtures/event.json
@@ -1,6 +1,8 @@
{
"repository": {
"owner": { "login": "JasonEtco" },
"owner": {
"login": "JasonEtco"
},
"name": "waddup"
}
}
}

0 comments on commit 8d42141

Please sign in to comment.