diff --git a/.github/workflows/if-nodejs-pr-testing.yml b/.github/workflows/if-nodejs-pr-testing.yml index 769039d1a0a..c8c3bec4d3c 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 195bcc35cf5..a8a57359ae2 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 85fa6bb47a7..00587645fee 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 04ba37b586e..29269ed3e1d 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 00000000000..8979f7e0b4a --- /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);