Skip to content

canary

canary #3

Workflow file for this run

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 doc/handwritten/for-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.
# The step PRINTS the installed runtimes and the highest major first, so the log shows the preview it is about
# to exercise instead of leaving it to be inferred. It then guards: only a runtime newer than the .NET 10 build
# SDK proves anything, so if the preview install did not yield one the run skips neutrally rather than
# false-passing on .NET 10. fce runs with --verbose, so the worker additionally logs the exact runtime it bound
# to (its "Documenting '…' on .NET <version>." banner); that banner is ASSERTED against the highest major, so
# fce silently binding an older runtime it also had — e.g. if the roll-forward env were dropped — turns the run
# red rather than passing green.
- 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
echo "Installed .NET runtimes:"
dotnet --list-runtimes
newest="$(dotnet --list-runtimes | sed -nE 's/^Microsoft\.NETCore\.App ([0-9]+)\..*/\1/p' | sort -n | tail -1)"
echo "Highest installed .NET major: ${newest:-none}"
if [ "${newest:-0}" -le 10 ]; then
echo "::notice::no runtime newer than the .NET 10 build SDK is installed; skipping this run"
exit 0
fi
# Capture fce's diagnostics (stderr) so the worker's runtime banner can be asserted, not just read. The
# set +e around the call keeps a genuine fce failure from aborting before those diagnostics are echoed.
set +e
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 2> canary-diag.log
fce_rc=$?
set -e
cat canary-diag.log
if [ "$fce_rc" -ne 0 ]; then
echo "::error::fce failed on the .NET ${newest} preview runtime (exit ${fce_rc})"
exit 1
fi
# Proof 1: the worker loaded the target and extracted documented errors.
if ! grep -q '"code"' canary-catalog.json; then
echo "::error::the net8 tooling produced no documented errors on the .NET ${newest} preview runtime"
cat canary-catalog.json
exit 1
fi
# Proof 2 (the whole point of the canary): the worker's own runtime banner confirms it bound to the NEWEST
# installed major, not a lower one it also had available. Without this, fce silently running on .NET 10
# would still document the target and pass green.
if ! grep -qE "Documenting .* on \.NET ${newest}\." canary-diag.log; then
echo "::error::fce did not report running on .NET ${newest} (see the banner above); the preview roll-forward may be broken"
exit 1
fi
echo "ok: the net8 fce and worker documented a net8 target on the .NET ${newest} preview runtime"
# Beyond the tooling above, layer BEHAVIOURAL coverage of the netstandard2.0 LIBRARIES on the preview. The fce
# step already proves the library LOADS on the preview runtime (the worker reflects over a netstandard2.0 target
# there); this runs the libraries' own unit and property suites on it, so a runtime regression that breaks
# behaviour — not just loading — turns the scheduled run red before that major ships. The test projects target
# net10.0; only the `dotnet test` execution is wrapped in DOTNET_ROLL_FORWARD, so the net10 test host binds the
# highest installed major (the preview) while the build stays on the .NET 10 SDK. Best-effort and non-blocking
# like the rest of the canary. RequestBinder.UnitTests IS included here (unlike the net472 floor job): DateOnly,
# which its fixtures bind, exists on every modern .NET — it is only absent from .NET Framework.
- name: Test the libraries on the preview runtime
if: steps.preview.outcome == 'success'
shell: bash
run: |
set -euo pipefail
echo "Installed .NET runtimes:"
dotnet --list-runtimes
newest="$(dotnet --list-runtimes | sed -nE 's/^Microsoft\.NETCore\.App ([0-9]+)\..*/\1/p' | sort -n | tail -1)"
if [ "${newest:-0}" -le 10 ]; then
echo "::notice::no runtime newer than the .NET 10 build SDK is installed; skipping the library preview run"
exit 0
fi
for proj in \
FirstClassErrors.UnitTests \
FirstClassErrors.PropertyTests \
FirstClassErrors.RequestBinder.UnitTests \
FirstClassErrors.RequestBinder.PropertyTests ; do
echo "::group::$proj on the .NET ${newest} preview"
# Build on the .NET 10 SDK (no roll-forward), then run ONLY the test host under roll-forward so it binds
# the preview major. The env is scoped inline to the test call so it never touches build/evaluation.
dotnet build "$proj/$proj.csproj" -c Release
DOTNET_ROLL_FORWARD=LatestMajor DOTNET_ROLL_FORWARD_TO_PRERELEASE=1 \
dotnet test "$proj/$proj.csproj" -c Release --no-build --logger "console;verbosity=normal"
echo "::endgroup::"
done