Skip to content

Commit 8d42141

Browse files
authored
Merge pull request #144 from JasonEtco/zod
Add Zod to validate front-matter
2 parents d78f56c + 5218616 commit 8d42141

12 files changed

+465
-354
lines changed

Dockerfile

-16
This file was deleted.

codecov.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,4 @@ coverage:
88
default:
99
threshold: 3
1010

11-
comment: false
11+
comment: false

package-lock.json

+10-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@
1515
"front-matter": "^4.0.2",
1616
"js-yaml": "^4.1.0",
1717
"nunjucks": "^3.2.3",
18-
"nunjucks-date-filter": "^0.1.1"
18+
"nunjucks-date-filter": "^0.1.1",
19+
"zod": "^3.20.2"
1920
},
2021
"devDependencies": {
2122
"@tsconfig/node12": "^1.0.11",

src/action.ts

+100-58
Original file line numberDiff line numberDiff line change
@@ -1,113 +1,155 @@
1-
import * as core from '@actions/core'
2-
import { Toolkit } from 'actions-toolkit'
3-
import fm from 'front-matter'
4-
import nunjucks from 'nunjucks'
5-
// @ts-ignore
6-
import dateFilter from 'nunjucks-date-filter'
7-
import { FrontMatterAttributes, listToArray, setOutputs } from './helpers'
8-
9-
function logError(tools: Toolkit, template: string, action: 'creating' | 'updating', err: any) {
1+
import * as core from "@actions/core";
2+
import { Toolkit } from "actions-toolkit";
3+
import fm from "front-matter";
4+
import nunjucks from "nunjucks";
5+
// @ts-expect-error
6+
import dateFilter from "nunjucks-date-filter";
7+
import { ZodError } from "zod";
8+
import {
9+
FrontMatterAttributes,
10+
frontmatterSchema,
11+
listToArray,
12+
setOutputs,
13+
} from "./helpers";
14+
15+
function logError(
16+
tools: Toolkit,
17+
template: string,
18+
action: "creating" | "updating" | "parsing",
19+
err: any
20+
) {
1021
// Log the error message
11-
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}!`
12-
tools.log.error(errorMessage)
13-
tools.log.error(err)
22+
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}!`;
23+
tools.log.error(errorMessage);
24+
tools.log.error(err);
1425

1526
// The error might have more details
16-
if (err.errors) tools.log.error(err.errors)
27+
if (err.errors) tools.log.error(err.errors);
1728

1829
// Exit with a failing status
19-
core.setFailed(errorMessage + '\n\n' + err.message)
20-
return tools.exit.failure()
30+
core.setFailed(errorMessage + "\n\n" + err.message);
31+
return tools.exit.failure();
2132
}
2233

23-
export async function createAnIssue (tools: Toolkit) {
24-
const template = tools.inputs.filename || '.github/ISSUE_TEMPLATE.md'
25-
const assignees = tools.inputs.assignees
34+
export async function createAnIssue(tools: Toolkit) {
35+
const template = tools.inputs.filename || ".github/ISSUE_TEMPLATE.md";
36+
const assignees = tools.inputs.assignees;
2637

27-
let updateExisting: Boolean | null = null
38+
let updateExisting: Boolean | null = null;
2839
if (tools.inputs.update_existing) {
29-
if (tools.inputs.update_existing === 'true') {
30-
updateExisting = true
31-
} else if (tools.inputs.update_existing === 'false') {
32-
updateExisting = false
40+
if (tools.inputs.update_existing === "true") {
41+
updateExisting = true;
42+
} else if (tools.inputs.update_existing === "false") {
43+
updateExisting = false;
3344
} else {
34-
tools.exit.failure(`Invalid value update_existing=${tools.inputs.update_existing}, must be one of true or false`)
45+
tools.exit.failure(
46+
`Invalid value update_existing=${tools.inputs.update_existing}, must be one of true or false`
47+
);
3548
}
3649
}
3750

38-
const env = nunjucks.configure({ autoescape: false })
39-
env.addFilter('date', dateFilter)
51+
const env = nunjucks.configure({ autoescape: false });
52+
env.addFilter("date", dateFilter);
4053

4154
const templateVariables = {
4255
...tools.context,
4356
repo: tools.context.repo,
4457
env: process.env,
45-
date: Date.now()
46-
}
58+
date: Date.now(),
59+
};
4760

4861
// Get the file
49-
tools.log.debug('Reading from file', template)
50-
const file = await tools.readFile(template) as string
62+
tools.log.debug("Reading from file", template);
63+
const file = (await tools.readFile(template)) as string;
5164

5265
// Grab the front matter as JSON
53-
const { attributes, body } = fm<FrontMatterAttributes>(file)
54-
tools.log(`Front matter for ${template} is`, attributes)
66+
const { attributes: rawAttributes, body } = fm<FrontMatterAttributes>(file);
67+
68+
let attributes: FrontMatterAttributes;
69+
try {
70+
attributes = await frontmatterSchema.parseAsync(rawAttributes);
71+
} catch (err) {
72+
if (err instanceof ZodError) {
73+
const formatted = err.format();
74+
return logError(tools, template, "parsing", formatted);
75+
}
76+
throw err;
77+
}
78+
79+
tools.log(`Front matter for ${template} is`, attributes);
5580

5681
const templated = {
5782
body: env.renderString(body, templateVariables),
58-
title: env.renderString(attributes.title, templateVariables)
59-
}
60-
tools.log.debug('Templates compiled', templated)
83+
title: env.renderString(attributes.title, templateVariables),
84+
};
85+
tools.log.debug("Templates compiled", templated);
6186

6287
if (updateExisting !== null) {
63-
tools.log.info(`Fetching issues with title "${templated.title}"`)
88+
tools.log.info(`Fetching issues with title "${templated.title}"`);
6489

65-
let query = `is:issue repo:${process.env.GITHUB_REPOSITORY} in:title "${templated.title.replace(/['"]/g, "\\$&")}"`
90+
let query = `is:issue repo:${
91+
process.env.GITHUB_REPOSITORY
92+
} in:title "${templated.title.replace(/['"]/g, "\\$&")}"`;
6693

67-
const searchExistingType = tools.inputs.search_existing || 'open'
68-
const allowedStates = ['open', 'closed']
94+
const searchExistingType = tools.inputs.search_existing || "open";
95+
const allowedStates = ["open", "closed"];
6996
if (allowedStates.includes(searchExistingType)) {
70-
query += ` is:${searchExistingType}`
97+
query += ` is:${searchExistingType}`;
7198
}
7299

73-
const existingIssues = await tools.github.search.issuesAndPullRequests({ q: query })
74-
const existingIssue = existingIssues.data.items.find(issue => issue.title === templated.title)
100+
const existingIssues = await tools.github.search.issuesAndPullRequests({
101+
q: query,
102+
});
103+
const existingIssue = existingIssues.data.items.find(
104+
(issue) => issue.title === templated.title
105+
);
75106
if (existingIssue) {
76107
if (updateExisting === false) {
77-
tools.exit.success(`Existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url} found but not updated`)
108+
tools.exit.success(
109+
`Existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url} found but not updated`
110+
);
78111
} else {
79112
try {
80-
tools.log.info(`Updating existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`)
113+
tools.log.info(
114+
`Updating existing issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`
115+
);
81116
const issue = await tools.github.issues.update({
82117
...tools.context.repo,
83118
issue_number: existingIssue.number,
84-
body: templated.body
85-
})
86-
setOutputs(tools, issue.data)
87-
tools.exit.success(`Updated issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`)
119+
body: templated.body,
120+
});
121+
setOutputs(tools, issue.data);
122+
tools.exit.success(
123+
`Updated issue ${existingIssue.title}#${existingIssue.number}: ${existingIssue.html_url}`
124+
);
88125
} catch (err: any) {
89-
return logError(tools, template, 'updating', err)
126+
return logError(tools, template, "updating", err);
90127
}
91128
}
92129
} else {
93-
tools.log.info('No existing issue found to update')
130+
tools.log.info("No existing issue found to update");
94131
}
95132
}
96133

97134
// Create the new issue
98-
tools.log.info(`Creating new issue ${templated.title}`)
135+
tools.log.info(`Creating new issue ${templated.title}`);
99136
try {
100137
const issue = await tools.github.issues.create({
101138
...tools.context.repo,
102139
...templated,
103-
assignees: assignees ? listToArray(assignees) : listToArray(attributes.assignees),
140+
assignees: assignees
141+
? listToArray(assignees)
142+
: listToArray(attributes.assignees),
104143
labels: listToArray(attributes.labels),
105-
milestone: Number(tools.inputs.milestone || attributes.milestone) || undefined
106-
})
107-
108-
setOutputs(tools, issue.data)
109-
tools.log.success(`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`)
144+
milestone:
145+
Number(tools.inputs.milestone || attributes.milestone) || undefined,
146+
});
147+
148+
setOutputs(tools, issue.data);
149+
tools.log.success(
150+
`Created issue ${issue.data.title}#${issue.data.number}: ${issue.data.html_url}`
151+
);
110152
} catch (err: any) {
111-
return logError(tools, template, 'creating', err)
153+
return logError(tools, template, "creating", err);
112154
}
113155
}

src/helpers.ts

+22-14
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,26 @@
1-
import { Toolkit } from 'actions-toolkit'
1+
import { Toolkit } from "actions-toolkit";
2+
import { z } from "zod";
23

3-
export interface FrontMatterAttributes {
4-
title: string
5-
assignees?: string[] | string
6-
labels?: string[] | string
7-
milestone?: string | number
8-
}
4+
export const frontmatterSchema = z
5+
.object({
6+
title: z.string(),
7+
assignees: z.union([z.array(z.string()), z.string()]).optional(),
8+
labels: z.union([z.array(z.string()), z.string()]).optional(),
9+
milestone: z.union([z.string(), z.number()]).optional(),
10+
})
11+
.strict();
12+
13+
export type FrontMatterAttributes = z.infer<typeof frontmatterSchema>;
914

10-
export function setOutputs (tools: Toolkit, issue: { number: number, html_url: string }) {
11-
tools.outputs.number = String(issue.number)
12-
tools.outputs.url = issue.html_url
15+
export function setOutputs(
16+
tools: Toolkit,
17+
issue: { number: number; html_url: string }
18+
) {
19+
tools.outputs.number = String(issue.number);
20+
tools.outputs.url = issue.html_url;
1321
}
1422

15-
export function listToArray (list?: string[] | string) {
16-
if (!list) return []
17-
return Array.isArray(list) ? list : list.split(', ')
18-
}
23+
export function listToArray(list?: string[] | string) {
24+
if (!list) return [];
25+
return Array.isArray(list) ? list : list.split(", ");
26+
}

src/index.ts

+4-4
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import { Toolkit } from 'actions-toolkit'
2-
import { createAnIssue } from './action'
1+
import { Toolkit } from "actions-toolkit";
2+
import { createAnIssue } from "./action";
33

44
Toolkit.run(createAnIssue, {
5-
secrets: ['GITHUB_TOKEN']
6-
})
5+
secrets: ["GITHUB_TOKEN"],
6+
});

tests/__snapshots__/index.test.ts.snap

+26
Original file line numberDiff line numberDiff line change
@@ -232,6 +232,32 @@ exports[`create-an-issue logs a helpful error if creating an issue throws an err
232232
]
233233
`;
234234

235+
exports[`create-an-issue logs a helpful error if the frontmatter is invalid 1`] = `
236+
[
237+
[
238+
"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!",
239+
],
240+
[
241+
{
242+
"_errors": [
243+
"Unrecognized key(s) in object: 'name', 'not_a_thing'",
244+
],
245+
"labels": {
246+
"_errors": [
247+
"Expected array, received number",
248+
"Expected string, received number",
249+
],
250+
},
251+
"title": {
252+
"_errors": [
253+
"Required",
254+
],
255+
},
256+
},
257+
],
258+
]
259+
`;
260+
235261
exports[`create-an-issue logs a helpful error if updating an issue throws an error with more errors 1`] = `
236262
[
237263
[
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
name: "Not a title"
3+
labels: 123
4+
not_a_thing: "testing"
5+
---
6+
Hi!

tests/fixtures/event.json

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
{
22
"repository": {
3-
"owner": { "login": "JasonEtco" },
3+
"owner": {
4+
"login": "JasonEtco"
5+
},
46
"name": "waddup"
57
}
6-
}
8+
}

0 commit comments

Comments
 (0)