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
93 changes: 93 additions & 0 deletions .github/workflows/gendoc-docs.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
name: gendoc-docs

# Keeps doc/generated/gendoc (GenDoc's own error catalog) in sync with a pull request's branch on every
# push, so the regenerated content travels through normal review like any other change — never pushed
# straight to main. See doc/handwritten/for-users/CatalogVersioningReference.en.md for the underlying `fce
# catalog update`/`diff` mechanics, and release.yml for the release-time gate that turns a breaking change
# reported here into a required major version bump of the cli train.
on:
pull_request:
branches:
- main

# Cancel a superseded run of the same pull request.
concurrency:
group: gendoc-docs-${{ github.event.pull_request.number }}
cancel-in-progress: true

# Least privilege: read-only at the top level; the single job below widens only what it needs.
permissions:
contents: read

env:
DOTNET_NOLOGO: 'true'
DOTNET_CLI_TELEMETRY_OPTOUT: 'true'
DOTNET_SKIP_FIRST_TIME_EXPERIENCE: 'true'

jobs:
publish:
name: Regenerate GenDoc's error catalog
runs-on: ubuntu-latest
# A pull request from a fork gets a read-only GITHUB_TOKEN regardless of the permissions below (GitHub's
# own security default), so the commit step would fail there anyway. Every pull request in this
# repository's history is same-repo, but skip outright rather than run a doomed build first.
if: github.event.pull_request.head.repo.full_name == github.repository
timeout-minutes: 15
permissions:
# Push the regenerated catalog back onto the pull request's own branch (never main).
contents: write
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
with:
ref: ${{ github.head_ref }}

- name: Setup .NET
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'

- name: Build the tooling
run: dotnet build FirstClassErrors.Cli/FirstClassErrors.Cli.csproj -c Release

- name: Regenerate the catalog and its diff report
run: |
set -euo pipefail
FCE="dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll"
GENDOC_DLL="FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll"
BASELINE="doc/generated/gendoc/errors-baseline.json"

# catalog/ is entirely renderer output: wipe it first so a removed error's page never lingers
# (fce generate only writes files, it never deletes a stale one from a previous run).
rm -rf doc/generated/gendoc/catalog
$FCE generate \
--assemblies "$GENDOC_DLL" \
--format markdown --layout split --service-name gendoc \
--output doc/generated/gendoc/catalog

# Informational only (--fail-on none): this workflow never blocks a pull request. Whether a
# breaking change is acceptable is enforced later, at release time, against the major version
# bump of the cli train (release.yml) -- not here.
$FCE catalog diff \
--assemblies "$GENDOC_DLL" \
--baseline "$BASELINE" \
--report markdown --fail-on none \
> doc/generated/gendoc/errors-diff.md

- name: Commit the regenerated catalog to this branch
run: |
set -euo pipefail
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if [ -z "$(git status --porcelain -- doc/generated/gendoc)" ]; then
echo "GenDoc's error catalog is unchanged; nothing to publish."
exit 0
fi

git add doc/generated/gendoc
git commit -m "docs(gendoc): regenerate the tool's own error catalog"
# Pushes made with the default GITHUB_TOKEN do not re-trigger workflows (GitHub's own loop
# guard), so this never causes another 'pull_request: synchronize' event and another run of
# this job.
git push origin HEAD:${{ github.head_ref }}
80 changes: 80 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,59 @@ jobs:
- name: Test
run: dotnet test FirstClassErrors.sln -c Release --no-build --logger "console;verbosity=normal"

# GenDoc ships bundled inside the fce tool (no package of its own -- see pack.sh), so a breaking
# change to its own error catalog is a breaking change of the cli train. doc/generated/gendoc/
# accumulates the pending diff against errors-baseline.json (the catalog as of the last cli
# release) on every pull request -- see gendoc-docs.yml. Enforce it here, once, at the one point a
# breaking change actually ships: refuse the release unless the version being published bumps the
# major component over the previous cli-v tag. Runs on a dry run too (everything up to attestation
# does, per the philosophy above), so a misconfigured or forgotten major bump is caught before a
# real release attempt, not during one.
- name: Require a major bump for GenDoc breaking changes
if: steps.version.outputs.component == 'cli'
env:
VERSION: ${{ steps.version.outputs.version }}
run: |
set -euo pipefail
PREFIX="cli-v"
CURRENT_TAG="${PREFIX}${VERSION}"
# Same lookup as tools/packaging/release-notes.sh: most recent same-train tag that is not the
# one being published. Empty on the train's first-ever release.
PREVIOUS_TAG="$(git tag --list "${PREFIX}*" --sort=-version:refname | grep -Fxv "$CURRENT_TAG" | head -n1 || true)"

set +e
dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll catalog diff \
--assemblies FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll \
--baseline doc/generated/gendoc/errors-baseline.json \
--fail-on breaking
DIFF_EXIT=$?
set -e

if [ "$DIFF_EXIT" -eq 0 ]; then
echo "ok: GenDoc's error catalog has no breaking change since the last cli release."
exit 0
fi
if [ "$DIFF_EXIT" -ne 2 ]; then
echo "::error::fce catalog diff failed unexpectedly (exit $DIFF_EXIT); see the output above."
exit 1
fi

# DIFF_EXIT = 2: at least one breaking change. Only acceptable if this release bumps the major
# version -- the major component is always the first dot-segment, pre-release suffix included.
if [ -z "$PREVIOUS_TAG" ]; then
echo "ok: no previous $PREFIX tag; this is the train's first release, nothing to compare the major version against."
exit 0
fi
NEW_MAJOR="${VERSION%%.*}"
PREVIOUS_VERSION="${PREVIOUS_TAG#"$PREFIX"}"
PREVIOUS_MAJOR="${PREVIOUS_VERSION%%.*}"

if [ "$NEW_MAJOR" -le "$PREVIOUS_MAJOR" ]; then
echo "::error::GenDoc's error catalog has a breaking change since $PREVIOUS_TAG, but $CURRENT_TAG does not bump the major version ($PREVIOUS_MAJOR -> $NEW_MAJOR). Publish $((PREVIOUS_MAJOR + 1)).0.0 instead, or revert the breaking change."
exit 1
fi
echo "ok: GenDoc's error catalog has a breaking change since $PREVIOUS_TAG, matched by the major version bump ($PREVIOUS_MAJOR -> $NEW_MAJOR)."

# Pack only the train this release targets (lib -> FirstClassErrors + .Testing; cli -> the fce tool),
# so a lib release never republishes the CLI and vice versa. The analyzer is bundled inside the main
# package and the GenDoc worker inside the CLI tool; the samples are not published.
Expand Down Expand Up @@ -234,3 +287,30 @@ jobs:
--title "$TAG" --notes-file release-notes.md --target "$GITHUB_SHA" --prerelease="$PRERELEASE" \
|| { gh release upload "$TAG" "${assets[@]}" --clobber \
&& gh release edit "$TAG" --prerelease="$PRERELEASE"; }

# Closes the loop opened by "Require a major bump...": now that the catalog above has actually been
# published, explicitly accept it as the new contract (fce catalog update) so the next pull request's
# diff starts from zero again. Real publishes only -- a dry run must never touch the real baseline.
# Best-effort: the actual release already succeeded by this point, so a rare race against another
# push to main (this step pushes directly, the one direct-to-main write in this pipeline, justified
# because it only ever follows a deliberate, already-published release) degrades to a warning rather
# than failing an otherwise-successful release; a maintainer can rerun `fce catalog update` by hand.
- name: Refresh GenDoc's error-catalog baseline
if: steps.version.outputs.component == 'cli' && (github.event_name == 'push' || inputs.dry_run == false)
run: |
set -euo pipefail
dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll catalog update \
--assemblies FirstClassErrors.GenDoc/bin/Release/net8.0/FirstClassErrors.GenDoc.dll \
--baseline doc/generated/gendoc/errors-baseline.json

git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"

if [ -z "$(git status --porcelain -- doc/generated/gendoc/errors-baseline.json)" ]; then
echo "Baseline already reflects the published catalog; nothing to refresh."
exit 0
fi

git add doc/generated/gendoc/errors-baseline.json
git commit -m "docs(gendoc): accept the released error-catalog contract (${{ steps.version.outputs.component }}-v${{ steps.version.outputs.version }})"
git push origin HEAD:main || echo "::warning::Could not push the refreshed baseline to main (the release itself already succeeded); run 'fce catalog update' by hand."
11 changes: 9 additions & 2 deletions doc/generated/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,15 @@ This folder holds **living documentation produced automatically by CI/CD**.
Do not edit its contents by hand: anything here is regenerated (and overwritten)
on the next generation run.

- [`gendoc/`](gendoc) — error catalog emitted by the GenDoc toolchain
(`FirstClassErrors.GenDoc` / `fce generate`).
- [`gendoc/`](gendoc) — GenDoc's own error catalog (`FirstClassErrors.GenDoc` documenting itself with
`fce generate`), regenerated on every pull request by the `gendoc-docs` workflow:
- [`catalog/`](gendoc/catalog) — the human-readable catalog (Markdown, one page per error).
- `errors-baseline.json` — the versioned contract: the catalog as of the last `cli` release. Only
changed by `fce catalog update`, run automatically after a successful `cli` release.
- `errors-diff.md` — the catalog's pending changes against that baseline (informational; see
[Catalog Versioning](../handwritten/for-users/CatalogVersioning.en.md)). A breaking change reported
here must be matched by a major version bump at the next `cli` release, or `release.yml` refuses to
publish it.

Handwritten documentation lives under [`../handwritten`](../handwritten):
[`for-users`](../handwritten/for-users) and
Expand Down
2 changes: 0 additions & 2 deletions doc/generated/gendoc/.gitkeep

This file was deleted.

20 changes: 20 additions & 0 deletions doc/generated/gendoc/catalog/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# Error Catalog

- [DocumentationRequest errors](./documentation-request-errors.md)
- [Requested assembly not found](./gendoc-assembly-not-found.md)
- [Documentation opt-in ambiguous](./gendoc-opt-in-ambiguous.md)
- [Solution file not found](./gendoc-solution-not-found.md)
- [Solution path not supported](./gendoc-solution-path-unsupported.md)
- [Project build output not found](./gendoc-target-assembly-not-found.md)
- [Configured worker path invalid](./gendoc-worker-path-invalid.md)
- [DocumentationToolchain errors](./documentation-toolchain-errors.md)
- [Toolchain process failed to start](./gendoc-process-start-failed.md)
- [Toolchain process timed out](./gendoc-process-timed-out.md)
- [Solution project enumeration failed](./gendoc-project-enumeration-failed.md)
- [Solution build failed](./gendoc-solution-build-failed.md)
- [Target path resolution failed](./gendoc-target-path-resolution-failed.md)
- [Documentation worker failed](./gendoc-worker-failed.md)
- [Documentation worker not deployed](./gendoc-worker-not-deployed.md)
- [Documentation worker output missing](./gendoc-worker-output-missing.md)
- [Documentation worker output unreadable](./gendoc-worker-output-unreadable.md)
- [Documentation worker run failed](./gendoc-worker-run-failed.md)
10 changes: 10 additions & 0 deletions doc/generated/gendoc/catalog/documentation-request-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# DocumentationRequest errors

Validation of a documentation-generation request: the solution, assemblies, worker path and opt-in markers it designates.

- [Requested assembly not found](./gendoc-assembly-not-found.md)
- [Documentation opt-in ambiguous](./gendoc-opt-in-ambiguous.md)
- [Solution file not found](./gendoc-solution-not-found.md)
- [Solution path not supported](./gendoc-solution-path-unsupported.md)
- [Project build output not found](./gendoc-target-assembly-not-found.md)
- [Configured worker path invalid](./gendoc-worker-path-invalid.md)
14 changes: 14 additions & 0 deletions doc/generated/gendoc/catalog/documentation-toolchain-errors.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# DocumentationToolchain errors

The toolchain the documentation generator drives: the .NET SDK commands it spawns and the extraction worker process it launches per assembly.

- [Toolchain process failed to start](./gendoc-process-start-failed.md)
- [Toolchain process timed out](./gendoc-process-timed-out.md)
- [Solution project enumeration failed](./gendoc-project-enumeration-failed.md)
- [Solution build failed](./gendoc-solution-build-failed.md)
- [Target path resolution failed](./gendoc-target-path-resolution-failed.md)
- [Documentation worker failed](./gendoc-worker-failed.md)
- [Documentation worker not deployed](./gendoc-worker-not-deployed.md)
- [Documentation worker output missing](./gendoc-worker-output-missing.md)
- [Documentation worker output unreadable](./gendoc-worker-output-unreadable.md)
- [Documentation worker run failed](./gendoc-worker-run-failed.md)
38 changes: 38 additions & 0 deletions doc/generated/gendoc/catalog/gendoc-assembly-not-found.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Requested assembly not found

- **Code:** `GENDOC_ASSEMBLY_NOT_FOUND`
- **Source:** `DocumentationRequest`

A documentation-generation request explicitly designates an assembly that does not exist on disk. The full path, as resolved by the generator, is carried in the error context.

> **Business rule:** Every assembly explicitly designated by a generation request must exist on disk.

## Diagnostics

- **The assembly was never built, or was built to a different configuration or target framework than the path assumes.** — _origin:_ External — Build the project first and compare the path in the error context with the actual build output directory.

## Examples

**Public response (RFC 9457)**

```json
{
"type": "urn:problem:gendoc:gendoc-assembly-not-found",
"title": "A requested assembly was not found.",
"detail": "One of the assemblies passed to the documentation generator does not exist on disk.",
"code": "GENDOC_ASSEMBLY_NOT_FOUND"
}
```

**Diagnostic (internal — not for external exposure)**

```text
2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Assembly not found: '/src/app/bin/Release/net8.0/Application.dll'. error.code=GENDOC_ASSEMBLY_NOT_FOUND
```

## Context

| Key | Type | Description | Example values |
| --- | --- | --- | --- |
| `AssemblyPath` | `System.String` | Full path of the assembly being documented. | `/src/app/bin/Release/net8.0/Application.dll` |

40 changes: 40 additions & 0 deletions doc/generated/gendoc/catalog/gendoc-opt-in-ambiguous.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Documentation opt-in ambiguous

- **Code:** `GENDOC_OPT_IN_AMBIGUOUS`
- **Source:** `DocumentationRequest`

The generator reads the documentation opt-in property literally from the project XML, without MSBuild evaluation. When the property is defined more than once, or gated behind an MSBuild Condition (directly or via a Choose/When branch), its effective value cannot be known, so the generator refuses to guess and skips the project. The project path, the property name, and the reason are carried in the error context.

> **Business rule:** The opt-in property must be declared at most once, literally and unconditionally, in each project file.

## Diagnostics

- **The property is declared in several PropertyGroups, or under a Condition attribute or a Choose/When branch.** — _origin:_ External — Inspect the project file named in the error context; keep a single unconditional declaration, or document the built assembly explicitly instead.

## Examples

**Public response (RFC 9457)**

```json
{
"type": "urn:problem:gendoc:gendoc-opt-in-ambiguous",
"title": "A project's documentation opt-in is ambiguous.",
"detail": "Declare the opt-in property once, literally and unconditionally, in the project file — or document the assembly explicitly.",
"code": "GENDOC_OPT_IN_AMBIGUOUS"
}
```

**Diagnostic (internal — not for external exposure)**

```text
2026-07-04T13:42:18.734Z ERROR [DocumentationRequest] Cannot determine the opt-in for project '/src/app/Application/Application.csproj': the 'GenerateErrorDocumentation' property is defined 2 times in the project XML, which GenDoc reads without MSBuild evaluation. Declare it once, literally and unconditionally in the project file, or document the built assembly explicitly. error.code=GENDOC_OPT_IN_AMBIGUOUS
```

## Context

| Key | Type | Description | Example values |
| --- | --- | --- | --- |
| `ProjectPath` | `System.String` | Full path of the project file being processed. | `/src/app/Application/Application.csproj` |
| `OptInProperty` | `System.String` | Name of the MSBuild property read as the documentation opt-in marker. | `GenerateErrorDocumentation` |
| `AmbiguityReason` | `System.String` | Why the opt-in property's effective value cannot be determined from the project XML. | `defined 2 times` |

36 changes: 36 additions & 0 deletions doc/generated/gendoc/catalog/gendoc-process-start-failed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Toolchain process failed to start

- **Code:** `GENDOC_PROCESS_START_FAILED`
- **Source:** `DocumentationToolchain`

The generator drives the .NET SDK and its extraction worker through child processes. One of them could not be started at all. The executable name is carried in the error context.

## Diagnostics

- **The 'dotnet' host is not installed or not on the PATH of the process running the generation.** — _origin:_ External — Run 'dotnet --info' in the same environment (shell, CI step, service account) as the generation.

## Examples

**Public response (RFC 9457)**

```json
{
"type": "urn:problem:gendoc:gendoc-process-start-failed",
"title": "A required process could not be started.",
"detail": "A child process required by the documentation generation could not be started.",
"code": "GENDOC_PROCESS_START_FAILED"
}
```

**Diagnostic (internal — not for external exposure)**

```text
2026-07-04T13:42:18.734Z ERROR [DocumentationToolchain] Failed to start process 'dotnet'. error.code=GENDOC_PROCESS_START_FAILED
```

## Context

| Key | Type | Description | Example values |
| --- | --- | --- | --- |
| `ProcessFileName` | `System.String` | Executable name of the child process the generator tried to run. | `dotnet` |

Loading