diff --git a/.github/workflows/pr-build.yml b/.github/workflows/pr-build.yml new file mode 100644 index 00000000..88d2c70d --- /dev/null +++ b/.github/workflows/pr-build.yml @@ -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 diff --git a/package.json b/package.json index 0b7565fa..428da65d 100644 --- a/package.json +++ b/package.json @@ -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": [ diff --git a/scripts/lint-mdx-headings.mjs b/scripts/lint-mdx-headings.mjs new file mode 100644 index 00000000..3adc89ed --- /dev/null +++ b/scripts/lint-mdx-headings.mjs @@ -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.`) diff --git a/src/pages/about-netbird/other.mdx b/src/pages/about-netbird/other.mdx index 43eb4501..f14e91c2 100644 --- a/src/pages/about-netbird/other.mdx +++ b/src/pages/about-netbird/other.mdx @@ -1,6 +1,6 @@ import {Note} from "@/components/mdx"; -export const title = 'Other' +# Other ## Google Summer of Code Ideas - 2022 diff --git a/src/pages/about-netbird/understanding-nat-and-connectivity.mdx b/src/pages/about-netbird/understanding-nat-and-connectivity.mdx index ba2f960a..3a7f702f 100644 --- a/src/pages/about-netbird/understanding-nat-and-connectivity.mdx +++ b/src/pages/about-netbird/understanding-nat-and-connectivity.mdx @@ -1,6 +1,6 @@ import {Note} from "@/components/mdx"; -export const title = 'Understanding NAT and Connectivity' +# Understanding NAT and Connectivity ## What is NAT? diff --git a/src/pages/manage/integrations/kubernetes/client-sidecar.mdx b/src/pages/manage/integrations/kubernetes/client-sidecar.mdx index 8ee24c13..2d695c3a 100644 --- a/src/pages/manage/integrations/kubernetes/client-sidecar.mdx +++ b/src/pages/manage/integrations/kubernetes/client-sidecar.mdx @@ -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.