mirror of
https://github.com/netbirdio/docs.git
synced 2026-08-27 10:11:27 +02:00
ci: validate PRs with build and MDX heading linter (#743)
* ci: validate PRs with build and MDX heading linter Adds a pull_request workflow running npm run lint:mdx, npm run build, and npm run lint so heading-hierarchy bugs and broken builds get caught before merge rather than after. The new linter (scripts/lint-mdx-headings.mjs) enforces that the first heading is h1 and that heading levels never jump by more than one. Also fixes three existing pages that had no h1 title — two were using a legacy export const title pattern, one was missing a title entirely. * ci: use npm install since lockfile is gitignored package-lock.json is in .gitignore, so npm ci and setup-node's npm cache both fail on a fresh CI checkout. Match the Dockerfile pattern (npm install, no cache) instead. * ci: drop ESLint step; project config is broken `npm run lint` fails with 'Converting circular structure to JSON' under ESLint 9.x — the repo has no .eslintrc or eslint.config file, so the legacy resolver hits the React plugin's circular reference. This is pre-existing (build_n_push.yml never ran lint, so it stayed hidden); fixing it needs flat-config migration and is out of scope. Drop the step until that lands.
This commit is contained in:
33
.github/workflows/pr-build.yml
vendored
Normal file
33
.github/workflows/pr-build.yml
vendored
Normal file
@@ -0,0 +1,33 @@
|
||||
name: pr-build
|
||||
on: [pull_request]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
concurrency:
|
||||
group: ${{ github.workflow }}-${{ github.ref }}-${{ github.head_ref || github.actor_id }}
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
build:
|
||||
name: lint and build
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install
|
||||
|
||||
- name: Lint MDX heading hierarchy
|
||||
run: npm run lint:mdx
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
@@ -11,7 +11,8 @@
|
||||
"gen": "swagger-codegen generate -i https://raw.githubusercontent.com/netbirdio/netbird/main/management/server/http/api/openapi.yml -l openapi -o generator/openapi && npx ts-node generator/index.ts gen --input generator/openapi/openapi.json --output src/pages/ipa/resources",
|
||||
"gen:llm": "node scripts/generate-llm-docs.mjs",
|
||||
"start": "next start",
|
||||
"lint": "eslint src/"
|
||||
"lint": "eslint src/",
|
||||
"lint:mdx": "node scripts/lint-mdx-headings.mjs"
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
|
||||
97
scripts/lint-mdx-headings.mjs
Normal file
97
scripts/lint-mdx-headings.mjs
Normal file
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env node
|
||||
/**
|
||||
* Lints MDX files for heading-hierarchy violations:
|
||||
* - the first heading on a page must be h1 (the page title)
|
||||
* - heading levels may not jump by more than 1 going down (h1 -> h3 is invalid; h3 -> h1 is fine)
|
||||
*
|
||||
* Skips src/pages/ipa/resources/ — those are auto-generated from the OpenAPI spec.
|
||||
* Headings inside fenced code blocks are ignored.
|
||||
*/
|
||||
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { fileURLToPath } from 'url'
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
||||
const ROOT = path.join(__dirname, '..')
|
||||
const PAGES_DIR = path.join(ROOT, 'src/pages')
|
||||
const SKIP_PREFIX = 'ipa/resources'
|
||||
|
||||
function findMdxFiles(dir, basePath = '') {
|
||||
const entries = fs.readdirSync(dir, { withFileTypes: true })
|
||||
const results = []
|
||||
for (const e of entries) {
|
||||
const rel = basePath ? `${basePath}/${e.name}` : e.name
|
||||
if (e.isDirectory()) {
|
||||
if (rel === SKIP_PREFIX || rel.startsWith(`${SKIP_PREFIX}/`)) continue
|
||||
results.push(...findMdxFiles(path.join(dir, e.name), rel))
|
||||
} else if (e.name.endsWith('.mdx')) {
|
||||
results.push({ relPath: rel, filePath: path.join(dir, e.name) })
|
||||
}
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
function checkHeadings(content) {
|
||||
const lines = content.split('\n')
|
||||
const violations = []
|
||||
let inCodeBlock = false
|
||||
let prevLevel = 0
|
||||
let prevLine = 0
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i]
|
||||
if (/^```/.test(line)) {
|
||||
inCodeBlock = !inCodeBlock
|
||||
continue
|
||||
}
|
||||
if (inCodeBlock) continue
|
||||
|
||||
const match = /^(#{1,6})\s+(.+?)\s*$/.exec(line)
|
||||
if (!match) continue
|
||||
|
||||
const level = match[1].length
|
||||
const text = match[2]
|
||||
|
||||
if (prevLevel === 0) {
|
||||
if (level !== 1) {
|
||||
violations.push({
|
||||
line: i + 1,
|
||||
message: `first heading is h${level} ("${text}"); expected h1`,
|
||||
})
|
||||
}
|
||||
} else if (level > prevLevel + 1) {
|
||||
violations.push({
|
||||
line: i + 1,
|
||||
message: `h${level} ("${text}") skips a level (previous was h${prevLevel} on line ${prevLine})`,
|
||||
})
|
||||
}
|
||||
|
||||
prevLevel = level
|
||||
prevLine = i + 1
|
||||
}
|
||||
|
||||
return violations
|
||||
}
|
||||
|
||||
const files = findMdxFiles(PAGES_DIR)
|
||||
let totalViolations = 0
|
||||
|
||||
for (const { relPath, filePath } of files) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8')
|
||||
const violations = checkHeadings(content)
|
||||
if (violations.length > 0) {
|
||||
console.error(`\nsrc/pages/${relPath}`)
|
||||
for (const v of violations) {
|
||||
console.error(` line ${v.line}: ${v.message}`)
|
||||
}
|
||||
totalViolations += violations.length
|
||||
}
|
||||
}
|
||||
|
||||
if (totalViolations > 0) {
|
||||
console.error(`\n${totalViolations} heading hierarchy violation(s) found.`)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
console.log(`Checked ${files.length} MDX files — no heading hierarchy violations.`)
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Note} from "@/components/mdx";
|
||||
|
||||
export const title = 'Other'
|
||||
# Other
|
||||
|
||||
## Google Summer of Code Ideas - 2022
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import {Note} from "@/components/mdx";
|
||||
|
||||
export const title = 'Understanding NAT and Connectivity'
|
||||
# Understanding NAT and Connectivity
|
||||
|
||||
## What is NAT?
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
## Client Sidecar
|
||||
# Client Sidecar
|
||||
|
||||
In certain situations we may want to have a pod act like a peer in the Netbird network instead of exposing it through a routing peer. In these cases a Netbird client container has to be added as a sidecar to the pod.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user