From a98259140e3cc8d9a8ba3760e2f1c26b4174c971 Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 14:36:04 +0200 Subject: [PATCH 1/2] Check the documentation links instead of reporting success on nothing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The skip pattern excluded every URL on localhost, which is where linkinator serves the files it scans. It matched the crawl root itself, so the whole scan was skipped and each run reported success having checked zero links — locally and in CI alike. - Skip only what falls outside /Documentation/, which is exactly the site-absolute paths a single repository cannot resolve. - Fail when a run scans zero links or reports no summary at all, so the check cannot quietly become a no-op again. - Scan .mdx pages as well, and let CI run the same script contributors run, so the two configurations cannot drift apart. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VRa2Z1cA6D3Fuw9TAo6m2p --- .github/workflows/markdown-verification.yml | 16 ++-- Documentation/verify-links.sh | 85 +++++++++++++++++++++ Documentation/verify-markdown.sh | 28 ++++--- 3 files changed, 106 insertions(+), 23 deletions(-) create mode 100755 Documentation/verify-links.sh diff --git a/.github/workflows/markdown-verification.yml b/.github/workflows/markdown-verification.yml index 1309386aa..95f11510e 100644 --- a/.github/workflows/markdown-verification.yml +++ b/.github/workflows/markdown-verification.yml @@ -33,12 +33,12 @@ jobs: - name: Checkout code uses: actions/checkout@v4 - - name: Check links in Documentation - uses: JustinBeckwith/linkinator-action@v1 + - name: Setup Node + uses: actions/setup-node@v4 with: - paths: 'Documentation/**/*.md' - markdown: true - recurse: true - redirects: allow - statusCodes: '403:ok' - linksToSkip: '^(https?:\/\/)?(localhost|127\.0\.0\.1)(:\d+)?(\/|$)' + node-version: 23.x + + # The same script contributors run locally, so that what CI checks and + # what verify-markdown.sh checks cannot drift apart. + - name: Check links in Documentation + run: ./Documentation/verify-links.sh diff --git a/Documentation/verify-links.sh b/Documentation/verify-links.sh new file mode 100755 index 000000000..ad1c9304e --- /dev/null +++ b/Documentation/verify-links.sh @@ -0,0 +1,85 @@ +#!/bin/bash + +# Link Verification Script +# Verifies the links in the documentation of this repository, and is used both +# by verify-markdown.sh and by the Markdown Verification workflow so the two +# cannot drift apart. +# +# What is verified here: +# - relative links between pages in this repository +# - external http(s) links +# +# What is not verified here: +# Links written as site-absolute paths (/arc/..., /chronicle/...) address the +# aggregated documentation site, where each product's Documentation folder is +# mounted under its own prefix. They cannot resolve against a single +# repository and are verified when that site is built. They are skipped by +# rule rather than by name, so a new prefix needs no change here. + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +# Pinned so that a new major version cannot silently change what is scanned. +LINKINATOR_VERSION="8.0.2" + +# linkinator serves the scanned files from a local web server, so every internal +# link resolves to http://127.0.0.1:/. Skipping what falls outside +# /Documentation/ therefore skips exactly the site-absolute paths this +# repository cannot resolve, and nothing else. The pattern must never match the +# crawl root itself - a pattern that did is what turned this check into a no-op. +SITE_ABSOLUTE_LINKS='^https?://(localhost|127\.0\.0\.1):[0-9]+/(?!Documentation/)' + +cd "$ROOT_DIR" + +if ! command -v npx &> /dev/null; then + echo "Error: npx is not installed. Please install Node.js and npm." + exit 1 +fi + +echo "Checking links in Documentation..." +echo "This may take a few minutes to check all links..." +echo "" + +set +e +OUTPUT=$(npx --yes "linkinator@$LINKINATOR_VERSION" \ + "Documentation/**/*.md" \ + "Documentation/**/*.mdx" \ + --markdown \ + --recurse \ + --directory-listing \ + --verbosity error \ + --status-code "403:ok" \ + --skip "$SITE_ABSOLUTE_LINKS" 2>&1) +LINKINATOR_EXIT_CODE=$? +set -e + +echo "$OUTPUT" + +# How many links were actually looked at. A check that scans nothing reports +# success while verifying nothing, which reads exactly like a passing check. +SCANNED=$(echo "$OUTPUT" | grep -oE 'canned [0-9]+ link' | grep -oE '[0-9]+' | tail -1) + +echo "" + +if [ -z "$SCANNED" ]; then + echo "✗ Link verification could not determine how many links were scanned." + echo " linkinator reported no scan summary - the file globs most likely matched nothing." + exit 1 +fi + +if [ "$SCANNED" -eq 0 ]; then + echo "✗ Link verification scanned zero links." + echo " Nothing was checked, so this run proves nothing about the links in Documentation." + exit 1 +fi + +if [ $LINKINATOR_EXIT_CODE -ne 0 ]; then + echo "✗ Link verification failed - $SCANNED links scanned." + exit $LINKINATOR_EXIT_CODE +fi + +echo "✓ Link verification passed - $SCANNED links scanned." +echo " Site-absolute links (/arc/..., /chronicle/...) resolve only on the aggregated" +echo " documentation site and are verified when that site is built." diff --git a/Documentation/verify-markdown.sh b/Documentation/verify-markdown.sh index 9fae3f01d..8bc90ce92 100755 --- a/Documentation/verify-markdown.sh +++ b/Documentation/verify-markdown.sh @@ -13,10 +13,9 @@ echo "Markdown Verification" echo "==========================================" echo "" -# Check if running from repository root or Documentation folder -if [ "$(basename "$PWD")" = "Documentation" ]; then - cd .. -fi +# Both steps address paths from the repository root, whether this script is run +# from there or from the Documentation folder. +cd "$ROOT_DIR" echo "Working directory: $PWD" echo "" @@ -32,8 +31,13 @@ if ! command -v npx &> /dev/null; then exit 1 fi -npx markdownlint-cli2 "Documentation/**/*.md" -LINT_EXIT_CODE=$? +# Each step captures its own exit code and the run continues, so that a failing +# lint step does not leave the state of the links unreported. +if npx --yes markdownlint-cli2 "Documentation/**/*.md"; then + LINT_EXIT_CODE=0 +else + LINT_EXIT_CODE=$? +fi echo "" if [ $LINT_EXIT_CODE -eq 0 ]; then @@ -48,17 +52,11 @@ echo "==========================================" echo "Step 2: Running link verification..." echo "==========================================" echo "" -echo "This may take a few minutes to check all links..." -echo "" - -npx linkinator "Documentation/**/*.md" --markdown --recurse --verbosity error --status-code "403:ok" --skip "^(https?:\\/\\/)?(localhost|127\\.0\\.0\\.1)(:\\d+)?(\\/|$)" -LINK_EXIT_CODE=$? -echo "" -if [ $LINK_EXIT_CODE -eq 0 ]; then - echo "✓ Link verification passed!" +if "$SCRIPT_DIR/verify-links.sh"; then + LINK_EXIT_CODE=0 else - echo "✗ Link verification failed with exit code $LINK_EXIT_CODE" + LINK_EXIT_CODE=$? fi echo "" From ffe33e5c68d44c8aad401202044c1da14d5ae2fe Mon Sep 17 00:00:00 2001 From: woksin Date: Wed, 29 Jul 2026 14:36:16 +0200 Subject: [PATCH 2/2] Point the documentation links at the pages they name Five links kept the .md extension after the page they name became .mdx, so they resolved nowhere at all. Nine more left the extension off; the rest of the documentation links to a page by its file name, and now these do too. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_01VRa2Z1cA6D3Fuw9TAo6m2p --- Documentation/backend/chronicle/index.md | 2 +- Documentation/backend/chronicle/react-to-an-event.md | 2 +- Documentation/backend/getting-started/index.md | 2 +- Documentation/backend/index.md | 2 +- Documentation/frontend/react/index.md | 2 +- Documentation/tutorial/books-and-relationships.mdx | 2 +- Documentation/tutorial/first-slice.mdx | 2 +- Documentation/tutorial/index.md | 12 ++++++------ Documentation/tutorial/real-time.mdx | 2 +- Documentation/tutorial/validation.mdx | 2 +- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/Documentation/backend/chronicle/index.md b/Documentation/backend/chronicle/index.md index 28a04a267..3b9378978 100644 --- a/Documentation/backend/chronicle/index.md +++ b/Documentation/backend/chronicle/index.md @@ -45,7 +45,7 @@ Without this package, Arc and Chronicle are independent. With it: | Topic | Description | | ----- | ----------- | | [Aggregates](aggregates/index.md) | Working with aggregate roots and event sourcing. | -| [Add event sourcing to an Arc slice](add-event-sourcing.md) | Move one database-backed slice to Chronicle while keeping its query and React screen in place. | +| [Add event sourcing to an Arc slice](add-event-sourcing.mdx) | Move one database-backed slice to Chronicle while keeping its query and React screen in place. | | [Cratis Package](cratis-package.md) | The convenience package for Arc + Chronicle applications. | | [React to an event](react-to-an-event.md) | Run side effects or follow-up commands from Chronicle events with reactors. | | [Commands](commands/index.md) | Returning events from commands, event source id resolution, and concurrency scoping. | diff --git a/Documentation/backend/chronicle/react-to-an-event.md b/Documentation/backend/chronicle/react-to-an-event.md index 0485f485e..5ba1e88e7 100644 --- a/Documentation/backend/chronicle/react-to-an-event.md +++ b/Documentation/backend/chronicle/react-to-an-event.md @@ -42,5 +42,5 @@ A reactor may be called more than once for the same event — during replay or r ## See also - [Returning Commands as Side Effects](./reactors/toc.yml) — execute commands as side effects from a reactor. -- [Add event sourcing to an Arc slice](./add-event-sourcing.md) — where reactors enter the Arc model. +- [Add event sourcing to an Arc slice](./add-event-sourcing.mdx) — where reactors enter the Arc model. - [Return a result or an error](/arc/scenarios/return-a-result-or-error/) — what the command you execute can return. diff --git a/Documentation/backend/getting-started/index.md b/Documentation/backend/getting-started/index.md index 5018935e4..634728206 100644 --- a/Documentation/backend/getting-started/index.md +++ b/Documentation/backend/getting-started/index.md @@ -7,7 +7,7 @@ Get an Arc backend up and building features. These first pages use a plain datab Once you have a project: -- **[Your first command and query](./your-first-command.md)** — build a backend slice end to end: a command with `Handle()`, the read model it writes, and the live query that serves it. +- **[Your first command and query](./your-first-command.mdx)** — build a backend slice end to end: a command with `Handle()`, the read model it writes, and the live query that serves it. - **[MongoDB integration](../mongodb/index.md)** — configure Arc over MongoDB collections and observable change streams. - **[Entity Framework integration](../entity-framework/getting-started.md)** — configure Arc over DbContexts and observed DbSets. diff --git a/Documentation/backend/index.md b/Documentation/backend/index.md index 23547eb3d..43a009dec 100644 --- a/Documentation/backend/index.md +++ b/Documentation/backend/index.md @@ -46,5 +46,5 @@ Arc meets the rest of your stack: | [Open API](./open-api/index.md) | OpenAPI/Swagger generation. | | [Code Analysis](./code-analysis/index.md) | Analyzers and fixers that catch mistakes at compile time. | -Building the UI on top? Head to the [frontend](../frontend/index.md), which consumes everything here +Building the UI on top? Head to the [frontend](../frontend/index.mdx), which consumes everything here through the generated proxies. diff --git a/Documentation/frontend/react/index.md b/Documentation/frontend/react/index.md index 22a020935..f74431f4f 100644 --- a/Documentation/frontend/react/index.md +++ b/Documentation/frontend/react/index.md @@ -28,7 +28,7 @@ those two with validation, scopes, and identity. | [Identity](./identity.md) | Who the user is, and what they're allowed to see and do. | | [Dialogs](./dialogs.md) | Consistent dialog handling for command and data-entry flows. | | [Proxy Generation](../../backend/proxy-generation/index.md) | How the typed proxies you import here are generated from C#. | -| [Storybook](./storybook.md) | The Storybook for the components Arc exposes. | +| [Storybook](./storybook.mdx) | The Storybook for the components Arc exposes. | | [Story Components](./stories) | Building good-looking, consistent stories. | Prefer a structured, testable approach for complex screens? See [MVVM with React](../react.mvvm/index.md). diff --git a/Documentation/tutorial/books-and-relationships.mdx b/Documentation/tutorial/books-and-relationships.mdx index e02392506..3c644fc02 100644 --- a/Documentation/tutorial/books-and-relationships.mdx +++ b/Documentation/tutorial/books-and-relationships.mdx @@ -144,4 +144,4 @@ The `authorId` is context the form needs to be *valid* from the start, so it goe - A **relationship as a foreign key + a filtered query**: `BooksForAuthor` reads exactly one author's catalog, live, with no join. - A React screen that composes the two: a list of authors, each rendering its own live catalog. -Two features, related, both live. We keep *saying* "it stays live" — next we'll make that concrete and watch a screen update itself the instant the data changes. [Let's make it live →](./real-time) +Two features, related, both live. We keep *saying* "it stays live" — next we'll make that concrete and watch a screen update itself the instant the data changes. [Let's make it live →](./real-time.mdx) diff --git a/Documentation/tutorial/first-slice.mdx b/Documentation/tutorial/first-slice.mdx index 031f818df..2862eb6c3 100644 --- a/Documentation/tutorial/first-slice.mdx +++ b/Documentation/tutorial/first-slice.mdx @@ -255,4 +255,4 @@ In one folder, read top to bottom: That's a complete vertical slice, backend to browser, over a plain database. The next feature will be another folder just like it. -There's one problem, though: right now a librarian can register an author with a blank name, or the same author twice, and nothing stops them. A real app has to say no. [Let's make it trustworthy →](./validation) +There's one problem, though: right now a librarian can register an author with a blank name, or the same author twice, and nothing stops them. A real app has to say no. [Let's make it trustworthy →](./validation.mdx) diff --git a/Documentation/tutorial/index.md b/Documentation/tutorial/index.md index 963882e42..c470c8a18 100644 --- a/Documentation/tutorial/index.md +++ b/Documentation/tutorial/index.md @@ -68,10 +68,10 @@ An Arc project running locally with a database. The [Get started](/arc/backend/g ## The tour -1. **[Your first full-stack slice](./first-slice)** — register an author from C# all the way to a live React screen, fully typed. -2. **[Make it trustworthy](./validation)** — reject bad input with a validator and a uniqueness rule, and show the reason in the form. -3. **[Relate your slices](./books-and-relationships)** — add books that belong to an author, and read them back. -4. **[Make it live](./real-time)** — observable queries that update the screen the moment the data changes. -5. **[Decide who can do what](./authorization)** — lock the catalog down with role-based authorization. +1. **[Your first full-stack slice](./first-slice.mdx)** — register an author from C# all the way to a live React screen, fully typed. +2. **[Make it trustworthy](./validation.mdx)** — reject bad input with a validator and a uniqueness rule, and show the reason in the form. +3. **[Relate your slices](./books-and-relationships.mdx)** — add books that belong to an author, and read them back. +4. **[Make it live](./real-time.mdx)** — observable queries that update the screen the moment the data changes. +5. **[Decide who can do what](./authorization.mdx)** — lock the catalog down with role-based authorization. -Each chapter ends where the next begins. By the end you'll have a real full-stack feature — and the model to build your own. Ready? [Let's build the first slice →](./first-slice) +Each chapter ends where the next begins. By the end you'll have a real full-stack feature — and the model to build your own. Ready? [Let's build the first slice →](./first-slice.mdx) diff --git a/Documentation/tutorial/real-time.mdx b/Documentation/tutorial/real-time.mdx index 480317f6b..2141cb9b5 100644 --- a/Documentation/tutorial/real-time.mdx +++ b/Documentation/tutorial/real-time.mdx @@ -79,4 +79,4 @@ That's the whole switch between "load once" and "stay live." You choose per quer - A clear picture of **why your screens update themselves** — observable queries are standing subscriptions, database to browser. - The one-line difference between a **one-shot** query and a **live** one — the return type, nothing else. -The catalog is live. There's just one thing left before it's a real back office: right now *anyone* can register authors and add books. Let's decide who's allowed. [Lock it down →](./authorization) +The catalog is live. There's just one thing left before it's a real back office: right now *anyone* can register authors and add books. Let's decide who's allowed. [Lock it down →](./authorization.mdx) diff --git a/Documentation/tutorial/validation.mdx b/Documentation/tutorial/validation.mdx index 4da45058e..8663fca71 100644 --- a/Documentation/tutorial/validation.mdx +++ b/Documentation/tutorial/validation.mdx @@ -101,4 +101,4 @@ One rule on the value type, one rule on the command, and both ends of the app ho - A `RegisterAuthorValidator` **business rule** that checks existing state in the database, with a unique index behind it for the hard guarantee. - A React form that surfaces both — client-side and server-side — with no extra frontend code. -Our author is trustworthy now. But an author with no books is a lonely thing. Next we'll give them a catalog — a second feature that *relates* to this one. [Let's add books →](./books-and-relationships) +Our author is trustworthy now. But an author with no books is a lonely thing. Next we'll give them a catalog — a second feature that *relates* to this one. [Let's add books →](./books-and-relationships.mdx)