Skip to content
Merged
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
135 changes: 95 additions & 40 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,44 @@ concurrency:
cancel-in-progress: true

jobs:
classify:
name: Classify changed paths
runs-on: ubuntu-24.04
timeout-minutes: 5
outputs:
document_only: ${{ steps.classify.outputs.document_only }}
documents_b64: ${{ steps.classify.outputs.documents_b64 }}
steps:
- name: Check out repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- name: Classify changed documents
id: classify
run:
node --input-type=module -e "import { runClassifier } from
'./scripts/lib/content-only-ci.mjs'; runClassifier()"
env:
GITHUB_EVENT_NAME: ${{ github.event_name }}
PR_BASE_SHA: ${{ github.event.pull_request.base.sha }}
MERGE_BASE_SHA: ${{ github.event.merge_group.base_sha }}

validate:
name: Validate repository
needs: classify
outputs:
document_only:
${{ needs.classify.result == 'success' &&
needs.classify.outputs.document_only == 'true' }}
if: ${{ !cancelled() }}
env:
FULL_VALIDATION:
${{ needs.classify.result != 'success' ||
needs.classify.outputs.document_only != 'true' }}
CONTENT_ONLY:
${{ needs.classify.result == 'success' &&
needs.classify.outputs.document_only == 'true' }}
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
Expand All @@ -30,8 +66,10 @@ jobs:
- name: Set up Node.js and install dependencies
uses: ./.github/actions/setup-node-deps
- name: Audit dependencies against the advisory allowlist
if: ${{ !cancelled() && env.FULL_VALIDATION == 'true' }}
run: npm run check:audit
- name: Run unit tests with coverage
if: ${{ !cancelled() && env.FULL_VALIDATION == 'true' }}
# A zone fourteen hours ahead of UTC, so a date formatted in the
# host zone instead of UTC lands on a different calendar day for most
# of the day; the suite used to pin TZ=UTC, which hid exactly that.
Expand All @@ -40,49 +78,47 @@ jobs:
run: npm run test:unit:coverage:check | tee coverage-summary.txt
shell: bash -o pipefail {0}
- name: Publish coverage summary
if: always()
if: ${{ !cancelled() && env.FULL_VALIDATION == 'true' }}
run: |
if [ -f coverage-summary.txt ]; then
{ echo '## Unit test coverage'; echo '```'; cat coverage-summary.txt; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
fi
- name: Upload coverage report
if: always()
if: ${{ !cancelled() && env.FULL_VALIDATION == 'true' }}
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-summary
path: coverage-summary.txt
retention-days: 90
- name: Validate metrics data
run: npm run validate:metrics
- name: Validate architectures data
run: npm run validate:architectures
- name: Validate architecture assets
run: npm run validate:architecture-assets
- name: Validate awards data
run: npm run validate:awards
- name: Validate community people data
run: npm run validate:community-people
- name: Validate community group links
run: npm run validate:community-groups
- name: Validate case studies data
run: npm run validate:case-studies
- name: Validate radar reports data
run: npm run validate:radar-reports
- name: Validate projects-born data
run: npm run validate:projects-born
- name: Validate End User talks data
run: npm run validate:end-user-talks
- name: Validate button contrast
run: npm run validate:button-contrast
- name: Validate End User landscape data
run: npm run validate:enduser-members
- name: Validate members data
run: npm run validate:members
- name: Validate repository data
if: ${{ !cancelled() && env.FULL_VALIDATION == 'true' }}
run: |
npm run validate:metrics
npm run validate:architectures
npm run validate:architecture-assets
npm run validate:awards
npm run validate:community-people
npm run validate:community-groups
npm run validate:case-studies
npm run validate:radar-reports
npm run validate:projects-born
npm run validate:end-user-talks
npm run validate:button-contrast
npm run validate:enduser-members
npm run validate:members
- name: Check content contracts
if: ${{ !cancelled() && env.CONTENT_ONLY == 'true' }}
run: >-
node --test tests/docs-contract.test.mjs
tests/markdown-references.test.mjs
tests/community-group-coverage.test.mjs
- name: Build production site
run: npm run build:production

lint:
name: Lint repository
needs: classify
if: ${{ !cancelled() }}
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
Expand All @@ -92,21 +128,35 @@ jobs:
persist-credentials: false
- name: Set up Node.js and install dependencies
uses: ./.github/actions/setup-node-deps
- name: Check formatting
run: npm run check:format
- name: Check spelling
run: npm run check:spelling
- name: Check markdown
run: npm run check:markdown
- name: Check duplication
run: npm run check:duplication
- name: Lint JavaScript
run: npm run check:lint
- name: Check unused code and dependencies
run: npm run check:unused
- name: Check changed-document formatting, spelling, and Markdown
if:
${{ !cancelled() && needs.classify.result == 'success' &&
needs.classify.outputs.document_only == 'true' }}
run:
node --input-type=module -e "import { runDocumentLint } from
'./scripts/lib/content-only-ci.mjs';
runDocumentLint(process.env.CHANGED_DOCUMENTS_B64)"
env:
CHANGED_DOCUMENTS_B64: ${{ needs.classify.outputs.documents_b64 }}
- name: Run full repository lint
if:
${{ !cancelled() && (needs.classify.result != 'success' ||
needs.classify.outputs.document_only != 'true') }}
run: |
npm run check:format
npm run check:spelling
npm run check:markdown
npm run check:duplication
npm run check:lint
npm run check:unused

e2e:
name: End-to-end tests
needs: [validate, lint]
if: >-
${{ !cancelled() && (needs.validate.result != 'success' ||
needs.lint.result != 'success' || needs.validate.outputs.document_only !=
'true') }}
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
Expand Down Expand Up @@ -146,6 +196,11 @@ jobs:

e2e-coverage:
name: End-to-end coverage
needs: [classify, validate, lint]
if: >-
${{ !cancelled() && (needs.classify.result != 'success' ||
needs.classify.outputs.document_only != 'true' || needs.validate.result !=
'success' || needs.lint.result != 'success') }}
runs-on: ubuntu-24.04
timeout-minutes: 20
env:
Expand Down
51 changes: 31 additions & 20 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -64,17 +64,19 @@ These recipes are a superset of the validation steps the "Refresh metrics" and
before the Netlify build. The recipes are **not** the same checks the "Validate
repository" and "Lint repository" PR gates run, and a green
`just build`/`just refresh` locally does **not** imply a green PR check. Before
opening a PR, also run `npm run test:unit:coverage:check` and the local check
scripts that match "Lint repository": `npm run check:format`,
`npm run check:spelling`, `npm run check:markdown`, `npm run check:duplication`,
`npm run check:lint` and `npm run check:unused`. The "Validate repository" gate
additionally runs `npm run check:audit` (dependency advisory allowlist) — see
step 5 below. `npm run check` also runs the lint checks, but it additionally
runs `check:audit`, `check:links` (network-dependent) and
`check:community-group-links` (needs `GH_TOKEN` and rewrites
`data/community-groups.json`) — see step 5 below before running the full
`npm run check`. `just` is optional — the npm scripts remain the canonical
interface and work without it.
For PRs that use the full-validation path, also run
`npm run test:unit:coverage:check` and the local check scripts that match "Lint
repository": `npm run check:format`, `npm run check:spelling`,
`npm run check:markdown`, `npm run check:duplication`, `npm run check:lint` and
`npm run check:unused`. Content-only PRs do not need to run the full unit
coverage or lint suites locally; CI runs focused checks on the changed documents
and builds the site. The full "Validate repository" gate also runs
`npm run check:audit` (dependency advisory allowlist) — see step 5 below.
`npm run check` runs the lint checks, but it additionally runs `check:audit`,
`check:links` (network-dependent) and `check:community-group-links` (needs
`GH_TOKEN` and rewrites `data/community-groups.json`) — see step 5 below before
running the full `npm run check`. `just` is optional — the npm scripts remain
the canonical interface and work without it.

## Content audience

Expand Down Expand Up @@ -272,25 +274,34 @@ enough for a first contribution.

## Making changes

Pull requests that change only recognized, human-maintained Markdown or MDX
documents use focused CI: formatting, spelling, and Markdown checks run on the
changed documents, document-contract tests run, and the production site is
built. Generated or data-backed pages, code, configuration, assets, mixed
changes, and unrecognized paths keep the full validation, lint, and browser
suites. If CI cannot confidently classify a change, it uses the full path.

1. Fork the repository and create a branch from `main`.
2. Make your change and run the relevant validation script.
3. Run `npm run test:unit:coverage:check` — the required "Validate repository"
check runs this on every PR, and it fails the build if unit-test coverage
3. For PRs that use the full-validation path, run
`npm run test:unit:coverage:check`. This check fails if unit-test coverage
drops below its thresholds (99% lines / 100% source / 96% regions / 100%
source regions). The harness under `tests/tools/` is floored separately (100%
lines / 99% regions, and 98% regions per harness file): it is measured like
source but reported in its own `harness files` row, so a gap in the test
apparatus cannot be paid for out of `src/`'s slack. `npm run test:unit` alone
skips the coverage gate, so a green `test:unit` locally does not guarantee a
green PR check. If you changed a file under `src/` or `scripts/lib/`, also
run its mutation tests — see [Mutation testing](#mutation-testing) below.
skips the coverage gate. Content-only PRs use focused document checks
instead. If you changed a file under `src/` or `scripts/lib/`, also run its
mutation tests — see [Mutation testing](#mutation-testing) below.
4. Verify with `npm run build` before opening a PR. If your change touches
pages, components, or navigation, also run the end-to-end suite — see
[End-to-end tests](#end-to-end-tests) below.
5. Run `npm run check:format`, `npm run check:spelling`,
`npm run check:markdown`, `npm run check:duplication`, `npm run check:lint`
and `npm run check:unused` — the required "Lint repository" check runs these
six on every PR. If a check fails:
5. For PRs that use the full-validation path, run `npm run check:format`,
`npm run check:spelling`, `npm run check:markdown`,
`npm run check:duplication`, `npm run check:lint` and `npm run check:unused`.
The "Lint repository" check runs these six on that path. Content-only PRs use
focused formatting, spelling and Markdown checks on changed documents. If a
full-path check fails:
- `check:format` — run `npm run fix:format` to auto-format.
- `check:spelling` — add the flagged term to `.cspell.yml` if it's a
legitimate project word, or fix the typo.
Expand Down
120 changes: 120 additions & 0 deletions adr/0011-content-focused-validation-for-documentation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
# ADR 0011: Content-only pull requests use focused validation

- **Status**: Accepted
- **Date**: 2026-10-29
- **Related**:
[ADR 0008](./0008-scope-agent-findings-to-user-observable-change.md),
[ADR 0009](./0009-refresh-generated-data-only-on-content-change.md),
[ADR 0010](./0010-architecture-pages-are-the-metadata-source.md)

## Context

Every pull request currently runs the repository-wide validation and lint
workflows, the full end-to-end suite, and end-to-end coverage. That includes
unit coverage thresholds, data validators, and code checks even when a
contributor changes only authored documentation. The production build is useful
for documentation changes because it catches broken internal links and invalid
content, but the code and browser suites provide little additional value for a
prose-only change.

Contributors updating end-user documentation should not need to understand
unrelated data pipelines or wait for broad test suites. At the same time,
generated content and mixed changes must keep the checks that validate their
sources and outputs. This decision covers human-maintained documents, not
generated data or pages such as imported architecture pages and generated
catalog content. Updates to generated case-study or radar-report data and other
generated snapshots remain on their existing validation path.

## Decision drivers

- Keep changes to end-user documentation easy to contribute and review.
- Catch formatting, spelling, Markdown, document-contract, and internal-link
regressions before publication.
- Preserve full validation for code, data, generated content, and mixed changes.
- Preserve the required check names and the merge-queue contract.
- Make uncertain change classification take the safe, full-validation path.
- Do not lower or change any existing coverage or quality thresholds.

## Options considered

1. **Run the existing full workflow for every pull request.** This is simple and
safe, but requires documentation-only changes to pass unrelated unit, data,
lint, and browser suites. Rejected because it imposes avoidable contributor
friction.
2. **Use focused checks for authored-document-only changes and retain full
checks otherwise.** This keeps the production build and relevant content
checks while skipping suites that do not exercise prose. Chosen.
3. **Skip CI or the production build for documentation-only changes.** This is
the fastest option, but can publish broken internal links or invalid
documentation. Rejected.

## Decision

The CI workflow will classify the changed paths and use two validation paths.

### Authored-document-only changes

A pull request qualifies only when every changed path is a human-maintained
Markdown or MDX document. This includes authored documentation, blog posts,
ADRs, and repository guidance. Generated pages and snapshots are not eligible.
Any code, test, workflow, configuration, data, generated file, non-document
asset, or other unrecognized path makes the change ineligible; a mixed change
uses full validation.

For eligible changes:

- Run formatting, spelling, and Markdown checks on the changed documents.
- Run the applicable document-contract checks, including checks for Markdown
references and documentation structure.
- Build the production site to validate the rendered content and internal links.
- Skip full unit coverage, data validators unrelated to changed documents, broad
code lint/duplication/unused checks, and browser tests and coverage.

The exact generated paths and document-contract checks will be maintained
explicitly in the classifier and workflow. Content authored from generated data
remains subject to its existing data validation lane; changes to generated data
or generated outputs always use full validation.

### All other changes

Code, test, workflow, configuration, data, generated-content, mixed, and
unrecognized changes continue to run the existing full validation, lint,
end-to-end, and end-to-end coverage checks. Manual workflow dispatch also keeps
the full path.

The classifier must support both `pull_request` and `merge_group` events. It
must evaluate the complete applicable diff, including additions, deletions, and
renames. If it cannot determine the changed paths or whether an output is
generated, it must select full validation.

The existing required check names remain stable. For document-only changes, the
required validation check still runs the focused content checks and production
build, and the lint check runs the changed-document checks. Browser jobs may be
skipped only when that required content-validation path succeeds. The merge
queue must apply the same classification and safety rules.

## Implementation plan

1. Add and test a change classifier for authored-document-only, generated,
mixed, code, unknown, and merge-queue diffs. Default classification failures
to full validation.
2. Adapt the existing CI jobs to use the classifier while retaining their
required check names. Keep the production build and content-specific checks
on the document-only path; skip only the unrelated broad suites.
3. Update contributor guidance to explain which checks run for document-only
changes and when the full suite applies.
4. Verify both paths, including merge-queue behavior, and ensure the
document-only path cannot pass when its build or focused checks fail.

## Consequences

- A prose-only contribution receives formatting, spelling, Markdown,
document-contract, and production-build validation without waiting on broad
code and browser suites.
- Generated-data changes and changes that mix content with code or data retain
the current full checks.
- Change classification becomes part of the CI trust boundary. Unknown paths,
missing diff information, or uncertain generated-file status must never
silently skip validation.
- This decision changes which checks run by change type; it does not change test
coverage requirements or any other frozen threshold.
1 change: 1 addition & 0 deletions adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ still pending) so future contributors understand why, not just what.
| [0008](./0008-scope-agent-findings-to-user-observable-change.md) | Scope agent findings to user-observable change | Accepted |
| [0009](./0009-refresh-generated-data-only-on-content-change.md) | Refresh generated data only when its content changes | Accepted |
| [0010](./0010-architecture-pages-are-the-metadata-source.md) | Architecture pages are the source of their catalog metadata | Accepted |
| [0011](./0011-content-focused-validation-for-documentation.md) | Content-only pull requests use focused validation | Accepted |

Supporting material is not part of the numbered ADR sequence and is not assigned
an ADR number, but is listed here for discoverability:
Expand Down
Loading
Loading