From 405a95b4496b7e02651a84d70a6ca91dc836a89a Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Sat, 3 Oct 2026 00:49:56 -0500 Subject: [PATCH 01/10] ci: lint the docs and cookbook Markdown with redocly recheck --- .github/workflows/docs-tests.yaml | 18 +++++++++++++ docs/.redocly.recheck-baseline.yaml | 11 ++++++++ docs/redocly.yaml | 42 +++++++++++++++++++++++++++++ 3 files changed, 71 insertions(+) create mode 100644 docs/.redocly.recheck-baseline.yaml create mode 100644 docs/redocly.yaml diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 90ad325583..8dd74040b6 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -16,6 +16,24 @@ jobs: docs/**/*.md cookbook/**/*.md + recheck: + name: recheck + runs-on: blacksmith-4vcpu-ubuntu-2404 + env: + REDOCLY_TELEMETRY: off + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version: 24 + cache: npm + - name: Install dependencies + run: npm ci + - name: Compile + run: npm run compile + - name: Lint Markdown with recheck + run: node packages/cli/lib/index.js recheck docs cookbook --config docs/redocly.yaml --format=github-actions + vale: name: vale action runs-on: blacksmith-4vcpu-ubuntu-2404 diff --git a/docs/.redocly.recheck-baseline.yaml b/docs/.redocly.recheck-baseline.yaml new file mode 100644 index 0000000000..9561364e73 --- /dev/null +++ b/docs/.redocly.recheck-baseline.yaml @@ -0,0 +1,11 @@ +# Generated by `redocly recheck --generate-baseline`. Do not edit by hand. +version: 1 +files: + '@v1/rules/oas/no-ambiguous-paths.md': + recheck/no-multiple-space-blockquote: 4 + '@v1/rules/oas/no-identical-paths.md': + recheck/no-multiple-space-blockquote: 2 + '@v2/rules/oas/no-ambiguous-paths.md': + recheck/no-multiple-space-blockquote: 4 + '@v2/rules/oas/no-identical-paths.md': + recheck/no-multiple-space-blockquote: 2 diff --git a/docs/redocly.yaml b/docs/redocly.yaml new file mode 100644 index 0000000000..639c927d89 --- /dev/null +++ b/docs/redocly.yaml @@ -0,0 +1,42 @@ +# Markdown linting for the docs and cookbook, run by `redocly recheck` in CI. +# The rule settings mirror .markdownlint.yaml so both tools agree. +# This file lives here, not in the repository root, because a root redocly.yaml +# changes the CLI package's unit tests. +extends: + - recheck/markdown + - recheck/markdoc + +recheck: + markdoc: true + rules: + # The docs use semantic line breaks, so line length is not enforced. + recheck/line-length: off + recheck/ul-style: off + recheck/no-duplicate-heading: + severity: error + assertions: + no-duplicate-heading: + siblingsOnly: true + # Pages keep the title under a nested `seo.title`, which the default pattern does not match. + recheck/single-h1: + severity: error + assertions: + single-h1: + frontMatterTitle: '' + recheck/no-trailing-punctuation: + severity: error + assertions: + no-trailing-punctuation: + punctuation: '.,;:γ€‚οΌŒοΌ›:' + recheck/ol-prefix: off + recheck/list-marker-space: off + recheck/no-inline-html: off + recheck/no-bare-urls: off + recheck/no-emphasis-as-heading: off + # Pages start with metadata, not a heading. + recheck/first-line-h1: off + recheck/code-block-style: off + recheck/blanks-around-headings: off + recheck/blanks-around-fences: off + recheck/blanks-around-lists: off + recheck/no-multiple-blanks: off From 8d1bfaf630fc19900a4a4029b8be1c1727c5525c Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Sat, 3 Oct 2026 01:04:17 -0500 Subject: [PATCH 02/10] chore: port the monorepo recheck rules and PR summary comment to the docs workflow --- .github/workflows/docs-tests.yaml | 162 ++++++++++- cookbook/custom-plugins/sorting/README.md | 2 +- docs/.redocly.recheck-baseline.yaml | 11 - docs/@v1/rules/oas/no-ambiguous-paths.md | 12 +- docs/@v1/rules/oas/no-identical-paths.md | 6 +- docs/@v2/rules/oas/no-ambiguous-paths.md | 12 +- docs/@v2/rules/oas/no-identical-paths.md | 6 +- docs/redocly.yaml | 318 +++++++++++++++++++--- 8 files changed, 471 insertions(+), 58 deletions(-) delete mode 100644 docs/.redocly.recheck-baseline.yaml diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 8dd74040b6..b83868668a 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -19,10 +19,16 @@ jobs: recheck: name: recheck runs-on: blacksmith-4vcpu-ubuntu-2404 + permissions: + contents: read + pull-requests: write env: REDOCLY_TELEMETRY: off + RECHECK: node packages/cli/lib/index.js recheck --config docs/redocly.yaml steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 @@ -31,8 +37,160 @@ jobs: run: npm ci - name: Compile run: npm run compile - - name: Lint Markdown with recheck - run: node packages/cli/lib/index.js recheck docs cookbook --config docs/redocly.yaml --format=github-actions + + # Diff against the live merge-base of the PR head and the base branch. + # `github.sha` is the auto-merge commit on the current base tip, so a + # diff against the stale base sha would list files other PRs changed. + - name: Changed Markdown files + id: diff + run: | + git fetch origin ${{ github.base_ref }} --quiet + git diff --name-only origin/${{ github.base_ref }}...${{ github.event.pull_request.head.sha }} \ + | grep -E '^(docs|cookbook)/.*\.md$' > changed.txt || true + # A deleted file makes the command fail, so keep only files that still exist. + while read -r file; do [ -f "$file" ] && echo "$file"; done < changed.txt > docs-changed.txt + echo "Changed Markdown files: $(wc -l < docs-changed.txt)" + echo "count=$(wc -l < docs-changed.txt)" >> "$GITHUB_OUTPUT" + + # Annotates the changed files, every severity, and writes the summary the + # comment below reads. It never gates: the full-tree step does. + - name: Annotate changed files + id: recheck + if: steps.diff.outputs.count != '0' + continue-on-error: true + run: | + exit_code=0 + xargs $RECHECK --format=github-actions --summary json --summary-path recheck-summary.json < docs-changed.txt || exit_code=$? + echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" + exit $exit_code + + # Runs over the whole tree, not just changed files, because deleting a + # file or renaming a heading breaks links in files a PR never touched. + # Errors gate; warnings and info stay visible in the summary comment. + # --max-problems lists errors first, so the cap only trims the backlog + # of warnings and info from unchanged files. + - name: Full docs tree must stay error-free + run: $RECHECK docs cookbook --format=github-actions --max-problems 50 + + # Scores every changed file on this PR and on the merge-base so the + # comment can show the readability change per file. Both sides run from + # detached worktrees: the workspace is the auto-merge commit, so scoring + # it would blend base-branch edits into the "This PR" column. + - name: Readability of changed files + if: steps.diff.outputs.count != '0' + continue-on-error: true + run: | + score() { + git worktree add --detach "$1" "$2" + ( + cd "$1" + while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ + | xargs node "$GITHUB_WORKSPACE/packages/cli/lib/index.js" recheck --config docs/redocly.yaml --readability --format=json \ + > "$GITHUB_WORKSPACE/$3" + ) || true + } + score /tmp/head-tree ${{ github.event.pull_request.head.sha }} readability-head.json + score /tmp/base-tree "$(git merge-base origin/${{ github.base_ref }} ${{ github.event.pull_request.head.sha }})" readability-base.json + + # Posts one comment per PR and updates it on every run. A PR from a fork + # has a read-only token, so a failed comment does not fail the job. + - name: Summary comment + if: always() && steps.diff.outcome == 'success' + continue-on-error: true + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require('fs'); + const changedCount = Number('${{ steps.diff.outputs.count }}'); + const exitCode = '${{ steps.recheck.outputs.exit_code }}'; + const runUrl = '${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'; + + let summary = null; + try { + if (fs.existsSync('recheck-summary.json')) summary = JSON.parse(fs.readFileSync('recheck-summary.json', 'utf8')); + } catch (error) { + console.log('Failed to read summary file:', error.message); + } + + const lines = ['## πŸ“ Recheck Summary', '']; + if (changedCount === 0) { + lines.push('ℹ️ **No Markdown files changed** under `docs/` or `cookbook/`.'); + } else if (!summary) { + lines.push('πŸ”§ **Recheck failed to run** on the changed files.'); + lines.push(''); + lines.push(`Check the [workflow logs](${runUrl}) for details.`); + } else { + const { totalErrors = 0, totalWarnings = 0, totalInfo = 0, filesScanned = 0, breakdown = {} } = summary; + const statusEmoji = totalErrors > 0 ? '❌' : totalWarnings > 0 ? '⚠️' : 'βœ…'; + const statusMessage = totalErrors > 0 ? 'Errors found' : totalWarnings > 0 ? 'Warnings found' : 'All good!'; + lines.push(`${statusEmoji} **${statusMessage}** β€’ Scanned ${filesScanned} changed file(s)`); + lines.push(''); + if (totalErrors > 0 || totalWarnings > 0 || totalInfo > 0) { + lines.push(`- **${totalErrors}** error(s)`); + lines.push(`- **${totalWarnings}** warning(s)`); + lines.push(`- **${totalInfo}** info`); + lines.push(''); + lines.push('### πŸ“‹ Findings by rule'); + lines.push(''); + for (const [rule, counts] of Object.entries(breakdown)) { + const icons = [counts.errors > 0 && 'πŸ”΄', counts.warnings > 0 && '🟑', counts.info > 0 && 'πŸ”΅'].filter(Boolean).join(' '); + lines.push(`${icons} **${rule.replace('recheck/', '')}**: ${counts.errors} errors, ${counts.warnings} warnings, ${counts.info} info`); + } + lines.push(''); + lines.push('Errors fail the full-tree check. Warnings and info are a worklist and do not block the PR.'); + } else { + lines.push('πŸŽ‰ No findings in the changed files.'); + } + } + + // Readability of changed files, head versus merge-base. The table shows + // the Automated Readability Index: a grade level, so lower is easier. + const parseScores = (path) => { + const scores = {}; + if (!fs.existsSync(path)) return scores; + try { + for (const row of JSON.parse(fs.readFileSync(path, 'utf8')).files ?? []) { + if (row.automatedReadabilityIndex !== null) scores[row.file] = row.automatedReadabilityIndex; + } + } catch (error) { + console.log(`Could not parse ${path}:`, error.message); + } + return scores; + }; + const headScores = parseScores('readability-head.json'); + const baseScores = parseScores('readability-base.json'); + const rows = Object.keys(headScores).sort().map((file) => { + const before = baseScores[file]; + const after = headScores[file]; + const change = before === undefined + ? 'new file' + : Math.abs(after - before) < 0.05 + ? 'Β±0' + : after < before + ? `🟒 ${(after - before).toFixed(1)} easier` + : `πŸ”΄ +${(after - before).toFixed(1)} harder`; + return `| ${file} | ${before === undefined ? 'β€”' : before.toFixed(1)} | ${after.toFixed(1)} | ${change} |`; + }); + if (rows.length > 0) { + lines.push(''); + lines.push('### πŸ“– Readability of changed files'); + lines.push(''); + lines.push('Automated Readability Index (ARI): a U.S. grade level. Lower is easier to read.'); + lines.push(''); + lines.push('| File | Base | This PR | Change |'); + lines.push('|---|---:|---:|---:|'); + const MAX_ROWS = 20; + lines.push(...rows.slice(0, MAX_ROWS)); + if (rows.length > MAX_ROWS) lines.push(`| …and ${rows.length - MAX_ROWS} more files | | | |`); + } + + const tag = ''; + const { owner, repo, number } = context.issue; + const body = `${tag}\n${lines.join('\n')}`; + const comments = await github.rest.issues.listComments({ owner, repo, issue_number: number }); + const previous = comments.data.find((comment) => comment.body && comment.body.includes(tag)); + if (previous) await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body }); + else await github.rest.issues.createComment({ owner, repo, issue_number: number, body }); vale: name: vale action diff --git a/cookbook/custom-plugins/sorting/README.md b/cookbook/custom-plugins/sorting/README.md index 1e39a44cfa..9c67fb51ae 100644 --- a/cookbook/custom-plugins/sorting/README.md +++ b/cookbook/custom-plugins/sorting/README.md @@ -107,7 +107,7 @@ Your command will look something like the following example: redocly lint openapi.yaml ``` -If your OpenAPI doesn't fulfil the criteria in the configured rules, the details of the warnings/errors are shown in the output. +If your OpenAPI doesn't fulfill the criteria in the configured rules, the details of the warnings/errors are shown in the output. Adjust the rule configuration or severity levels to meet your needs, and let us know if there's some other rules you'd like to see included. diff --git a/docs/.redocly.recheck-baseline.yaml b/docs/.redocly.recheck-baseline.yaml deleted file mode 100644 index 9561364e73..0000000000 --- a/docs/.redocly.recheck-baseline.yaml +++ /dev/null @@ -1,11 +0,0 @@ -# Generated by `redocly recheck --generate-baseline`. Do not edit by hand. -version: 1 -files: - '@v1/rules/oas/no-ambiguous-paths.md': - recheck/no-multiple-space-blockquote: 4 - '@v1/rules/oas/no-identical-paths.md': - recheck/no-multiple-space-blockquote: 2 - '@v2/rules/oas/no-ambiguous-paths.md': - recheck/no-multiple-space-blockquote: 4 - '@v2/rules/oas/no-identical-paths.md': - recheck/no-multiple-space-blockquote: 2 diff --git a/docs/@v1/rules/oas/no-ambiguous-paths.md b/docs/@v1/rules/oas/no-ambiguous-paths.md index 48ea884ae4..ba17147dde 100644 --- a/docs/@v1/rules/oas/no-ambiguous-paths.md +++ b/docs/@v1/rules/oas/no-ambiguous-paths.md @@ -11,13 +11,17 @@ According to the OpenAPI specification: > The following paths are considered identical and invalid: > -> /pets/{petId} -> /pets/{name} +> ```text +> /pets/{petId} +> /pets/{name} +> ``` > > The following may lead to ambiguous resolution: > -> /{entity}/me -> /books/{id} +> ```text +> /{entity}/me +> /books/{id} +> ``` | OAS | Compatibility | | --- | ------------- | diff --git a/docs/@v1/rules/oas/no-identical-paths.md b/docs/@v1/rules/oas/no-identical-paths.md index 90b1d138c2..6c75d82f2a 100644 --- a/docs/@v1/rules/oas/no-identical-paths.md +++ b/docs/@v1/rules/oas/no-identical-paths.md @@ -16,8 +16,10 @@ According to the OpenAPI specification: > The following paths are considered identical and invalid: > -> /pets/{petId} -> /pets/{name} +> ```text +> /pets/{petId} +> /pets/{name} +> ``` ```mermaid flowchart TD diff --git a/docs/@v2/rules/oas/no-ambiguous-paths.md b/docs/@v2/rules/oas/no-ambiguous-paths.md index e38e689cd7..13dc0f09fe 100644 --- a/docs/@v2/rules/oas/no-ambiguous-paths.md +++ b/docs/@v2/rules/oas/no-ambiguous-paths.md @@ -11,13 +11,17 @@ According to the OpenAPI specification: > The following paths are considered identical and invalid: > -> /pets/{petId} -> /pets/{name} +> ```text +> /pets/{petId} +> /pets/{name} +> ``` > > The following may lead to ambiguous resolution: > -> /{entity}/me -> /books/{id} +> ```text +> /{entity}/me +> /books/{id} +> ``` | OAS | Compatibility | | --- | ------------- | diff --git a/docs/@v2/rules/oas/no-identical-paths.md b/docs/@v2/rules/oas/no-identical-paths.md index 3fdece9353..4eb8316aa5 100644 --- a/docs/@v2/rules/oas/no-identical-paths.md +++ b/docs/@v2/rules/oas/no-identical-paths.md @@ -17,8 +17,10 @@ According to the OpenAPI specification: > The following paths are considered identical and invalid: > -> /pets/{petId} -> /pets/{name} +> ```text +> /pets/{petId} +> /pets/{name} +> ``` ```mermaid flowchart TD diff --git a/docs/redocly.yaml b/docs/redocly.yaml index 639c927d89..6a42b62b3c 100644 --- a/docs/redocly.yaml +++ b/docs/redocly.yaml @@ -1,42 +1,296 @@ -# Markdown linting for the docs and cookbook, run by `redocly recheck` in CI. -# The rule settings mirror .markdownlint.yaml so both tools agree. -# This file lives here, not in the repository root, because a root redocly.yaml -# changes the CLI package's unit tests. +# Markdown linting for docs/ and cookbook/, run by `redocly recheck` in CI +# (.github/workflows/docs-tests.yaml). This file lives here, not in the +# repository root, because a root redocly.yaml changes the CLI package's unit +# tests. The rules are ported from the Redocly monorepo's recheck.yaml, with +# severities set from a measurement of this tree on 2026-10-03: a rule at zero +# findings gates at error, a rule with a backlog reports at warn until the +# backlog is cleared, then steps up to error. extends: - recheck/markdown - recheck/markdoc + - recheck/plain-language + - recheck/technical-english + - recheck/inclusive-language recheck: markdoc: true rules: - # The docs use semantic line breaks, so line length is not enforced. - recheck/line-length: off - recheck/ul-style: off - recheck/no-duplicate-heading: + # -- Ported from .markdownlint.yaml so the two tools agree -- + recheck/line-length: off # MD013; readable-line-length below governs + recheck/ul-style: off # MD004; bullet-style-dash below governs + recheck/ol-prefix: off # MD029 + recheck/list-marker-space: off # MD030 + recheck/no-inline-html: off # MD033 + recheck/no-bare-urls: off # MD034 + recheck/no-emphasis-as-heading: off # MD036 + recheck/first-line-h1: off # MD041 + recheck/code-block-style: off # MD046 + recheck/blanks-around-headings: off # blank_lines + recheck/blanks-around-fences: off # blank_lines + recheck/blanks-around-lists: off # blank_lines + recheck/no-multiple-blanks: off # blank_lines + # MD024 siblings_only: no-duplicate-headings below runs the same assertion + # with respectSections, so duplicates report once. + recheck/no-duplicate-heading: off + # no-broken-fragment-links below runs the same assertion across files. + recheck/link-fragments: off + recheck/single-h1: # MD025 front_matter_title: '' severity: error + assertions: + single-h1: { frontMatterTitle: '' } + recheck/no-trailing-punctuation: # MD026 + severity: error + assertions: + no-trailing-punctuation: { punctuation: '.,;:γ€‚οΌŒοΌ›:' } + + # -- recheck/plain-language: the word-choice and structure rules at zero + # findings gate at error. excess-intensifiers (36) and + # paragraph-sentence-count (9) stay at the preset's warn until cleared. -- + plain-language/filler-phrases: error + plain-language/an-amount-of: error + plain-language/complex-words: error + plain-language/redundant-pairs: error + plain-language/double-negative-phrases: error + plain-language/jargon-terms: error + plain-language/double-negative-patterns: error + plain-language/paragraph-max-words: error + + # -- recheck/technical-english stays at the preset defaults: sentence-length + # (425) and paragraph-length (18) at warn as the visible worklist, + # passive-voice (1157) at info. -- + + recheck/us-spelling: + severity: error + message: 'Use the US spelling "%s" instead of British "%s".' + link: https://docs.microsoft.com/en-us/style-guide/word-choice/use-us-spelling-avoid-non-english-words + assertions: + swap: + ignoreCase: true + wordBoundary: true + pairs: + aeon: eon + aeroplane: airplane + ageing: aging + aluminium: aluminum + anaemia: anemia + anaesthesia: anesthesia + analyse: analyze + annexe: annex + apologise: apologize + behaviour: behavior + busses: buses + calibre: caliber + cancelled: canceled + cancellation: cancelation + catalogue: catalog + categorise: categorize + categorised: categorized + categorises: categorizes + categorising: categorizing + centre: center + cheque: check + civilisation: civilization + civilise: civilize + colour: color + cosy: cozy + cypher: cipher + dependant: dependent + defence: defense + distil: distill + draught: draft + encyclopaedia: encyclopedia + enquiry: inquiry + enrol: enroll + enrolment: enrollment + enthral: enthrall + expiry: expiration + favourite: favorite + fibre: fiber + fillet: filet + flavour: flavor + furore: furor + fulfil: fulfill + gaol: jail + humour: humor + honour: honor + initialled: initialed + initialling: initialing + instil: instill + jewellery: jewelry + labelling: labeling + labelled: labeled + labour: labor + libellous: libelous + licence: license + likeable: likable + liveable: livable + lustre: luster + manoeuvre: maneuver + marvellous: marvelous + matt: matte + meagre: meager + metre: meter + modelling: modeling + moustache: mustache + neighbour: neighbor + normalise: normalize + offence: offense + organise: organize + organisation: organization + orientated: oriented + paralyse: paralyze + plough: plow + pretence: pretense + programme: program + pyjamas: pajamas + rateable: ratable + realise: realize + recognise: recognize + reconnoitre: reconnoiter + rumour: rumor + sabre: saber + saleable: salable + saltpetre: saltpeter + sceptic: skeptic + sepulchre: sepulcher + signalling: signaling + sizeable: sizable + skilful: skillful + sombre: somber + smoulder: smolder + speciality: specialty + spectre: specter + splendour: splendor + standardise: standardize + standardised: standardized + sulphur: sulfur + theatre: theater + travelled: traveled + traveller: traveler + travelling: traveling + unshakeable: unshakable + wilful: willful + yoghurt: yogurt + + recheck/inclusion-gender-culture: + severity: error + message: 'Use inclusive language. Consider "%s" instead of "%s".' + assertions: + swap: + keysAreRegex: true + wordBoundary: true + ignoreCase: true + pairs: + he: they + his: their + she: they + hers: their + 'blacklist(?:ed|ing|s)?': blocklist + 'whitelist(?:ed|ing|s)?': allowlist + master: primary + slave: replica + he/she: they + s/he: they + exceptions: + lines: + # 'master' as a git branch segment inside URLs we do not control. + - '/master/' + + # Four headings still start with a gerund; warn until they are reworded. + recheck/no-gerund-headings: + severity: warn + scope: heading + message: 'Do not start headings with a gerund.' + assertions: + pattern: + ignoreCase: true + tokens: + - "^\\w*ing\\b" + exceptions: + lines: + - Breaking + - Contributing + - Monitoring + - Naming + - Routing + - Settings + - String + - Testing + - Training + - Troubleshooting + + recheck/oxford-comma: + severity: error + scope: sentence + message: "Use the Oxford comma in '%s'." + assertions: + pattern: + tokens: + - '(?:[^\s,]+,){1,} \w+ (?:and|or) \w+[.?!]' + + # 22 lines over the limit; warn until they are split. + recheck/readable-line-length: + severity: warn + message: 'Keep lines under %s characters.' + assertions: + line-length: + lineLength: 400 + codeBlocks: false + tables: false + + recheck/no-trailing-spaces: + severity: error + message: 'Remove trailing spaces.' + assertions: + no-trailing-spaces: + codeBlocks: false + + recheck/bullet-style-dash: + severity: error + message: "Use '-' for unordered list bullets." + assertions: + ul-style: + style: dash + + # 628 findings; warn until the docs are rewrapped one sentence per line. + recheck/semantic-line-breaks: + severity: warn + message: 'Use semantic line breaks (%s mode).' + assertions: + semantic-line-breaks: + mode: sentence + ignoreCodeBlocks: true + ignoreTables: true + + recheck/no-hard-tabs: + severity: error + message: 'Use spaces instead of tabs.' + assertions: + no-hard-tabs: + codeBlocks: false + spacesPerTab: 2 + + recheck/no-duplicate-headings: + severity: error + message: 'Duplicate heading "%s".' assertions: no-duplicate-heading: - siblingsOnly: true - # Pages keep the title under a nested `seo.title`, which the default pattern does not match. - recheck/single-h1: - severity: error - assertions: - single-h1: - frontMatterTitle: '' - recheck/no-trailing-punctuation: - severity: error - assertions: - no-trailing-punctuation: - punctuation: '.,;:γ€‚οΌŒοΌ›:' - recheck/ol-prefix: off - recheck/list-marker-space: off - recheck/no-inline-html: off - recheck/no-bare-urls: off - recheck/no-emphasis-as-heading: off - # Pages start with metadata, not a heading. - recheck/first-line-h1: off - recheck/code-block-style: off - recheck/blanks-around-headings: off - recheck/blanks-around-fences: off - recheck/blanks-around-lists: off - recheck/no-multiple-blanks: off + caseSensitive: false + ignoreCommonHeadings: false + respectSections: true + + recheck/no-broken-fragment-links: + severity: error + message: 'Broken link fragment: %s' + assertions: + link-fragments: + crossFile: true + + # Three images are over the limit; warn until they are compressed. + recheck/optimize-images: + severity: warn + message: 'Image too large: %s' + assertions: + max-image-size: + maxSizeKB: 100 + extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'] From 125f819cc5b058461334740d4e990d3d96db3d98 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Sat, 3 Oct 2026 07:46:10 -0500 Subject: [PATCH 03/10] chore: read the readability config from the workspace so the merge-base run can score files --- .github/workflows/docs-tests.yaml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index b83868668a..801bdc42f6 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -75,7 +75,8 @@ jobs: # Scores every changed file on this PR and on the merge-base so the # comment can show the readability change per file. Both sides run from # detached worktrees: the workspace is the auto-merge commit, so scoring - # it would blend base-branch edits into the "This PR" column. + # it would blend base-branch edits into the "This PR" column. Both read + # the workspace config, because the merge-base may not have one. - name: Readability of changed files if: steps.diff.outputs.count != '0' continue-on-error: true @@ -85,7 +86,7 @@ jobs: ( cd "$1" while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ - | xargs node "$GITHUB_WORKSPACE/packages/cli/lib/index.js" recheck --config docs/redocly.yaml --readability --format=json \ + | xargs node "$GITHUB_WORKSPACE/packages/cli/lib/index.js" recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ > "$GITHUB_WORKSPACE/$3" ) || true } From 27307d0b8a21d7c183460ce944f828fe47d298a6 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Mon, 5 Oct 2026 11:29:32 -0500 Subject: [PATCH 04/10] chore(ci): run the released CLI in the docs recheck job and add the reading-time budget The recheck job runs @redocly/cli@2.58.1 with npx. It does not install or compile the repository now. Add recheck/page-reading-time for docs and cookbook at warn. Three pages are over the 15-minute budget. The recheck docs from main show British spellings in swap-pair examples. Add line exceptions to recheck/us-spelling for these pairs. Update the measurement date and the technical-english counts. --- .github/workflows/docs-tests.yaml | 9 ++------- docs/redocly.yaml | 27 ++++++++++++++++++++++++--- 2 files changed, 26 insertions(+), 10 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 801bdc42f6..c2c451d2a0 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -24,7 +24,7 @@ jobs: pull-requests: write env: REDOCLY_TELEMETRY: off - RECHECK: node packages/cli/lib/index.js recheck --config docs/redocly.yaml + RECHECK: npx --yes @redocly/cli@2.58.1 recheck --config docs/redocly.yaml steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -32,11 +32,6 @@ jobs: - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 - cache: npm - - name: Install dependencies - run: npm ci - - name: Compile - run: npm run compile # Diff against the live merge-base of the PR head and the base branch. # `github.sha` is the auto-merge commit on the current base tip, so a @@ -86,7 +81,7 @@ jobs: ( cd "$1" while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ - | xargs node "$GITHUB_WORKSPACE/packages/cli/lib/index.js" recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ + | xargs npx --yes @redocly/cli@2.58.1 recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ > "$GITHUB_WORKSPACE/$3" ) || true } diff --git a/docs/redocly.yaml b/docs/redocly.yaml index 6a42b62b3c..7da6cb8de7 100644 --- a/docs/redocly.yaml +++ b/docs/redocly.yaml @@ -2,7 +2,7 @@ # (.github/workflows/docs-tests.yaml). This file lives here, not in the # repository root, because a root redocly.yaml changes the CLI package's unit # tests. The rules are ported from the Redocly monorepo's recheck.yaml, with -# severities set from a measurement of this tree on 2026-10-03: a rule at zero +# severities set from a measurement of this tree on 2026-10-05: a rule at zero # findings gates at error, a rule with a backlog reports at warn until the # backlog is cleared, then steps up to error. extends: @@ -56,8 +56,8 @@ recheck: plain-language/paragraph-max-words: error # -- recheck/technical-english stays at the preset defaults: sentence-length - # (425) and paragraph-length (18) at warn as the visible worklist, - # passive-voice (1157) at info. -- + # (446) and paragraph-length (18) at warn as the visible worklist, + # passive-voice (1207) at info. -- recheck/us-spelling: severity: error @@ -171,6 +171,14 @@ recheck: unshakeable: unshakable wilful: willful yoghurt: yogurt + exceptions: + lines: + # Swap-pair examples in the recheck docs show British spellings on purpose. + - 'colour: color' + - 'color: colour' + - 'behaviour: behavior' + - 'behavior: behaviour' + - 'organise: organize' recheck/inclusion-gender-culture: severity: error @@ -294,3 +302,16 @@ recheck: max-image-size: maxSizeKB: 100 extensions: ['jpg', 'jpeg', 'png', 'gif', 'webp', 'svg'] + + # On 2026-10-05, three pages are over 15 minutes at 200 words per minute: + # both changelogs and guides/use-generated-client.md. Warn until they are split. + recheck/page-reading-time: + severity: warn + message: 'Page %s is %s minutes; at most 15 expected. Split it, or move reference material to its own page.' + appliesTo: + - 'docs/**' + - 'cookbook/**' + assertions: + metric: + formula: reading-time + max: 15 From c9edaa0ba03d3eb2d84866dda96b4b9e589aba21 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Mon, 5 Oct 2026 11:32:31 -0500 Subject: [PATCH 05/10] fix(ci): run npx outside the workspace in the docs recheck job The repository root declares @redocly/cli as a workspace at version 2.58.1. In a checkout without node_modules, npx takes that workspace package as installed. It does not download the release, and the call fails with "redocly: command not found". The --prefix /tmp flag makes npx resolve the pinned release. --- .github/workflows/docs-tests.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index c2c451d2a0..081593fe9e 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -24,7 +24,11 @@ jobs: pull-requests: write env: REDOCLY_TELEMETRY: off - RECHECK: npx --yes @redocly/cli@2.58.1 recheck --config docs/redocly.yaml + # The `--prefix /tmp` flag keeps npx outside the repository root, which + # declares @redocly/cli as a workspace. When the workspace version equals + # the pin, npx does not download the release and fails with + # "redocly: command not found". + RECHECK: npx --prefix /tmp --yes @redocly/cli@2.58.1 recheck --config docs/redocly.yaml steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -81,7 +85,7 @@ jobs: ( cd "$1" while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ - | xargs npx --yes @redocly/cli@2.58.1 recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ + | xargs npx --prefix /tmp --yes @redocly/cli@2.58.1 recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ > "$GITHUB_WORKSPACE/$3" ) || true } From bd3c4698b48b3a7b7ffc82d4aab0d1ae21f7fedb Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Mon, 5 Oct 2026 11:40:03 -0500 Subject: [PATCH 06/10] docs(cookbook): link the Spectral OWASP ruleset repository instead of the retired blog post --- cookbook/rulesets/security/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cookbook/rulesets/security/README.md b/cookbook/rulesets/security/README.md index 1f34c77c92..a5965c30e5 100644 --- a/cookbook/rulesets/security/README.md +++ b/cookbook/rulesets/security/README.md @@ -192,4 +192,4 @@ Pick a limit that's generous for the size of the data that you expect, but small ## References - [OWASP top ten](https://owasp.org/Top10/) -- Published linting rulesets from [Spectral](https://blog.stoplight.io/spectral-owasp-api-2023-security-ruleset) and [Vacuum](https://quobix.com/vacuum/rules/owasp/) +- Published linting rulesets from [Spectral](https://github.com/stoplightio/spectral-owasp-ruleset) and [Vacuum](https://quobix.com/vacuum/rules/owasp/) From 85e1e43400de4d225608c8075129f4b87e660ef5 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Mon, 5 Oct 2026 14:15:18 -0500 Subject: [PATCH 07/10] fix(ci): paginate PR comments when looking up the recheck summary comment --- .github/workflows/docs-tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 081593fe9e..23ae1ba958 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -187,8 +187,8 @@ jobs: const tag = ''; const { owner, repo, number } = context.issue; const body = `${tag}\n${lines.join('\n')}`; - const comments = await github.rest.issues.listComments({ owner, repo, issue_number: number }); - const previous = comments.data.find((comment) => comment.body && comment.body.includes(tag)); + const comments = await github.paginate(github.rest.issues.listComments, { owner, repo, issue_number: number, per_page: 100 }); + const previous = comments.find((comment) => comment.body && comment.body.includes(tag)); if (previous) await github.rest.issues.updateComment({ owner, repo, comment_id: previous.id, body }); else await github.rest.issues.createComment({ owner, repo, issue_number: number, body }); From 01cbe684906db625c8120a5571ceaf2e1b1f4ae1 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Tue, 6 Oct 2026 07:49:52 -0500 Subject: [PATCH 08/10] chore(ci): run the latest released CLI in the docs recheck job --- .github/workflows/docs-tests.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 23ae1ba958..bc668fbde7 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -26,9 +26,9 @@ jobs: REDOCLY_TELEMETRY: off # The `--prefix /tmp` flag keeps npx outside the repository root, which # declares @redocly/cli as a workspace. When the workspace version equals - # the pin, npx does not download the release and fails with + # the latest release, npx does not download it and fails with # "redocly: command not found". - RECHECK: npx --prefix /tmp --yes @redocly/cli@2.58.1 recheck --config docs/redocly.yaml + RECHECK: npx --prefix /tmp --yes @redocly/cli@latest recheck --config docs/redocly.yaml steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: From bbd44bc237faa603cc30c86733e3d21b49cdf917 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Tue, 6 Oct 2026 07:54:36 -0500 Subject: [PATCH 09/10] fix(ci): run every docs recheck step through the same CLI invocation --- .github/workflows/docs-tests.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index bc668fbde7..651e374fdc 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -28,7 +28,7 @@ jobs: # declares @redocly/cli as a workspace. When the workspace version equals # the latest release, npx does not download it and fails with # "redocly: command not found". - RECHECK: npx --prefix /tmp --yes @redocly/cli@latest recheck --config docs/redocly.yaml + RECHECK: npx --prefix /tmp --yes @redocly/cli@latest recheck steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -59,7 +59,7 @@ jobs: continue-on-error: true run: | exit_code=0 - xargs $RECHECK --format=github-actions --summary json --summary-path recheck-summary.json < docs-changed.txt || exit_code=$? + xargs $RECHECK --config docs/redocly.yaml --format=github-actions --summary json --summary-path recheck-summary.json < docs-changed.txt || exit_code=$? echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" exit $exit_code @@ -69,7 +69,7 @@ jobs: # --max-problems lists errors first, so the cap only trims the backlog # of warnings and info from unchanged files. - name: Full docs tree must stay error-free - run: $RECHECK docs cookbook --format=github-actions --max-problems 50 + run: $RECHECK --config docs/redocly.yaml docs cookbook --format=github-actions --max-problems 50 # Scores every changed file on this PR and on the merge-base so the # comment can show the readability change per file. Both sides run from @@ -85,7 +85,7 @@ jobs: ( cd "$1" while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ - | xargs npx --prefix /tmp --yes @redocly/cli@2.58.1 recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ + | xargs $RECHECK --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ > "$GITHUB_WORKSPACE/$3" ) || true } From ee796a94edf5478501278ba1b14bc8256ae50376 Mon Sep 17 00:00:00 2001 From: Adam Altman Date: Tue, 6 Oct 2026 13:59:43 -0500 Subject: [PATCH 10/10] ci(docs): install the local CLI pack in the docs recheck job The recheck job now builds the repository and installs the local pack, as the smoke workflows do. A PR that changes a recheck rule now shows its effect on the docs in the same run. The RECHECK variable and the npx --prefix /tmp workaround are no longer necessary. --- .github/workflows/docs-tests.yaml | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/.github/workflows/docs-tests.yaml b/.github/workflows/docs-tests.yaml index 651e374fdc..62fa7b6ec9 100644 --- a/.github/workflows/docs-tests.yaml +++ b/.github/workflows/docs-tests.yaml @@ -24,11 +24,6 @@ jobs: pull-requests: write env: REDOCLY_TELEMETRY: off - # The `--prefix /tmp` flag keeps npx outside the repository root, which - # declares @redocly/cli as a workspace. When the workspace version equals - # the latest release, npx does not download it and fails with - # "redocly: command not found". - RECHECK: npx --prefix /tmp --yes @redocly/cli@latest recheck steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: @@ -36,6 +31,14 @@ jobs: - uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 24 + cache: npm + - name: Install dependencies + run: npm ci + - name: Pack and install the CLI + run: | + npm run pack:prepare + npm i -g ./redocly-cli.tgz + redocly --version # Diff against the live merge-base of the PR head and the base branch. # `github.sha` is the auto-merge commit on the current base tip, so a @@ -59,7 +62,7 @@ jobs: continue-on-error: true run: | exit_code=0 - xargs $RECHECK --config docs/redocly.yaml --format=github-actions --summary json --summary-path recheck-summary.json < docs-changed.txt || exit_code=$? + xargs redocly recheck --config docs/redocly.yaml --format=github-actions --summary json --summary-path recheck-summary.json < docs-changed.txt || exit_code=$? echo "exit_code=$exit_code" >> "$GITHUB_OUTPUT" exit $exit_code @@ -69,7 +72,7 @@ jobs: # --max-problems lists errors first, so the cap only trims the backlog # of warnings and info from unchanged files. - name: Full docs tree must stay error-free - run: $RECHECK --config docs/redocly.yaml docs cookbook --format=github-actions --max-problems 50 + run: redocly recheck --config docs/redocly.yaml docs cookbook --format=github-actions --max-problems 50 # Scores every changed file on this PR and on the merge-base so the # comment can show the readability change per file. Both sides run from @@ -85,7 +88,7 @@ jobs: ( cd "$1" while read -r file; do [ -f "$file" ] && echo "$file"; done < "$GITHUB_WORKSPACE/docs-changed.txt" \ - | xargs $RECHECK --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ + | xargs redocly recheck --config "$GITHUB_WORKSPACE/docs/redocly.yaml" --readability --format=json \ > "$GITHUB_WORKSPACE/$3" ) || true }