Skip to content

new blog linting rules #13947

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
30 changes: 27 additions & 3 deletions .github/workflows/pull-request.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,35 @@ jobs:
environment: testing
steps:

- uses: actions/checkout@v2

- uses: actions/checkout@v4
with:
# Only fetch PR branch
fetch-depth: 1
ref: ${{ github.event.pull_request.head.sha }}

- name: Fetch base branch
run: |
# Fetch and create local master branch
git -c protocol.version=2 fetch --no-tags --prune --progress --no-recurse-submodules --depth=1 origin master:master
# Verify branches
echo "Local branches:"
git branch -v
echo "\nRemote branches:"
git branch -r

- name: Set base branch env
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "BASE_BRANCH=${{ github.event.pull_request.base.ref }}" >> $GITHUB_ENV
else
# This covers push scenario to master
# or any other scenario with no PR info
echo "BASE_BRANCH=" >> $GITHUB_ENV
fi
echo "Branch:${{ github.event.pull_request.base.ref }}"
- uses: actions/setup-node@v1
with:
node-version: '18.x'

- uses: peaceiris/actions-hugo@v2
with:
hugo-version: '0.135.0'
Expand Down Expand Up @@ -55,6 +78,7 @@ jobs:
ALGOLIA_APP_SEARCH_KEY: ${{ vars.ALGOLIA_APP_SEARCH_KEY }}
ALGOLIA_APP_ADMIN_KEY: ${{ secrets.ALGOLIA_APP_ADMIN_KEY }}
NODE_OPTIONS: "--max_old_space_size=8192"
BASE_BRANCH: ${{ env.BASE_BRANCH }}

- name: Archive test results
uses: actions/upload-artifact@v4
Expand Down
5 changes: 2 additions & 3 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -141,12 +141,11 @@ new-example-program:

.PHONY: new-blog-post
new-blog-post:
hugo new --kind blog-post --contentDir content \
"blog/$(shell bash -c 'read -p "Slug (e.g., 'my-new-post'): " slug; echo $$slug')"
./scripts/new-blog-post.sh

.PHONY: lint
lint:
./scripts/lint.sh
./scripts/lint/lint.sh

.PHONY: format
format:
Expand Down
2 changes: 1 addition & 1 deletion archetypes/blog-post/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ title: "{{ replace .Name "-" " " | title }}"
# published. To influence the ordering of posts published on the same date, use
# the time portion of the date value; posts are sorted in descending order by
# date/time.
date: {{ .Date }}
date: {{ now.Format "2006-01-02" }}

# The draft setting determines whether a post is published. Set it to true if
# you want to be able to merge the post without publishing it.
Expand Down
2 changes: 1 addition & 1 deletion content/blog/sam-cogan-testing-best-practices/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ draft: true
# of the content of the post, which is useful for targeting search results or
# social-media previews. This field is required or the build will fail the
# linter test. Max length is 160 characters.
meta_desc:
meta_desc: Infrastructure Testing Best Practices of Sam Cogan, Puluminary & Azure MVP

# The meta_image appears in social-media previews and on the blog home page. A
# placeholder image representing the recommended format, dimensions and aspect
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
# Name of the event, <= 60 characters
title: "Codefest: Build a Dagger Module with Pulumi & Chainguard"
meta_desc: Join Pulumi, Dagger, and Chainguard for an evening of networking, coding, and talks. Learn basics with Dagger's tutorial, then showcase your module with lightning demos.
meta_desc: Join Pulumi, Dagger, and Chainguard for an evening of networking, coding, and talks.
meta_image:

# A featured webinar will display first in the list.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
# Name of the event, <= 60 characters
title: Connecting, Securing and Scaling Microservices with Kubernetes
allow_long_title: true
meta_desc: Listen in and you will learn practical insights on how organizations can ease growing pains and successfully take Kubernetes from test to production.
meta_image:

Expand Down
1 change: 1 addition & 0 deletions content/events/kubecon-meetup/index.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
---
# Name of the event, <= 60 characters
title: "RSVP to Meetup: Development & Data Productivity in the Age of AI"
allow_long_title: true
meta_desc: Join Docker, Pulumi, Tailscale, and New Relic for drinks, snacks, and casual conversations.
meta_image:

Expand Down
16 changes: 13 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
"license": "Apache-2.0",
"scripts": {
"minify-css": "node scripts/minify-css.js",
"prepare": "husky"
"prepare": "husky",
"ts-node": "ts-node"
},
"dependencies": {
"@aws-sdk/client-s3": "^3.427.0",
Expand Down Expand Up @@ -36,10 +37,19 @@
"turndown": "^7.2.0",
"typedoc": "^0.25.11",
"typedoc-plugin-script-inject": "^1.0.0",
"typescript": "^4.9.5"
"typescript": "^4.9.5",
"zod": "^3.24.1",
"zod-matter": "^0.1.1"
},
"resolutions": {
"@types/ms": "0.7.32"
},
"devDependencies": {
"@types/js-yaml": "^4.0.9",
"@types/markdownlint": "^0.18.0",
"@types/node": "^22.13.1",
"husky": "^9.0.11",
"prettier": "^2.6.2"
"prettier": "^2.6.2",
"ts-node": "^10.9.2"
}
}
6 changes: 0 additions & 6 deletions scripts/lint.sh

This file was deleted.

212 changes: 212 additions & 0 deletions scripts/lint/find-markdown.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import * as fs from 'fs';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adapted from previous lint-markdown.js but expanded to cover if file is net new when compared to parent branch.

import * as path from 'path';
import { execSync } from 'child_process';
import { z } from 'zod';
import { parse as parseMatter } from 'zod-matter';

const frontMatterSchema = z.object({}).catchall(z.any());

type FrontMatter = z.infer<typeof frontMatterSchema>;

export interface MarkdownFile {
path: string;
isNew: boolean;
content: string;
// frontMatter can be anything that we parse out
frontMatter?: FrontMatter;
}

/**
* Different modes for finding markdown files and determining if they're new
*/
export type FinderMode = 'git' | 'gha';

const AUTO_GENERATED_HEADING_REGEX = /^> This page was automatically generated\./m;

/**
* Parse front matter from a markdown file
*/
function parseFrontMatter(filePath: string): FrontMatter | undefined {
try {
const content = fs.readFileSync(filePath, 'utf8');

// Check for auto-generated heading in content
if (content.match(AUTO_GENERATED_HEADING_REGEX)) {
return { no_edit_this_page: true };
}

// Parse front matter with zod-matter
const { data } = parseMatter(content, frontMatterSchema);
return data;
} catch (e) {
// If there's no front matter or it's invalid, return null
return undefined;
}
}

/**
* Check if a file should be excluded based on its front matter
*/
function shouldExcludeByFrontMatter(frontMatter: FrontMatter | undefined): boolean {
if (!frontMatter) return false;

return frontMatter.no_edit_this_page === true ||
typeof frontMatter.redirect_to === 'string' ||
frontMatter.block_external_search_index === true ||
!!frontMatter.allow_long_title;
}

import { debug } from './utils';

/**
* Get list of files that are newly added in the current branch compared to base branch,
* including staged and unstaged changes. Handles both local git and GitHub Actions environments.
*/
function getGitModifiedFiles(baseBranch?: string): Set<string> {
try {
debug('Getting git modified files...');
const newFiles = new Set<string>();

// In GitHub Actions, use FETCH_HEAD since we just fetched the base branch
const isGHA = process.env.GITHUB_ACTIONS === 'true';
const baseRef = isGHA ? 'FETCH_HEAD' : (baseBranch || process.env.BASE_BRANCH || 'master');
debug('Using base ref:', baseRef);

try {
// In GHA, we've just fetched the base branch to FETCH_HEAD, so this is safe
const diffOutput = execSync(`git diff --name-status ${baseRef} HEAD`, { encoding: 'utf8' });
debug('Got diff output');
diffOutput.split('\n')
.filter(line => line.startsWith('A\t'))
.map(line => line.split('\t')[1])
.forEach(file => newFiles.add(file));
} catch (error) {
debug('Diff failed:', error);
if (isGHA) {
// In GHA, if diff fails, something is wrong with our setup
throw error;
}
// In local dev, try to get staged/untracked files
debug('Falling back to staged/untracked files only');
}

// In local dev, also look for staged and untracked files
if (!isGHA) {
try {
// Get new files from staged changes
const stagedFiles = execSync('git diff --name-status --cached', { encoding: 'utf8' });
stagedFiles.split('\n')
.filter(line => line.startsWith('A\t'))
.map(line => line.split('\t')[1])
.forEach(file => newFiles.add(file));

// Get untracked files
debug('Getting untracked files...');
const untrackedFiles = execSync('git ls-files --others --exclude-standard --full-name', { encoding: 'utf8' });
debug('Raw untracked files output:', untrackedFiles);
untrackedFiles.split('\n').filter(Boolean).forEach(file => newFiles.add(file));

// Also get untracked markdown files specifically
const untrackedMarkdown = execSync('git ls-files --others --exclude-standard --full-name "*.md"', { encoding: 'utf8' });
debug('Raw untracked markdown files:', untrackedMarkdown);
untrackedMarkdown.split('\n').filter(Boolean).forEach(file => newFiles.add(file));
} catch (error) {
debug('Failed to get staged/untracked files:', error);
}
}

debug('Found new files:', newFiles);
return newFiles;
} catch (error) {
console.warn('Failed to get git modified files:', error);
if (process.env.GITHUB_ACTIONS === 'true') {
console.warn('Running in GitHub Actions - treating all files as existing');
} else {
console.warn('Running locally - treating all files as existing');
}
return new Set<string>();
}
}



/**
* Find all markdown files in the given directory and its subdirectoriesA
* Optionally marks files as new based on git/GHA context.
*/
export function findMarkdownFiles(
rootDir: string,
mode: FinderMode = 'git',
excludePaths: string[] = ['/content/docs/reference/pkg', '/content/registry', '/node_modules']
): MarkdownFile[] {
const files: MarkdownFile[] = [];

// Determine which files are considered new based on mode
let modifiedFiles = new Set<string>();
if (mode === 'git') {
modifiedFiles = getGitModifiedFiles();
} else if (mode === 'gha') {
const baseBranch = process.env.BASE_BRANCH;
debug('GHA mode using base branch:', baseBranch);
modifiedFiles = getGitModifiedFiles(baseBranch);
}

function isExcluded(filePath: string): boolean {
return excludePaths.some(excludePath => filePath.includes(excludePath));
}

function searchDirectory(dir: string) {
const entries = fs.readdirSync(dir);

for (const entry of entries) {
const fullPath = path.join(dir, entry);
const stat = fs.statSync(fullPath);

if (isExcluded(fullPath)) {
continue;
}

if (stat.isDirectory()) {
searchDirectory(fullPath);
continue;
}

if (path.extname(fullPath) === '.md') {
const content = fs.readFileSync(fullPath, 'utf8');
const frontMatter = parseFrontMatter(fullPath);
if (!shouldExcludeByFrontMatter(frontMatter)) {
// Convert absolute path to relative for comparing with git modified files
const relPath = path.relative(process.cwd(), fullPath);
files.push({
path: fullPath,
isNew: modifiedFiles.has(relPath),
content,
frontMatter
});
}
}
}
}

searchDirectory(path.resolve(rootDir));
return files;
}

// If run directly, output found files
if (require.main === module) {
const mode = (process.argv[2] as FinderMode) || 'gha';
const showAll = process.argv.includes('--show-all');
debug('Running in mode:', mode, 'show all:', showAll);

// Find all markdown files
const files = findMarkdownFiles('.', mode);

if (showAll) {
console.log('All markdown files (new files marked with *):')
console.log(JSON.stringify(files.map(f => `${f.isNew ? '*' : ' '} ${f.path}`), null, 2));
} else {
const newFiles = files.filter(f => f.isNew).map(f => f.path);
console.log('New markdown files detected:');
console.log(JSON.stringify(newFiles, null, 2));
}
}
Loading
Loading