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

# Early-warning that the documentation tooling still runs on the NEXT, not-yet-released .NET.
#
# The shipped tooling targets net8.0 and reaches every newer runtime purely by roll-forward (the worker uses
# LatestMajor; see maintainers/adr/0002-floor-the-tooling-runtime.md). A runtime breaking change in a future
# major would surface here first — on a schedule, against the current preview — instead of in a user's CI the
# day that major ships. It is the upper-end counterpart of the `floor` job in ci.yml, which pins the lower end.
#
# Deliberately NOT a pull-request gate: preview runtimes are unstable and sometimes not published yet, and their
# breakage is not this repo's bug. It runs on a schedule (and on demand); a real regression turns the scheduled
# run red and GitHub notifies the maintainer, while an unavailable preview ends the run neutral.

on:
schedule:
# Weekly, Monday 06:00 UTC. Preview builds drop roughly monthly, so weekly catches a new one within days
# without adding noise. Scheduled workflows run from the default branch only.
- cron: '0 6 * * 1'
workflow_dispatch:

concurrency:
group: canary-${{ github.ref }}
cancel-in-progress: true

# Least privilege: checkout + build only.
permissions:
contents: read

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

jobs:
preview:
name: Documentation tooling on the next .NET preview
runs-on: ubuntu-latest
# Build (~20s) plus one doc-generation run; cap a hung run like the ci jobs.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

# Best effort: a preview for the next major may not be published yet, or may fail to install. Marking this
# step continue-on-error keeps that from reddening the canary — the build/run steps below are gated on its
# outcome and simply skip, leaving the job neutral. Bump the major (11.0.x -> 12.0.x -> ...) once the
# current preview reaches GA; see ADR 0002.
- name: Setup the next .NET preview (best effort)
id: preview
continue-on-error: true
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '11.0.x'
dotnet-quality: preview

# The build SDK. net8.0 is only a target framework; global.json pins the .NET 10 SDK for the build itself.
- name: Setup the build SDK (.NET 10)
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: '10.0.x'

- name: No preview available — skip (not a failure)
if: steps.preview.outcome != 'success'
run: echo "::notice::no .NET preview was available to canary against; skipping this run"

# The fce tool, its GenDoc worker (copied next to fce by the CLI build target) and a net8 build of the Usage
# sample as a real target to document.
- name: Build the net8 tooling and a net8 target
if: steps.preview.outcome == 'success'
run: |
dotnet build FirstClassErrors.Cli/FirstClassErrors.Cli.csproj -c Release
dotnet build FirstClassErrors.Usage/FirstClassErrors.Usage.csproj -c Release -f net8.0

# Force the net8 tooling onto the newest installed major, INCLUDING the prerelease preview:
# * DOTNET_ROLL_FORWARD=LatestMajor overrides the CLI's Major (and, redundantly, the worker's LatestMajor)
# so both processes select the highest installed major rather than staying on a stable one;
# * DOTNET_ROLL_FORWARD_TO_PRERELEASE=1 lets a stable net8 app bind a *prerelease* runtime, which
# roll-forward refuses by default.
# A guard first confirms a newer major than the build SDK is actually installed, so the canary never
# false-passes by silently running on .NET 10. A regression in the preview runtime's roll-forward behaviour
# then turns this step red, and the scheduled run notifies the maintainer.
- name: Generate documentation on the preview runtime
if: steps.preview.outcome == 'success'
env:
DOTNET_ROLL_FORWARD: LatestMajor
DOTNET_ROLL_FORWARD_TO_PRERELEASE: '1'
run: |
set -euo pipefail
if ! dotnet --list-runtimes | grep -Eq 'Microsoft\.NETCore\.App (1[1-9]|[2-9][0-9])\.'; then
echo "::notice::the installed 'preview' is not a newer major than the build SDK; skipping this run"
exit 0
fi
dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll generate \
--assemblies FirstClassErrors.Usage/bin/Release/net8.0/FirstClassErrors.Usage.dll \
--format json --verbose > canary-catalog.json
# Positive proof, not just exit 0: the worker actually loaded the target and extracted documented errors.
if ! grep -q '"code"' canary-catalog.json; then
echo "::error::the net8 tooling failed to document a target on the .NET preview runtime"
cat canary-catalog.json
exit 1
fi
echo "ok: the net8 fce and worker documented a net8 target on the .NET preview runtime"
55 changes: 55 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,3 +69,58 @@ jobs:
name: coverage-${{ matrix.os }}
path: artifacts/coverage/**/coverage.opencover.xml
if-no-files-found: error

# build-test above runs the whole suite on the LATEST .NET (10). This job proves the other end of the
# supported range: that the fce tool and its worker, which ship targeting net8.0 (the floor — see
# maintainers/adr/0002-floor-the-tooling-runtime.md), actually run on the .NET 8 RUNTIME. The net8 TFM
# already guards the compile-time API surface on every build; what it cannot show is that the tooling
# executes on its advertised minimum runtime, which is what this job checks against the real artifacts.
floor:
name: Documentation tooling on the .NET 8 floor
runs-on: ubuntu-latest
# Build (~20s) plus one doc-generation run; cap a hung run like the build-test job.
timeout-minutes: 15
steps:
- name: Checkout
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7

# Two SDKs on purpose: 10.0.x satisfies the repo global.json (which pins the .NET 10 SDK) so the build
# runs normally, and 8.0.x brings the .NET 8 runtime — the floor the tooling ships against — so the run
# step below can execute the net8 binaries on it.
- name: Setup .NET (floor runtime + build SDK)
uses: actions/setup-dotnet@26b0ec14cb23fa6904739307f278c14f94c95bf1 # v5
with:
dotnet-version: |
8.0.x
10.0.x

# The fce tool, its GenDoc worker (copied next to fce by the CLI build target) and a net8 build of the
# Usage sample as a real target to document. Built with the .NET 10 SDK — net8.0 is only a target here.
- name: Build the net8 tooling and a net8 target
run: |
dotnet build FirstClassErrors.Cli/FirstClassErrors.Cli.csproj -c Release
dotnet build FirstClassErrors.Usage/FirstClassErrors.Usage.csproj -c Release -f net8.0

# Run the net8 fce + worker on the .NET 8 RUNTIME (not rolled forward to the newer one the runner also
# carries) and document a net8 assembly end to end.
#
# DOTNET_ROLL_FORWARD=LatestPatch overrides the roll-forward baked into each runtimeconfig (Major on the
# CLI, LatestMajor on the worker): the environment variable wins over runtimeconfig, and LatestPatch stays
# within the requested major, so both processes bind the highest .NET 8 patch present and can never roll
# onto .NET 10. If .NET 8 were absent the host would fail to start — so a green step means the net8 tooling
# genuinely ran on the .NET 8 runtime (the worker loads the net8 target via Assembly.LoadFrom).
- name: Generate documentation on the .NET 8 runtime
env:
DOTNET_ROLL_FORWARD: LatestPatch
run: |
set -euo pipefail
dotnet FirstClassErrors.Cli/bin/Release/net8.0/fce.dll generate \
--assemblies FirstClassErrors.Usage/bin/Release/net8.0/FirstClassErrors.Usage.dll \
--format json --verbose > floor-catalog.json
# Positive proof, not just exit 0: the worker actually loaded the target and extracted documented errors.
if ! grep -q '"code"' floor-catalog.json; then
echo "::error::no documented errors were extracted on the .NET 8 floor"
cat floor-catalog.json
exit 1
fi
echo "ok: the net8 fce and worker documented a net8 target on the .NET 8 runtime"
10 changes: 9 additions & 1 deletion FirstClassErrors.Cli/FirstClassErrors.Cli.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,17 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<!-- Floor TFM: fce ships as a net8.0 tool so a shop whose newest runtime is .NET 8 can install and run it.
RollForward=Major lets the very same build run on any newer major runtime — a machine that has only
.NET 10 rolls 8->10 — so one floor build covers every supported runtime from .NET 8 up, with no
per-release target matrix. Without Major, the default Minor policy would not cross 8->major, and the
tool would fail to start on the (common) machine that has a newer .NET but not .NET 8. -->
<TargetFramework>net8.0</TargetFramework>
<RollForward>Major</RollForward>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Keep modern C# despite the net8.0 floor (see the netstandard2.0 projects, which do the same). -->
<LangVersion>latest</LangVersion>
<!-- The project keeps its name; only the produced executable is called "fce". -->
<AssemblyName>fce</AssemblyName>
</PropertyGroup>
Expand Down
4 changes: 3 additions & 1 deletion FirstClassErrors.Cli/README.nuget.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ artifact and always matches the deployed system, with no manual upkeep.

dotnet tool install --global FirstClassErrors.Cli

This installs the `fce` command and requires the .NET 10 runtime. Use `--global` for a
This installs the `fce` command and requires the **.NET 8 runtime or newer** — `fce` targets
.NET 8 and rolls forward onto any later runtime, so a machine that has only .NET 10 (or a
future major) runs it just as well. Use `--global` for a
machine-wide tool, or install it into a
[tool manifest](https://learn.microsoft.com/dotnet/core/tools/local-tools-how-to-use)
for a version-pinned, per-repository tool.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,20 @@

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Target the floor runtime (net8.0) but keep the language at the SDK's newest C#, as the netstandard2.0
projects already do. The TFM only bounds the BCL surface and the runtime this process binds to; it must
not drag the source back to net8's default C# 12. -->
<LangVersion>latest</LangVersion>
<IsPackable>false</IsPackable>
<!-- The worker is launched by the host against a target's own dependency closure; allow it to run on a runtime
newer than the one it was built for. -->
<RollForward>Major</RollForward>
<!-- The worker loads a target assembly (Assembly.LoadFrom) that may be built for ANY runtime the user
targets, so its process must run on a runtime >= the target's. LatestMajor (not Major) is required here:
Major only rolls up when the requested major is ABSENT, so on a machine carrying both .NET 8 and .NET 10 a
net8.0 worker would bind to 8 and fail to load a net10 target. LatestMajor always selects the highest
installed major, so the worker can document a target built for any runtime present on the machine. -->
<RollForward>LatestMajor</RollForward>
</PropertyGroup>

<ItemGroup>
Expand Down
6 changes: 5 additions & 1 deletion FirstClassErrors.GenDoc/FirstClassErrors.GenDoc.csproj
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- Loaded in-process by the fce tool, so it targets the same floor runtime as the CLI (net8.0); the runtime
it actually binds to is chosen by the CLI's runtimeconfig, so no RollForward is set here. -->
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<!-- Keep modern C# despite the net8.0 floor (see the netstandard2.0 projects, which do the same). -->
<LangVersion>latest</LangVersion>
</PropertyGroup>

<ItemGroup>
Expand Down
5 changes: 4 additions & 1 deletion FirstClassErrors.Usage/FirstClassErrors.Usage.csproj
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<!-- Multi-targeted on purpose: net10.0 is what the unit tests reference and document, while net8.0 gives the
CI "documentation tooling on the .NET 8 floor" job a real net8 target to feed the net8 fce/worker — and,
for free, dogfoods the netstandard2.0 library on the floor runtime. This is a sample, never packed. -->
<TargetFrameworks>net8.0;net10.0</TargetFrameworks>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand Down
Loading