From 56b3251a619bbdda74ea253ed0e4d3c69a7d4da8 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 12:54:11 -0400 Subject: [PATCH 01/18] Add script to validate formatting --- scripts/.gitignore | 34 ++++++++++++++++++++++++ scripts/bun.lock | 58 +++++++++++++++++++++++++++++++++++++++++ scripts/package.json | 15 +++++++++++ scripts/src/index.ts | 36 +++++++++++++++++++++++++ scripts/src/schema.ts | 12 +++++++++ scripts/src/validate.ts | 37 ++++++++++++++++++++++++++ scripts/tsconfig.json | 28 ++++++++++++++++++++ 7 files changed, 220 insertions(+) create mode 100644 scripts/.gitignore create mode 100644 scripts/bun.lock create mode 100644 scripts/package.json create mode 100644 scripts/src/index.ts create mode 100644 scripts/src/schema.ts create mode 100644 scripts/src/validate.ts create mode 100644 scripts/tsconfig.json diff --git a/scripts/.gitignore b/scripts/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/scripts/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/scripts/bun.lock b/scripts/bun.lock new file mode 100644 index 0000000..4e843d2 --- /dev/null +++ b/scripts/bun.lock @@ -0,0 +1,58 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "scripts", + "dependencies": { + "gray-matter": "^4.0.3", + "marked": "^16.2.0", + "zod": "^4.1.3", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/react": ["@types/react@19.1.11", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-lr3jdBw/BGj49Eps7EvqlUaoeA0xpj3pc0RoJkHpYaCHkVK7i28dKyImLQb3JVlqs3aYSXf7qYuWOW/fgZnTXQ=="], + + "argparse": ["argparse@1.0.10", "", { "dependencies": { "sprintf-js": "~1.0.2" } }, "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg=="], + + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "extend-shallow": ["extend-shallow@2.0.1", "", { "dependencies": { "is-extendable": "^0.1.0" } }, "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug=="], + + "gray-matter": ["gray-matter@4.0.3", "", { "dependencies": { "js-yaml": "^3.13.1", "kind-of": "^6.0.2", "section-matter": "^1.0.0", "strip-bom-string": "^1.0.0" } }, "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q=="], + + "is-extendable": ["is-extendable@0.1.1", "", {}, "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw=="], + + "js-yaml": ["js-yaml@3.14.1", "", { "dependencies": { "argparse": "^1.0.7", "esprima": "^4.0.0" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g=="], + + "kind-of": ["kind-of@6.0.3", "", {}, "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw=="], + + "marked": ["marked@16.2.0", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-LbbTuye+0dWRz2TS9KJ7wsnD4KAtpj0MVkWc90XvBa6AslXsT0hTBVH5k32pcSyHH1fst9XEFJunXHktVy0zlg=="], + + "section-matter": ["section-matter@1.0.0", "", { "dependencies": { "extend-shallow": "^2.0.1", "kind-of": "^6.0.0" } }, "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA=="], + + "sprintf-js": ["sprintf-js@1.0.3", "", {}, "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="], + + "strip-bom-string": ["strip-bom-string@1.0.0", "", {}, "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "zod": ["zod@4.1.3", "", {}, "sha512-1neef4bMce1hNTrxvHVKxWjKfGDn0oAli3Wy1Uwb7TRO1+wEwoZUZNP1NXIEESybOBiFnBOhI6a4m6tCLE8dog=="], + } +} diff --git a/scripts/package.json b/scripts/package.json new file mode 100644 index 0000000..7fd4772 --- /dev/null +++ b/scripts/package.json @@ -0,0 +1,15 @@ +{ + "private": true, + "type": "module", + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "gray-matter": "^4.0.3", + "marked": "^16.2.0", + "zod": "^4.1.3" + } +} diff --git a/scripts/src/index.ts b/scripts/src/index.ts new file mode 100644 index 0000000..7064e01 --- /dev/null +++ b/scripts/src/index.ts @@ -0,0 +1,36 @@ +import { readdir } from 'node:fs/promises' +import matter from 'gray-matter' +import path from 'node:path' + +import { ProposalFrontmatter } from './schema' +import { validateProposalHeadings } from './validate' + +const rootPath = path.join(import.meta.dirname, '..', '..') +const root = await readdir(rootPath) +const sppSeasons = root.filter((entry) => entry.startsWith('spp-')) + +for (const sppSeason of sppSeasons) { + const seasonPath = path.join(rootPath, sppSeason) + const providers = await readdir(seasonPath) + + for (const provider of providers) { + const providerPath = path.join(seasonPath, provider) + + // Read the proposal.md file + const proposal = await Bun.file(`${providerPath}/proposal.md`).text() + const { data: proposalFrontmatter, content: proposalContent } = + matter(proposal) + + // Validate the frontmatter + const validatedFrontmatter = ProposalFrontmatter.parse(proposalFrontmatter) + + // Check if the logo (as a relative path) exists + const logoPath = path.join(providerPath, validatedFrontmatter.logo) + const logoExists = await Bun.file(logoPath).exists() + if (!logoExists) { + throw new Error(`File does not exist: ${logoPath}`) + } + + validateProposalHeadings(proposalContent) + } +} diff --git a/scripts/src/schema.ts b/scripts/src/schema.ts new file mode 100644 index 0000000..5d47cee --- /dev/null +++ b/scripts/src/schema.ts @@ -0,0 +1,12 @@ +import { z } from 'zod' + +export const ProposalFrontmatter = z.object({ + name: z.string('Name is required'), + description: z.string('Description is required'), + logo: z + .string('Logo is required as a relative path') + .refine((str) => str.startsWith('./')), + website: z.url('Website is required'), + twitter: z.url('Twitter (full URL, not just username) is required'), + github: z.url('GitHub (full URL, not just username) is required'), +}) diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts new file mode 100644 index 0000000..7b0b2f2 --- /dev/null +++ b/scripts/src/validate.ts @@ -0,0 +1,37 @@ +import { marked, type Tokens } from 'marked' + +const requiredProposalH2s = [ + 'Applicant Information', + 'Eligibility Confirmation', + 'Open Source Commitment', + 'Scope of Work & Budget', + 'Past Achievements & Additional Information', + 'Video Introduction', + 'Conflict Of Interest Statment', +] + +export function validateProposalHeadings(content: string) { + const headingTokens = marked.lexer(content) + const headings = headingTokens.filter( + (token) => token.type === 'heading' + ) as Tokens.Heading[] + + const h1s = headings.filter((token) => token.depth === 1) + const h2s = headings.filter((token) => token.depth === 2) + + if (h1s.length !== 1) { + throw new Error('Only one H1 is allowed') + } + + const missingH2s = requiredProposalH2s.filter( + (h2) => !h2s.some((h) => h.text.includes(h2)) + ) + + if (missingH2s.length > 0) { + throw new Error( + `Missing required headings: ${missingH2s.join( + ', ' + )}. Please add them to the proposal.md file.` + ) + } +} diff --git a/scripts/tsconfig.json b/scripts/tsconfig.json new file mode 100644 index 0000000..9c62f74 --- /dev/null +++ b/scripts/tsconfig.json @@ -0,0 +1,28 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "ESNext", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +} From d606bcf72f4ecc4b2494377e9b0543d5638fbd78 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:12:05 -0400 Subject: [PATCH 02/18] Add GitHub workflow --- .github/workflows/validate.yml | 30 ++++++++++++++++++++++++++++++ scripts/package.json | 3 +++ 2 files changed, 33 insertions(+) create mode 100644 .github/workflows/validate.yml diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..407b0f3 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,30 @@ +name: Validate SPP Data + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + validate: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: | + cd scripts + bun install + + - name: Run validation + run: | + cd scripts + bun run validate diff --git a/scripts/package.json b/scripts/package.json index 7fd4772..f4ee09a 100644 --- a/scripts/package.json +++ b/scripts/package.json @@ -1,6 +1,9 @@ { "private": true, "type": "module", + "scripts": { + "validate": "bun run src/index.ts" + }, "devDependencies": { "@types/bun": "latest" }, From 1c19cdb162b316629698843b140f0c301f6e595e Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:13:26 -0400 Subject: [PATCH 03/18] Always run --- .github/workflows/validate.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index 407b0f3..c4d683e 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,9 +2,7 @@ name: Validate SPP Data on: push: - branches: ['main'] pull_request: - branches: ['main'] jobs: validate: From 3600d39966ad812bb0445b8756adb71eb60e18f7 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:35:03 -0400 Subject: [PATCH 04/18] Update readme --- README.md | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index c0aabd2..6570ce3 100644 --- a/README.md +++ b/README.md @@ -4,15 +4,14 @@ The goal of this repo is to provide all relevant data for ENS DAO Service Provid ## Structure -- Each SPP season has its own folder in the root of the repo, e.g. `spp-2` +- Each SPP season has its own folder in the root of the repo, e.g. `spp-2`. - Each provider has its own folder within the SPP season with the following structure: - - `proposal.md` - the initial proposal for the season that got approved by the ENS DAO, including a bunch of stuff in the frontmatter, e.g. `name`, `description`, `logo`, `website`, `twitter`, `github` - - `assets/` - any relevant files, like images, for the proposal/updates, e.g. `logo.svg` - - `updates/` - the quarterly updates for the provider - - `1.md` - the first update for the provider, e.g. `Quarterly Report Q1 2025` - - `2.md` - the second update for the provider, e.g. `Quarterly Report Q2 2025`, etc. + - `proposal.md` - the initial proposal for the season that got approved by the ENS DAO. This file must include basic information about the provider in the frontmatter including `name`, `description`, `logo`, `website`, `twitter`, `github`. + - `assets/` - any relevant files, like images, for the proposal/updates, e.g. `logo.svg`. + - `updates/` - the quarterly updates for the provider. + - `1.md` - the first update for the provider in the given season. - ... ## Guidelines -ENS DAO MetaGov stewards are admins of this repo, and responsible for merging PRs from service providers. They should only merge PRs once the formatting is correct. +ENS DAO MetaGov stewards are admins of this repo, and responsible for merging PRs from service providers. They should only merge PRs once the formatting is correct, which is partially validated by the CI. From 1ac9851194f9d2f4f9371391ba0bf6ad54faccae Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 14:53:42 -0400 Subject: [PATCH 05/18] Validate updates --- scripts/src/index.ts | 37 ++++++++++++++++++++++++++++++++----- scripts/src/validate.ts | 38 +++++++++++++++++++++++++++++++------- 2 files changed, 63 insertions(+), 12 deletions(-) diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 7064e01..d2a4e6e 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -3,7 +3,7 @@ import matter from 'gray-matter' import path from 'node:path' import { ProposalFrontmatter } from './schema' -import { validateProposalHeadings } from './validate' +import { validateProposalHeadings, validateUpdateHeadings } from './validate' const rootPath = path.join(import.meta.dirname, '..', '..') const root = await readdir(rootPath) @@ -17,9 +17,9 @@ for (const sppSeason of sppSeasons) { const providerPath = path.join(seasonPath, provider) // Read the proposal.md file - const proposal = await Bun.file(`${providerPath}/proposal.md`).text() - const { data: proposalFrontmatter, content: proposalContent } = - matter(proposal) + const { data: proposalFrontmatter, content: proposalMarkdown } = matter( + await Bun.file(`${providerPath}/proposal.md`).text() + ) // Validate the frontmatter const validatedFrontmatter = ProposalFrontmatter.parse(proposalFrontmatter) @@ -31,6 +31,33 @@ for (const sppSeason of sppSeasons) { throw new Error(`File does not exist: ${logoPath}`) } - validateProposalHeadings(proposalContent) + validateProposalHeadings(proposalMarkdown) + + const updates = await readdir(path.join(providerPath, 'updates')) + const updateFiles = updates.filter((file) => file.endsWith('.md')) + + // Make sure there's nothing else besides {number}.md files + if (updateFiles.length !== updates.length) { + throw new Error( + `There should only be numbered .md files in the updates folder` + ) + } + + // Check that updates are numbered sequentially + for (let i = 0; i < updateFiles.length; i++) { + const update = updateFiles[i]! + const updateNumber = parseInt(update.split('.')[0]!) + if (updateNumber !== i + 1) { + throw new Error('Updates are not numbered sequentially') + } + } + + for (const updateFile of updateFiles) { + const updatePath = path.join(providerPath, 'updates', updateFile) + const updateContent = await Bun.file(updatePath).text() + const { content: updateMarkdown } = matter(updateContent) + + validateUpdateHeadings(updateMarkdown) + } } } diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index 7b0b2f2..01b7f0f 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -11,13 +11,7 @@ const requiredProposalH2s = [ ] export function validateProposalHeadings(content: string) { - const headingTokens = marked.lexer(content) - const headings = headingTokens.filter( - (token) => token.type === 'heading' - ) as Tokens.Heading[] - - const h1s = headings.filter((token) => token.depth === 1) - const h2s = headings.filter((token) => token.depth === 2) + const { h1s, h2s } = extractHeadings(content) if (h1s.length !== 1) { throw new Error('Only one H1 is allowed') @@ -35,3 +29,33 @@ export function validateProposalHeadings(content: string) { ) } } + +const requiredUpdateH2s = ['Summary', 'KPIs'] + +export function validateUpdateHeadings(content: string) { + const { h1s, h2s } = extractHeadings(content) + + if (h1s.length !== 1) { + throw new Error('Only one H1 is allowed') + } + + const missingH2s = requiredUpdateH2s.filter( + (h2) => !h2s.some((h) => h.text.includes(h2)) + ) + + if (missingH2s.length > 0) { + throw new Error(`Missing required headings: ${missingH2s.join(', ')}`) + } +} + +function extractHeadings(content: string) { + const tokens = marked.lexer(content) + const headingTokens = tokens.filter( + (token) => token.type === 'heading' + ) as Tokens.Heading[] + + const h1s = headingTokens.filter((token) => token.depth === 1) + const h2s = headingTokens.filter((token) => token.depth === 2) + + return { h1s, h2s } +} From 9a6b594385b7439ecd00458cdeb6c640b21d37df Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:09:45 -0400 Subject: [PATCH 06/18] Improve error handling in CI --- scripts/src/error.ts | 14 +++++++ scripts/src/index.ts | 93 ++++++++++++++++++++++++----------------- scripts/src/validate.ts | 21 ++++++++-- 3 files changed, 86 insertions(+), 42 deletions(-) create mode 100644 scripts/src/error.ts diff --git a/scripts/src/error.ts b/scripts/src/error.ts new file mode 100644 index 0000000..b2576b0 --- /dev/null +++ b/scripts/src/error.ts @@ -0,0 +1,14 @@ +type GitHubErrorOptions = { + filename: string + line: number + title: string + message: string +} + +export class GitHubError extends Error { + constructor({ filename, line, title, message }: GitHubErrorOptions) { + // GitHub will display inline errors if we log the error in the following format + const errorMessage = `::error file=${filename},line=${line},title=${title}::${message}` + super(errorMessage) + } +} diff --git a/scripts/src/index.ts b/scripts/src/index.ts index d2a4e6e..55478e5 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -4,60 +4,77 @@ import path from 'node:path' import { ProposalFrontmatter } from './schema' import { validateProposalHeadings, validateUpdateHeadings } from './validate' +import { GitHubError } from './error' const rootPath = path.join(import.meta.dirname, '..', '..') const root = await readdir(rootPath) const sppSeasons = root.filter((entry) => entry.startsWith('spp-')) -for (const sppSeason of sppSeasons) { - const seasonPath = path.join(rootPath, sppSeason) - const providers = await readdir(seasonPath) +try { + await validate() +} catch (error) { + if (error instanceof GitHubError) { + // Log the error instead of throwing it so it displays nicely in GitHub Workflows + console.log(error.message) + process.exit(1) + } - for (const provider of providers) { - const providerPath = path.join(seasonPath, provider) + throw error +} - // Read the proposal.md file - const { data: proposalFrontmatter, content: proposalMarkdown } = matter( - await Bun.file(`${providerPath}/proposal.md`).text() - ) +async function validate() { + for (const sppSeason of sppSeasons) { + const seasonPath = path.join(rootPath, sppSeason) + const providers = await readdir(seasonPath) - // Validate the frontmatter - const validatedFrontmatter = ProposalFrontmatter.parse(proposalFrontmatter) + for (const provider of providers) { + const providerPath = path.join(seasonPath, provider) - // Check if the logo (as a relative path) exists - const logoPath = path.join(providerPath, validatedFrontmatter.logo) - const logoExists = await Bun.file(logoPath).exists() - if (!logoExists) { - throw new Error(`File does not exist: ${logoPath}`) - } + // Read the proposal.md file + const proposalPath = path.join(providerPath, 'proposal.md') + const { data: proposalFrontmatter, content: proposalMarkdown } = matter( + await Bun.file(proposalPath).text() + ) - validateProposalHeadings(proposalMarkdown) + // Validate the frontmatter + const validatedFrontmatter = + ProposalFrontmatter.parse(proposalFrontmatter) - const updates = await readdir(path.join(providerPath, 'updates')) - const updateFiles = updates.filter((file) => file.endsWith('.md')) + // Check if the logo (as a relative path) exists + const logoPath = path.join(providerPath, validatedFrontmatter.logo) + const logoExists = await Bun.file(logoPath).exists() + if (!logoExists) { + throw new Error(`File does not exist: ${logoPath}`) + } - // Make sure there's nothing else besides {number}.md files - if (updateFiles.length !== updates.length) { - throw new Error( - `There should only be numbered .md files in the updates folder` - ) - } + validateProposalHeadings(proposalMarkdown, proposalPath) + + const updates = await readdir(path.join(providerPath, 'updates')) + const updateFiles = updates.filter((file) => file.endsWith('.md')) - // Check that updates are numbered sequentially - for (let i = 0; i < updateFiles.length; i++) { - const update = updateFiles[i]! - const updateNumber = parseInt(update.split('.')[0]!) - if (updateNumber !== i + 1) { - throw new Error('Updates are not numbered sequentially') + // Make sure there's nothing else besides {number}.md files + if (updateFiles.length !== updates.length) { + throw new Error( + `There should only be numbered .md files in the updates folder` + ) } - } - for (const updateFile of updateFiles) { - const updatePath = path.join(providerPath, 'updates', updateFile) - const updateContent = await Bun.file(updatePath).text() - const { content: updateMarkdown } = matter(updateContent) + // Check that updates are numbered sequentially + for (let i = 0; i < updateFiles.length; i++) { + const update = updateFiles[i]! + const updateNumber = parseInt(update.split('.')[0]!) + if (updateNumber !== i + 1) { + throw new Error('Updates are not numbered sequentially') + } + } + + for (const updateFile of updateFiles) { + const updatePath = path.join(providerPath, 'updates', updateFile) + const updateContent = await Bun.file(updatePath).text() + const { content: updateMarkdown } = matter(updateContent) - validateUpdateHeadings(updateMarkdown) + validateUpdateHeadings(updateMarkdown, updatePath) + } } } } diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index 01b7f0f..1988a91 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -1,4 +1,5 @@ import { marked, type Tokens } from 'marked' +import { GitHubError } from './error' const requiredProposalH2s = [ 'Applicant Information', @@ -10,11 +11,16 @@ const requiredProposalH2s = [ 'Conflict Of Interest Statment', ] -export function validateProposalHeadings(content: string) { +export function validateProposalHeadings(content: string, filename: string) { const { h1s, h2s } = extractHeadings(content) if (h1s.length !== 1) { - throw new Error('Only one H1 is allowed') + throw new GitHubError({ + filename, + line: 0, + title: 'Only one H1 is allowed', + message: 'Please add only one H1 to the proposal.md file.', + }) } const missingH2s = requiredProposalH2s.filter( @@ -32,7 +38,7 @@ export function validateProposalHeadings(content: string) { const requiredUpdateH2s = ['Summary', 'KPIs'] -export function validateUpdateHeadings(content: string) { +export function validateUpdateHeadings(content: string, filename: string) { const { h1s, h2s } = extractHeadings(content) if (h1s.length !== 1) { @@ -44,7 +50,14 @@ export function validateUpdateHeadings(content: string) { ) if (missingH2s.length > 0) { - throw new Error(`Missing required headings: ${missingH2s.join(', ')}`) + throw new GitHubError({ + filename, + line: 0, + title: 'Missing required headings', + message: `This update is missing the following headings: ${missingH2s.join( + ', ' + )}`, + }) } } From 376ea0731049678278612c31c757f51cb1b3e212 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:09:57 -0400 Subject: [PATCH 07/18] Test --- spp-template/provider/updates/1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp-template/provider/updates/1.md b/spp-template/provider/updates/1.md index a0febb7..ffa03f8 100644 --- a/spp-template/provider/updates/1.md +++ b/spp-template/provider/updates/1.md @@ -1,6 +1,6 @@ # Quarterly Report Q1 2025 -## Summary +## Summar This section is for showcasing your work this quarter—shipments, improvements, and impact. From 4c392c8e772ba9c2e74a0201a6925ee60424bf01 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:14:38 -0400 Subject: [PATCH 08/18] Fix ? --- .github/workflows/validate.yml | 1 - scripts/src/error.ts | 8 +++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml index c4d683e..f821103 100644 --- a/.github/workflows/validate.yml +++ b/.github/workflows/validate.yml @@ -2,7 +2,6 @@ name: Validate SPP Data on: push: - pull_request: jobs: validate: diff --git a/scripts/src/error.ts b/scripts/src/error.ts index b2576b0..5e703da 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -1,3 +1,5 @@ +import path from 'node:path' + type GitHubErrorOptions = { filename: string line: number @@ -7,8 +9,12 @@ type GitHubErrorOptions = { export class GitHubError extends Error { constructor({ filename, line, title, message }: GitHubErrorOptions) { + // Convert the filename to a relative path + const rootPath = path.join(import.meta.dirname, '..', '..') + const relativeFilename = path.relative(rootPath, filename) + // GitHub will display inline errors if we log the error in the following format - const errorMessage = `::error file=${filename},line=${line},title=${title}::${message}` + const errorMessage = `::error file=${relativeFilename},line=${line},title=${title}::${message}` super(errorMessage) } } From e2d214b883ff77ec3154d2205d6f00c145db7852 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:22:16 -0400 Subject: [PATCH 09/18] Only throw GitHubError's --- scripts/src/index.ts | 28 ++++++++++++++++++++++------ scripts/src/validate.ts | 18 +++++++++++++----- 2 files changed, 35 insertions(+), 11 deletions(-) diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 55478e5..3d52161 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -14,6 +14,8 @@ try { await validate() } catch (error) { if (error instanceof GitHubError) { + console.log(error) + // Log the error instead of throwing it so it displays nicely in GitHub Workflows console.log(error.message) process.exit(1) @@ -44,19 +46,28 @@ async function validate() { const logoPath = path.join(providerPath, validatedFrontmatter.logo) const logoExists = await Bun.file(logoPath).exists() if (!logoExists) { - throw new Error(`File does not exist: ${logoPath}`) + throw new GitHubError({ + filename: logoPath, + line: 0, + title: 'File does not exist', + message: `The logo file does not exist: ${logoPath}`, + }) } validateProposalHeadings(proposalMarkdown, proposalPath) - const updates = await readdir(path.join(providerPath, 'updates')) + const updatesPath = path.join(providerPath, 'updates') + const updates = await readdir(updatesPath) const updateFiles = updates.filter((file) => file.endsWith('.md')) // Make sure there's nothing else besides {number}.md files if (updateFiles.length !== updates.length) { - throw new Error( - `There should only be numbered .md files in the updates folder` - ) + throw new GitHubError({ + filename: providerPath, + line: 0, + title: 'Invalid update files', + message: `There should only be numbered .md files in the updates folder`, + }) } // Check that updates are numbered sequentially @@ -64,7 +75,12 @@ async function validate() { const update = updateFiles[i]! const updateNumber = parseInt(update.split('.')[0]!) if (updateNumber !== i + 1) { - throw new Error('Updates are not numbered sequentially') + throw new GitHubError({ + filename: updatesPath, + line: 0, + title: 'Updates are not numbered sequentially', + message: `The update files are not numbered sequentially`, + }) } } diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index 1988a91..ab53cb3 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -28,11 +28,14 @@ export function validateProposalHeadings(content: string, filename: string) { ) if (missingH2s.length > 0) { - throw new Error( - `Missing required headings: ${missingH2s.join( + throw new GitHubError({ + filename, + line: 0, + title: 'Missing required headings', + message: `This proposal is missing the following headings: ${missingH2s.join( ', ' - )}. Please add them to the proposal.md file.` - ) + )}`, + }) } } @@ -42,7 +45,12 @@ export function validateUpdateHeadings(content: string, filename: string) { const { h1s, h2s } = extractHeadings(content) if (h1s.length !== 1) { - throw new Error('Only one H1 is allowed') + throw new GitHubError({ + filename, + line: 0, + title: 'Only one H1 is allowed', + message: 'Please add only one H1 to the update.md file.', + }) } const missingH2s = requiredUpdateH2s.filter( From 5b9469d536da0e88b9ebbbf4c0f3f4d5d6deae06 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:25:21 -0400 Subject: [PATCH 10/18] Fix ? --- scripts/src/error.ts | 16 +++++++++++++--- scripts/src/index.ts | 9 ++++----- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/scripts/src/error.ts b/scripts/src/error.ts index 5e703da..e153085 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -8,13 +8,23 @@ type GitHubErrorOptions = { } export class GitHubError extends Error { + public readonly filename: string + public readonly line: number + public readonly title: string + public readonly annotation: string + constructor({ filename, line, title, message }: GitHubErrorOptions) { + // Use the message as the regular error message + super(message) + // Convert the filename to a relative path const rootPath = path.join(import.meta.dirname, '..', '..') const relativeFilename = path.relative(rootPath, filename) - // GitHub will display inline errors if we log the error in the following format - const errorMessage = `::error file=${relativeFilename},line=${line},title=${title}::${message}` - super(errorMessage) + // Store the GitHub annotation separately + this.filename = relativeFilename + this.line = line + this.title = title + this.annotation = `::error file=${relativeFilename},line=${line},title=${title}::${message}` } } diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 3d52161..75a16de 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -14,14 +14,13 @@ try { await validate() } catch (error) { if (error instanceof GitHubError) { - console.log(error) - - // Log the error instead of throwing it so it displays nicely in GitHub Workflows - console.log(error.message) + // Output the GitHub annotation to stdout + console.log(error.annotation) process.exit(1) } - throw error + console.error(error) + process.exit(1) } async function validate() { From e6f022165e0e3f88d3675327cb68befdedf152cf Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:27:28 -0400 Subject: [PATCH 11/18] Remove line --- scripts/src/error.ts | 7 ++----- scripts/src/index.ts | 3 --- scripts/src/validate.ts | 4 ---- 3 files changed, 2 insertions(+), 12 deletions(-) diff --git a/scripts/src/error.ts b/scripts/src/error.ts index e153085..79338de 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -2,18 +2,16 @@ import path from 'node:path' type GitHubErrorOptions = { filename: string - line: number title: string message: string } export class GitHubError extends Error { public readonly filename: string - public readonly line: number public readonly title: string public readonly annotation: string - constructor({ filename, line, title, message }: GitHubErrorOptions) { + constructor({ filename, title, message }: GitHubErrorOptions) { // Use the message as the regular error message super(message) @@ -23,8 +21,7 @@ export class GitHubError extends Error { // Store the GitHub annotation separately this.filename = relativeFilename - this.line = line this.title = title - this.annotation = `::error file=${relativeFilename},line=${line},title=${title}::${message}` + this.annotation = `::error file=${relativeFilename},title=${title}::${message}` } } diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 75a16de..da01e9c 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -47,7 +47,6 @@ async function validate() { if (!logoExists) { throw new GitHubError({ filename: logoPath, - line: 0, title: 'File does not exist', message: `The logo file does not exist: ${logoPath}`, }) @@ -63,7 +62,6 @@ async function validate() { if (updateFiles.length !== updates.length) { throw new GitHubError({ filename: providerPath, - line: 0, title: 'Invalid update files', message: `There should only be numbered .md files in the updates folder`, }) @@ -76,7 +74,6 @@ async function validate() { if (updateNumber !== i + 1) { throw new GitHubError({ filename: updatesPath, - line: 0, title: 'Updates are not numbered sequentially', message: `The update files are not numbered sequentially`, }) diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index ab53cb3..01fd21b 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -17,7 +17,6 @@ export function validateProposalHeadings(content: string, filename: string) { if (h1s.length !== 1) { throw new GitHubError({ filename, - line: 0, title: 'Only one H1 is allowed', message: 'Please add only one H1 to the proposal.md file.', }) @@ -30,7 +29,6 @@ export function validateProposalHeadings(content: string, filename: string) { if (missingH2s.length > 0) { throw new GitHubError({ filename, - line: 0, title: 'Missing required headings', message: `This proposal is missing the following headings: ${missingH2s.join( ', ' @@ -47,7 +45,6 @@ export function validateUpdateHeadings(content: string, filename: string) { if (h1s.length !== 1) { throw new GitHubError({ filename, - line: 0, title: 'Only one H1 is allowed', message: 'Please add only one H1 to the update.md file.', }) @@ -60,7 +57,6 @@ export function validateUpdateHeadings(content: string, filename: string) { if (missingH2s.length > 0) { throw new GitHubError({ filename, - line: 0, title: 'Missing required headings', message: `This update is missing the following headings: ${missingH2s.join( ', ' From df49b8e6417733740feec5e611663d071a54b9f4 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:38:19 -0400 Subject: [PATCH 12/18] Remove special chars --- scripts/src/validate.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index 01fd21b..f6fb050 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -30,9 +30,7 @@ export function validateProposalHeadings(content: string, filename: string) { throw new GitHubError({ filename, title: 'Missing required headings', - message: `This proposal is missing the following headings: ${missingH2s.join( - ', ' - )}`, + message: `${missingH2s.join(', ')}`, }) } } @@ -58,9 +56,7 @@ export function validateUpdateHeadings(content: string, filename: string) { throw new GitHubError({ filename, title: 'Missing required headings', - message: `This update is missing the following headings: ${missingH2s.join( - ', ' - )}`, + message: `${missingH2s.join(', ')}`, }) } } From 8ab6c6cb1aa5443a135a3fa85d58d283e4d2c84c Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:38:45 -0400 Subject: [PATCH 13/18] Add line, log error --- scripts/src/error.ts | 2 +- scripts/src/index.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/src/error.ts b/scripts/src/error.ts index 79338de..20cfdf0 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -22,6 +22,6 @@ export class GitHubError extends Error { // Store the GitHub annotation separately this.filename = relativeFilename this.title = title - this.annotation = `::error file=${relativeFilename},title=${title}::${message}` + this.annotation = `::error file=${relativeFilename},line=1,title=${title}::${message}` } } diff --git a/scripts/src/index.ts b/scripts/src/index.ts index da01e9c..304a40d 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -14,6 +14,7 @@ try { await validate() } catch (error) { if (error instanceof GitHubError) { + console.log(error) // Output the GitHub annotation to stdout console.log(error.annotation) process.exit(1) From f81915be750fa659ddaac59a7ac61c479773435a Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:46:58 -0400 Subject: [PATCH 14/18] Revert --- scripts/src/error.ts | 2 +- scripts/src/validate.ts | 8 ++++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/scripts/src/error.ts b/scripts/src/error.ts index 20cfdf0..79338de 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -22,6 +22,6 @@ export class GitHubError extends Error { // Store the GitHub annotation separately this.filename = relativeFilename this.title = title - this.annotation = `::error file=${relativeFilename},line=1,title=${title}::${message}` + this.annotation = `::error file=${relativeFilename},title=${title}::${message}` } } diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index f6fb050..01fd21b 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -30,7 +30,9 @@ export function validateProposalHeadings(content: string, filename: string) { throw new GitHubError({ filename, title: 'Missing required headings', - message: `${missingH2s.join(', ')}`, + message: `This proposal is missing the following headings: ${missingH2s.join( + ', ' + )}`, }) } } @@ -56,7 +58,9 @@ export function validateUpdateHeadings(content: string, filename: string) { throw new GitHubError({ filename, title: 'Missing required headings', - message: `${missingH2s.join(', ')}`, + message: `This update is missing the following headings: ${missingH2s.join( + ', ' + )}`, }) } } From b3cf2b2ece7f6de10c72c20f42d21a48eca36403 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:52:16 -0400 Subject: [PATCH 15/18] Yay --- scripts/src/error.ts | 2 +- scripts/src/index.ts | 5 +---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/scripts/src/error.ts b/scripts/src/error.ts index 79338de..20cfdf0 100644 --- a/scripts/src/error.ts +++ b/scripts/src/error.ts @@ -22,6 +22,6 @@ export class GitHubError extends Error { // Store the GitHub annotation separately this.filename = relativeFilename this.title = title - this.annotation = `::error file=${relativeFilename},title=${title}::${message}` + this.annotation = `::error file=${relativeFilename},line=1,title=${title}::${message}` } } diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 304a40d..1fa74a3 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -14,14 +14,11 @@ try { await validate() } catch (error) { if (error instanceof GitHubError) { - console.log(error) - // Output the GitHub annotation to stdout console.log(error.annotation) process.exit(1) } - console.error(error) - process.exit(1) + throw error } async function validate() { From d1c89f2a92039224224ef9ae706871912b95cca4 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 15:53:06 -0400 Subject: [PATCH 16/18] Fix update template --- spp-template/provider/updates/1.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/spp-template/provider/updates/1.md b/spp-template/provider/updates/1.md index ffa03f8..a0febb7 100644 --- a/spp-template/provider/updates/1.md +++ b/spp-template/provider/updates/1.md @@ -1,6 +1,6 @@ # Quarterly Report Q1 2025 -## Summar +## Summary This section is for showcasing your work this quarter—shipments, improvements, and impact. From 03f051f517b3abb49e7fa48fc1195df1dd0c2b82 Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 16:04:53 -0400 Subject: [PATCH 17/18] Prettier --- .prettierrc | 8 ++++++++ .vscode/extensions.json | 3 +++ scripts/src/index.ts | 2 +- scripts/src/validate.ts | 1 + 4 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 .prettierrc create mode 100644 .vscode/extensions.json diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..94a537f --- /dev/null +++ b/.prettierrc @@ -0,0 +1,8 @@ +{ + "semi": false, + "tabWidth": 2, + "printWidth": 80, + "useTabs": false, + "singleQuote": true, + "trailingComma": "es5" +} diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..c83e263 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,3 @@ +{ + "recommendations": ["esbenp.prettier-vscode"] +} diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 1fa74a3..75405a0 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -2,9 +2,9 @@ import { readdir } from 'node:fs/promises' import matter from 'gray-matter' import path from 'node:path' +import { GitHubError } from './error' import { ProposalFrontmatter } from './schema' import { validateProposalHeadings, validateUpdateHeadings } from './validate' -import { GitHubError } from './error' const rootPath = path.join(import.meta.dirname, '..', '..') const root = await readdir(rootPath) diff --git a/scripts/src/validate.ts b/scripts/src/validate.ts index 01fd21b..93ee6d5 100644 --- a/scripts/src/validate.ts +++ b/scripts/src/validate.ts @@ -1,4 +1,5 @@ import { marked, type Tokens } from 'marked' + import { GitHubError } from './error' const requiredProposalH2s = [ From b70fd6b53089b457110a3ed47af0b476b7136fcd Mon Sep 17 00:00:00 2001 From: Greg Skriloff <35093316+gskril@users.noreply.github.com> Date: Tue, 26 Aug 2025 17:45:06 -0400 Subject: [PATCH 18/18] Safe parse frontmatter --- scripts/src/index.ts | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/scripts/src/index.ts b/scripts/src/index.ts index 75405a0..88eefb6 100644 --- a/scripts/src/index.ts +++ b/scripts/src/index.ts @@ -36,17 +36,27 @@ async function validate() { ) // Validate the frontmatter - const validatedFrontmatter = - ProposalFrontmatter.parse(proposalFrontmatter) + const { data: validatedFrontmatter, error: frontmatterError } = + ProposalFrontmatter.safeParse(proposalFrontmatter) + + if (frontmatterError) { + throw new GitHubError({ + filename: proposalPath, + title: 'Invalid frontmatter', + message: `The frontmatter is invalid: ${frontmatterError.issues + .map((issue) => issue.message) + .join(', ')}`, + }) + } // Check if the logo (as a relative path) exists const logoPath = path.join(providerPath, validatedFrontmatter.logo) const logoExists = await Bun.file(logoPath).exists() if (!logoExists) { throw new GitHubError({ - filename: logoPath, - title: 'File does not exist', - message: `The logo file does not exist: ${logoPath}`, + filename: proposalPath, + title: 'Logo does not exist', + message: `\`${validatedFrontmatter.logo}\` does not exist`, }) }