diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index de927001c58..7f255e0fd72 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -168,6 +168,14 @@ jobs: name: dist path: dist + - name: Upload Nimbus link data + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 + with: + name: nimbus + path: .nimbus + include-hidden-files: true + if-no-files-found: error + - name: Upload build log if: always() uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7 @@ -213,6 +221,30 @@ jobs: - name: Tests (Workers) run: pnpm run test:postbuild + # Lint steps use `!cancelled()` so a lint failure doesn't skip the + # others. The report step is the gate: it prints the findings and fails. + - name: Download Nimbus link data + if: ${{ !cancelled() }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + name: nimbus + path: .nimbus + + - name: Run Nimbus lint + id: nimbus-lint + if: ${{ !cancelled() }} + run: | + set +e + pnpm exec nimbus-docs lint --format json > "$RUNNER_TEMP/nimbus-lint.json" + echo "exit_code=$?" >> "$GITHUB_OUTPUT" + exit 0 + + - name: Check internal links + if: ${{ !cancelled() }} + env: + LINT_EXIT_CODE: ${{ steps.nimbus-lint.outputs.exit_code }} + run: pnpm exec tsx bin/nimbus-lint-report.ts "$RUNNER_TEMP/nimbus-lint.json" + notify: name: Notify needs: [pre-build, post-build] diff --git a/.gitignore b/.gitignore index e2e665d8890..6238d347500 100644 --- a/.gitignore +++ b/.gitignore @@ -72,3 +72,4 @@ tools/relevant_changed_files.txt .nimbus/coordinates.json .nimbus/lint.json .nimbus/routes.json +.nimbus/page-collections.json diff --git a/AGENTS.md b/AGENTS.md index 4d487d55568..0d2195351f4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -112,7 +112,8 @@ For full component documentation including props, examples, and mandatory usage ```bash pnpm run check # Type-check (validates frontmatter schemas + Astro types) -pnpm run build # Full build (validates MDX parsing, image paths, internal links) — LOCAL ONLY, skip in CI +pnpm run build # Full build (validates MDX parsing, image paths) — LOCAL ONLY, skip in CI +pnpm run lint:nimbus # Nimbus lint, including internal links (needs a prior build: reads .nimbus/routes.json) ``` ### Minimum validation for code changes (.ts/.tsx/.astro/.js) @@ -140,7 +141,8 @@ pnpm run format:core:check # Prettier formatting check pnpm run check # Astro + Worker type checking pnpm run lint # ESLint pnpm run format:core:check # Prettier formatting check -pnpm run build # Full build with link checking +pnpm run build # Full build +pnpm run lint:nimbus # Internal link + frontmatter + image-ref lint (run after build) pnpm run test # All test suites pnpm exec tsm bin/validate-redirects.ts # Only if public/__redirects was modified ``` @@ -170,6 +172,7 @@ The CI workflow (`.github/workflows/ci.yml`) runs on PRs to `production` and che 3. ESLint (reported inline on PR via reviewdog) 4. `pnpm run format:core:check` (Prettier formatting) 5. `pnpm run build` (full build + MDX parsing + image path validation) + - Post Build runs `nimbus-docs lint` against the build's `.nimbus/` data (internal links, redirects) 6. Redirect validation (`bin/validate-redirects.ts`) 7. `pnpm run test` (all Vitest suites) diff --git a/astro.config.ts b/astro.config.ts index c496c6eee7e..d26e6e92d9c 100644 --- a/astro.config.ts +++ b/astro.config.ts @@ -234,8 +234,29 @@ const integrations = [ "error", { aliases: { "~/assets/": "src/assets/" } }, ], - "nimbus/internal-link": "error", + // Paths below are served by the Worker or another deployment, so the + // build's route list never contains them. + "nimbus/internal-link": [ + "error", + { + ignore: [ + "/api/**", + "/openapi.json", + "/.well-known/**", + "**/llms-full.txt", + "**/og.png", + "**/index.md", + // Model 404s are handled directly via custom worker/index.ts logic. + "/workers-ai/models/**", + "/ai/models/**", + ], + }, + ], + // Reuses internal-link's `ignore` list. + "nimbus/redirected-link": "error", }, + // The Worker reads redirects from this file, not from `_redirects`. + redirectsFile: "public/__redirects", }), ]; diff --git a/bin/nimbus-lint-report.ts b/bin/nimbus-lint-report.ts new file mode 100644 index 00000000000..4402f7a5104 --- /dev/null +++ b/bin/nimbus-lint-report.ts @@ -0,0 +1,108 @@ +/** + * Reports `nimbus-docs lint --format json` output and gates the CI step: + * - findings in the log, grouped by rule + * - `::error` annotations (GitHub shows at most 10 per step) + * - per-rule counts in the GitHub step summary + * + * Exits 1 if lint failed or reported errors. + * + * Usage: LINT_EXIT_CODE= tsx bin/nimbus-lint-report.ts + */ +import { appendFileSync, readFileSync } from "node:fs"; + +interface Diagnostic { + code: string; + severity: "error" | "warn" | "warning" | "info"; + message: string; + file: string; + line: number; + column: number; + endLine?: number; + endColumn?: number; +} + +interface LintOutput { + summary: { errors: number; warnings: number; total: number; files: number }; + diagnostics: Diagnostic[]; +} + +const [inputPath] = process.argv.slice(2); +if (!inputPath) { + console.error("Usage: tsx bin/nimbus-lint-report.ts "); + process.exit(2); +} + +const lintExitCode = process.env.LINT_EXIT_CODE ?? "0"; + +// Workflow command escaping: https://docs.github.com/en/actions/reference/workflow-commands-for-github-actions +const escapeData = (s: string) => + s.replace(/%/g, "%25").replace(/\r/g, "%0D").replace(/\n/g, "%0A"); +const escapeProperty = (s: string) => + escapeData(s).replace(/:/g, "%3A").replace(/,/g, "%2C"); + +const raw = readFileSync(inputPath, "utf-8"); +let output: LintOutput; +try { + output = JSON.parse(raw); +} catch { + // Lint crashed before producing JSON, so show whatever it wrote. + console.log(raw); + console.log( + `::error::nimbus-docs lint exited with code ${lintExitCode} and did not produce valid JSON.`, + ); + process.exit(1); +} + +const { diagnostics, summary } = output; + +const byRule = new Map(); +for (const d of diagnostics) { + byRule.set(d.code, [...(byRule.get(d.code) ?? []), d]); +} + +for (const [code, items] of byRule) { + console.log(`::group::${code} (${items.length})`); + for (const d of items) { + console.log(`${d.file}:${d.line}:${d.column} ${d.severity} ${d.message}`); + } + console.log("::endgroup::"); +} + +// GitHub shows only the first 10 annotations per step; the log has the rest. +for (const d of diagnostics) { + const level = d.severity === "error" ? "error" : "warning"; + console.log( + `::${level} file=${escapeProperty(d.file)},line=${d.line},col=${d.column},title=${escapeProperty(d.code)}::${escapeData(d.message)}`, + ); +} + +console.log( + `\n${summary.errors} errors, ${summary.warnings} warnings in ${summary.files} files`, +); + +const summaryPath = process.env.GITHUB_STEP_SUMMARY; +if (summaryPath) { + const rows = [...byRule.entries()] + .sort((a, b) => b[1].length - a[1].length) + .map(([code, items]) => `| \`${code}\` | ${items.length} |`); + appendFileSync( + summaryPath, + [ + "### Nimbus lint", + "", + `${summary.errors} errors, ${summary.warnings} warnings in ${summary.files} files`, + "", + "| Rule | Findings |", + "| --- | --- |", + ...(rows.length ? rows : ["| none | 0 |"]), + "", + ].join("\n"), + ); +} + +if (lintExitCode !== "0" || summary.errors > 0) { + console.log( + `::error::nimbus-docs lint found ${summary.errors} errors (exit code ${lintExitCode}). See the grouped log and job summary.`, + ); + process.exit(1); +} diff --git a/package.json b/package.json index ff5b67e6b50..d2d5ad379e4 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,7 @@ "flue:replay:spam": "tsx .flue/bin/replay-spam.ts", "flue:replay:comment-spam": "tsx .flue/bin/replay-comment-spam.ts", "lint": "eslint", + "lint:nimbus": "nimbus-docs lint", "generate:config-reference": "node bin/generate-cf-config-reference.mjs", "prepare": "husky", "prebuild:incremental": "pnpm run fetch:assets && pnpm run generate:config-reference", diff --git a/src/content/docs/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/index.mdx b/src/content/docs/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/index.mdx index 38d539f2595..76d8612016d 100644 --- a/src/content/docs/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/index.mdx +++ b/src/content/docs/cloudflare-one/networks/connectors/cloudflare-wan/configuration/appliance/network-options/application-based-policies/index.mdx @@ -11,9 +11,9 @@ import { DirectoryListing } from "~/components"; Standard traffic policies match on network-layer attributes like IP addresses and port ranges. Application-aware policies go further — they identify traffic by the application generating it, so you can make routing and security decisions based on what the traffic is, not just where it is going. -Cloudflare One Appliance (formerly Magic WAN Connector) classifies traffic using the same application categories used across Cloudflare's [Secure Web Gateway](/cloudflare-one/traffic-policies/). This means routing decisions on the Appliance and security policies in Gateway use the same application definitions. +Cloudflare One Appliance (formerly Magic WAN Connector) classifies traffic using the same application categories used across Cloudflare's [Secure Web Gateway](/cloudflare-one/traffic-controls/). This means routing decisions on the Appliance and security policies in Gateway use the same application definitions. -For the full list of recognized applications and categories, refer to [Applications and app types](/cloudflare-one/traffic-policies/application-app-types/). +For the full list of recognized applications and categories, refer to [Applications and app types](/cloudflare-one/traffic-controls/application-app-types/). With application-aware policies, you can: