Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

test: add markdown tests #3301

Merged
merged 22 commits into from
Oct 23, 2024
Merged
Show file tree
Hide file tree
Changes from 16 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 36 additions & 2 deletions .github/workflows/if-nodejs-pr-testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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<<EOF" >> $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
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
141 changes: 141 additions & 0 deletions scripts/markdown/check-markdown.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
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)) {
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved
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`);
}
});
}
}
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved

return errors.length ? errors : null;
}
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
}
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved

/**
* 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;
}
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved

files.forEach(file => {
const filePath = path.join(folderPath, file);
const relativeFilePath = path.join(relativePath, file);

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);
anshgoyalevil marked this conversation as resolved.
Show resolved Hide resolved

const errors = validateFunction(frontmatter);
if (errors) {
console.log(`Errors in file ${relativeFilePath}:`);
errors.forEach(error => console.log(`${error}\n`));
process.exitCode = 1;
}
}
});
});
});
}

const docsFolderPath = path.resolve(__dirname, '../../markdown/docs');
const blogsFolderPath = path.resolve(__dirname, '../../markdown/blog');

checkMarkdownFiles(docsFolderPath, validateDocs);
checkMarkdownFiles(blogsFolderPath, validateBlogs);
Loading