From 0b09994e899077c86643bc5fe535b2ca020ed393 Mon Sep 17 00:00:00 2001 From: Ansh Goyal Date: Wed, 23 Oct 2024 12:46:21 +0530 Subject: [PATCH] test: add markdown tests (#3301) Co-authored-by: Akshat Nema <76521428+akshatnema@users.noreply.github.com> --- .github/workflows/if-nodejs-pr-testing.yml | 38 +++++- markdown/docs/migration/index.md | 1 + markdown/docs/migration/migrating-to-v3.md | 1 + package.json | 1 + scripts/markdown/check-markdown.js | 146 +++++++++++++++++++++ 5 files changed, 185 insertions(+), 2 deletions(-) create mode 100644 scripts/markdown/check-markdown.js diff --git a/.github/workflows/if-nodejs-pr-testing.yml b/.github/workflows/if-nodejs-pr-testing.yml index 769039d1a0aa..c8c3bec4d3c7 100644 --- a/.github/workflows/if-nodejs-pr-testing.yml +++ b/.github/workflows/if-nodejs-pr-testing.yml @@ -61,8 +61,7 @@ jobs: name: Setup Node.js uses: actions/setup-node@v4 with: - node-version: "${{ steps.nodeversion.outputs.version }}" - + node-version: '${{ steps.nodeversion.outputs.version }}' - name: Install dependencies run: npm ci - if: steps.packagejson.outputs.exists == 'true' @@ -76,6 +75,41 @@ jobs: shell: bash run: npm run generate:assets --if-present + # Run the test:md script and capture output + - if: steps.packagejson.outputs.exists == 'true' + name: Run markdown checks + id: markdown_check + run: | + ERRORS=$(npm run test:md | sed -n '/Errors in file/,$p') + echo "markdown_output<> $GITHUB_OUTPUT + echo "$ERRORS" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + # Post a comment using sticky-pull-request-comment + - name: Comment on PR with markdown issues + if: ${{ steps.markdown_check.outputs.markdown_output != '' }} + uses: marocchino/sticky-pull-request-comment@3d60a5b2dae89d44e0c6ddc69dd7536aec2071cd + with: + header: markdown-check-error + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + message: | + ### Markdown Check Results + + We found issues in the following markdown files: + + ``` + ${{ steps.markdown_check.outputs.markdown_output }} + ``` + + # Delete the comment if there are no issues + - if: ${{ steps.markdown_check.outputs.markdown_output == '' }} + name: Delete markdown check comment + uses: marocchino/sticky-pull-request-comment@3d60a5b2dae89d44e0c6ddc69dd7536aec2071cd + with: + header: markdown-check-error + delete: true + GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} + - if: steps.packagejson.outputs.exists == 'true' name: Upload Coverage to Codecov uses: codecov/codecov-action@e28ff129e5465c2c0dcc6f003fc735cb6ae0c673 diff --git a/markdown/docs/migration/index.md b/markdown/docs/migration/index.md index 195bcc35cf54..a8a57359ae21 100644 --- a/markdown/docs/migration/index.md +++ b/markdown/docs/migration/index.md @@ -1,5 +1,6 @@ --- title: "Overview" +weight: 1 --- Migration to a new major version is always difficult, and AsyncAPI is no exception, but we want to provide as smooth a transition as possible. diff --git a/markdown/docs/migration/migrating-to-v3.md b/markdown/docs/migration/migrating-to-v3.md index 85fa6bb47a76..00587645fee6 100644 --- a/markdown/docs/migration/migrating-to-v3.md +++ b/markdown/docs/migration/migrating-to-v3.md @@ -1,5 +1,6 @@ --- title: "Migrating to v3" +weight: 2 --- diff --git a/package.json b/package.json index 04ba37b586eb..29269ed3e1d6 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "generate:videos": "node scripts/build-newsroom-videos.js", "generate:tools": "node scripts/build-tools.js", "test:netlify": "deno test --allow-env --trace-ops netlify/**/*.test.ts", + "test:md": "node scripts/markdown/check-markdown.js", "dev:storybook": "storybook dev -p 6006", "build:storybook": "storybook build" }, diff --git a/scripts/markdown/check-markdown.js b/scripts/markdown/check-markdown.js new file mode 100644 index 000000000000..8979f7e0b4ab --- /dev/null +++ b/scripts/markdown/check-markdown.js @@ -0,0 +1,146 @@ +const fs = require('fs'); +const matter = require('gray-matter'); +const path = require('path'); + +/** + * Checks if a given string is a valid URL. + * @param {string} str - The string to validate as a URL. + * @returns {boolean} True if the string is a valid URL, false otherwise. + */ +function isValidURL(str) { + try { + new URL(str); + return true; + } catch (err) { + return false; + } +} + +/** + * Validates the frontmatter of a blog post. + * @param {object} frontmatter - The frontmatter object to validate. + * @param {string} filePath - The path to the file being validated. + * @returns {string[]|null} An array of validation error messages, or null if no errors. + */ +function validateBlogs(frontmatter) { + const requiredAttributes = ['title', 'date', 'type', 'tags', 'cover', 'authors']; + const errors = []; + + // Check for required attributes + requiredAttributes.forEach(attr => { + if (!frontmatter.hasOwnProperty(attr)) { + errors.push(`${attr} is missing`); + } + }); + + // Validate date format + if (frontmatter.date && Number.isNaN(Date.parse(frontmatter.date))) { + errors.push(`Invalid date format: ${frontmatter.date}`); + } + + // Validate tags format (must be an array) + if (frontmatter.tags && !Array.isArray(frontmatter.tags)) { + errors.push(`Tags should be an array`); + } + + // Validate cover is a string + if (frontmatter.cover && typeof frontmatter.cover !== 'string') { + errors.push(`Cover must be a string`); + } + + // Validate authors (must be an array with valid attributes) + if (frontmatter.authors) { + if (!Array.isArray(frontmatter.authors)) { + errors.push('Authors should be an array'); + } else { + frontmatter.authors.forEach((author, index) => { + if (!author.name) { + errors.push(`Author at index ${index} is missing a name`); + } + if (author.link && !isValidURL(author.link)) { + errors.push(`Invalid URL for author at index ${index}: ${author.link}`); + } + if (!author.photo) { + errors.push(`Author at index ${index} is missing a photo`); + } + }); + } + } + + return errors.length ? errors : null; +} + +/** + * Validates the frontmatter of a documentation file. + * @param {object} frontmatter - The frontmatter object to validate. + * @param {string} filePath - The path to the file being validated. + * @returns {string[]|null} An array of validation error messages, or null if no errors. + */ +function validateDocs(frontmatter) { + const errors = []; + + // Check if title exists and is a string + if (!frontmatter.title || typeof frontmatter.title !== 'string') { + errors.push('Title is missing or not a string'); + } + + // Check if weight exists and is a number + if (frontmatter.weight === undefined || typeof frontmatter.weight !== 'number') { + errors.push('Weight is missing or not a number'); + } + + return errors.length ? errors : null; +} + +/** + * Recursively checks markdown files in a folder and validates their frontmatter. + * @param {string} folderPath - The path to the folder to check. + * @param {Function} validateFunction - The function used to validate the frontmatter. + * @param {string} [relativePath=''] - The relative path of the folder for logging purposes. + */ +function checkMarkdownFiles(folderPath, validateFunction, relativePath = '') { + fs.readdir(folderPath, (err, files) => { + if (err) { + console.error('Error reading directory:', err); + return; + } + + files.forEach(file => { + const filePath = path.join(folderPath, file); + const relativeFilePath = path.join(relativePath, file); + + // Skip the folder 'docs/reference/specification' + if (relativeFilePath.includes('reference/specification')) { + return; + } + + fs.stat(filePath, (err, stats) => { + if (err) { + console.error('Error reading file stats:', err); + return; + } + + // Recurse if directory, otherwise validate markdown file + if (stats.isDirectory()) { + checkMarkdownFiles(filePath, validateFunction, relativeFilePath); + } else if (path.extname(file) === '.md') { + const fileContent = fs.readFileSync(filePath, 'utf-8'); + const { data: frontmatter } = matter(fileContent); + + const errors = validateFunction(frontmatter); + if (errors) { + console.log(`Errors in file ${relativeFilePath}:`); + errors.forEach(error => console.log(` - ${error}`)); + process.exitCode = 1; + } + } + }); + }); + }); +} + +const docsFolderPath = path.resolve(__dirname, '../../markdown/docs'); +const blogsFolderPath = path.resolve(__dirname, '../../markdown/blog'); + +checkMarkdownFiles(docsFolderPath, validateDocs); +checkMarkdownFiles(blogsFolderPath, validateBlogs);