From c0392389728d21fda8929496a38b3c833926350f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 13 Jul 2026 09:03:28 +0000 Subject: [PATCH] build(cli): guard that the fce tool package ships its GenDoc worker `fce generate` spawns FirstClassErrors.GenDoc.Worker in a child process and resolves it next to the installed executable (AppContext.BaseDirectory), so the tool only works if the worker and its closure travel inside the .nupkg under tools//any. That bundling already happens -- PackAsTool packs the CLI's publish output and _PublishDocumentationWorker drops the worker there -- but nothing enforced it: neither `dotnet build` nor `dotnet publish` exercises the .nupkg content, so a regression would pass every local check and only surface as "documentation worker could not be located" on a user's first run. Add a pack-time assertion in tools/packaging/pack.sh (cli scope) that fails the pack when FirstClassErrors.GenDoc.Worker.dll is absent from tools//any, in the same spirit as the existing SBOM check. Verified it bites by temporarily disabling the packaging target: the pack then exits non-zero. Replace the misleading "validate with dotnet build and dotnet publish" comment in the CLI .csproj with a pointer to that guard and to a new manual tool-install smoke test, documented in maintainers/ReleaseDryRun (EN + FR): install the packed tool and run `fce generate`, the only oracle that proves the packaged closure actually runs. Checked once here against FirstClassErrors.Usage -- a non-empty catalog, no worker-not-located error. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_0132ctxy3scPRq5mjAS8Xjeu --- .../FirstClassErrors.Cli.csproj | 24 ++++++--- maintainers/ReleaseDryRun.en.md | 51 ++++++++++++++++++ maintainers/ReleaseDryRun.fr.md | 53 +++++++++++++++++++ tools/packaging/pack.sh | 24 ++++++++- 4 files changed, 143 insertions(+), 9 deletions(-) diff --git a/FirstClassErrors.Cli/FirstClassErrors.Cli.csproj b/FirstClassErrors.Cli/FirstClassErrors.Cli.csproj index d1ac920..213f3ec 100644 --- a/FirstClassErrors.Cli/FirstClassErrors.Cli.csproj +++ b/FirstClassErrors.Cli/FirstClassErrors.Cli.csproj @@ -10,11 +10,14 @@ + PackAsTool bundles the app and its managed dependency closure under tools//any, so a + `dotnet tool install FirstClassErrors.Cli` yields a runnable `fce`. The GenDoc worker the tool spawns as a + separate process is NOT a managed dependency (ReferenceOutputAssembly=false, below), so PackAsTool does not + embed it on its own. It reaches the package because PackAsTool packs the CLI's *publish* output and the + _PublishDocumentationWorker target (below) drops the worker's closure into that publish output — so it lands + under tools//any next to the executable, where the tool resolves it at runtime + (AppContext.BaseDirectory). tools/packaging/pack.sh packs this alongside the two library packages and asserts + the worker is actually present in the .nupkg, so its bundling is enforced by a test, not hoped for. --> true fce @@ -90,9 +93,14 @@ + build output sits under the sibling bin folder for the current Configuration/TargetFramework, already built via + the ProjectReference above. _CopyDocumentationWorker (AfterTargets="Build") feeds $(OutDir), so running from + source (dev, integration tests) finds the worker; _PublishDocumentationWorker (AfterTargets="Publish") feeds + $(PublishDir), which is what PackAsTool zips into the tool package under tools//any. + Do NOT rely on `dotnet build` or `dotnet publish` to validate that the tool ships its worker: neither exercises + the .nupkg content, so both stay green even if the worker never reaches the package. The real gates are the + worker-present assertion in tools/packaging/pack.sh and the manual tool-install smoke test documented in + maintainers/ReleaseDryRun (install the packed tool, run `fce generate`, expect a non-empty catalog). --> <_DocumentationWorkerFiles Include="$(MSBuildThisFileDirectory)..\FirstClassErrors.GenDoc.Worker\bin\$(Configuration)\$(TargetFramework)\**\*.*" /> diff --git a/maintainers/ReleaseDryRun.en.md b/maintainers/ReleaseDryRun.en.md index 9f0d8f2..23dc7ff 100644 --- a/maintainers/ReleaseDryRun.en.md +++ b/maintainers/ReleaseDryRun.en.md @@ -106,6 +106,57 @@ The actual **push to nuget.org** and the **repository-signed bytes** nuget.org serves cannot be exercised without publishing — nuget.org has no "dry-run push". That final link is only ever validated by a real release. +Neither dry run proves that the **installed `fce` tool actually runs**, either. +`fce generate` does not do all the work in-process: it spawns +`FirstClassErrors.GenDoc.Worker` in a child process and resolves it next to the +installed executable (`AppContext.BaseDirectory`). The worker travels inside the +tool package only because `PackAsTool` packs the CLI's *publish* output and the +`_PublishDocumentationWorker` target drops the worker's closure there — a +mechanism `dotnet build` and `dotnet publish` do not exercise (they lay files out +next to a build binary, never inside the `.nupkg`). See the tool-install smoke +test below. + +## The tool-install smoke test (the `fce` worker) + +`tools/packaging/pack.sh` asserts, for the `cli` train, that the worker **file** +(`FirstClassErrors.GenDoc.Worker.dll`) is present under `tools//any/` in the +`.nupkg`. That guard is real and runs on every packed `cli` train — but a present +file is not a working tool: the worker's closure can still be incomplete (a +missing dependency, a wrong `.runtimeconfig.json`), which the file-presence check +cannot see. The only oracle that proves the packaged closure actually runs is a +real install followed by `fce generate`. + +Run it **at least once before the first `cli-v…` tag**, and again after any change +to the CLI packaging (the `.csproj` worker targets, `PackAsTool`, or the worker's +dependencies): + +```sh +# 1. Build and pack the cli train exactly as the release does. +dotnet build FirstClassErrors.sln -c Release +tools/packaging/pack.sh 0.0.0-workercheck.1 cli # -> artifacts/FirstClassErrors.Cli.0.0.0-workercheck.1.nupkg + +# 2. Install the packed tool globally. Install by PACKAGE ID (FirstClassErrors.Cli); +# that is not the command name (fce). +dotnet tool install --global --add-source ./artifacts FirstClassErrors.Cli --version 0.0.0-workercheck.1 + +# 3. Generate a catalog. Either point at a built, opted-in assembly... +fce generate --assemblies path/to/YourProject.dll --format markdown --output ./out/catalog.md --service-name demo +# ...or at a solution with at least one opted-in project (GenerateErrorDocumentation=true): +fce generate --solution path/to/Your.sln --format markdown --output ./out --service-name demo +# Expect a NON-EMPTY catalog and ZERO "documentation worker could not be located". + +# 4. Clean up. +dotnet tool uninstall --global FirstClassErrors.Cli +``` + +A convenient in-repo target for step 3 is `FirstClassErrors.Usage`: it opts in +(`GenerateErrorDocumentation=true`) and defines documented errors, so after a +Release build its assembly at +`FirstClassErrors.Usage/bin/Release/net10.0/FirstClassErrors.Usage.dll` yields a +non-empty catalog. Two naming traps to avoid: the package file is +`FirstClassErrors.Cli..nupkg` (the `PackageId`), not `fce..nupkg`, +and `dotnet tool install` takes that same package id, not the `fce` command name. + ## Related - [`release`](workflows/release.en.md) — the workflow this rehearses, described diff --git a/maintainers/ReleaseDryRun.fr.md b/maintainers/ReleaseDryRun.fr.md index 717ddf0..9e983d6 100644 --- a/maintainers/ReleaseDryRun.fr.md +++ b/maintainers/ReleaseDryRun.fr.md @@ -113,6 +113,59 @@ nuget.org sert ne peuvent pas être exercés sans publier — nuget.org n'a pas « push à blanc ». Ce dernier maillon n'est jamais validé que par une vraie release. +Aucun dry run ne prouve non plus que l'**outil `fce` installé fonctionne +réellement**. `fce generate` ne fait pas tout le travail en process : il lance +`FirstClassErrors.GenDoc.Worker` dans un process enfant et le résout à côté de +l'exécutable installé (`AppContext.BaseDirectory`). Le worker ne voyage dans le +package du tool que parce que `PackAsTool` empaquette la sortie *publish* du CLI +et que la cible `_PublishDocumentationWorker` y dépose la clôture du worker — un +mécanisme que `dotnet build` et `dotnet publish` n'exercent pas (ils déposent des +fichiers à côté d'un binaire de build, jamais dans le `.nupkg`). Voir le test de +tool-install ci-dessous. + +## Le test de fumée tool-install (le worker `fce`) + +`tools/packaging/pack.sh` vérifie, pour le train `cli`, que le **fichier** du +worker (`FirstClassErrors.GenDoc.Worker.dll`) est présent sous `tools//any/` +dans le `.nupkg`. Ce garde est réel et tourne sur chaque train `cli` packé — mais +un fichier présent n'est pas un tool qui marche : la clôture du worker peut rester +incomplète (une dépendance manquante, un mauvais `.runtimeconfig.json`), ce que le +contrôle de présence ne voit pas. Le seul oracle qui prouve que la clôture +empaquetée s'exécute vraiment est une installation réelle suivie d'un +`fce generate`. + +Lance-le **au moins une fois avant le premier tag `cli-v…`**, et de nouveau après +tout changement de l'empaquetage du CLI (les cibles worker du `.csproj`, +`PackAsTool`, ou les dépendances du worker) : + +```sh +# 1. Builder et packer le train cli exactement comme la release. +dotnet build FirstClassErrors.sln -c Release +tools/packaging/pack.sh 0.0.0-workercheck.1 cli # -> artifacts/FirstClassErrors.Cli.0.0.0-workercheck.1.nupkg + +# 2. Installer le tool packé en global. Installer par IDENTIFIANT DE PACKAGE +# (FirstClassErrors.Cli) ; ce n'est pas le nom de commande (fce). +dotnet tool install --global --add-source ./artifacts FirstClassErrors.Cli --version 0.0.0-workercheck.1 + +# 3. Générer un catalogue. Soit en pointant une assembly buildée et opt-in... +fce generate --assemblies chemin/vers/VotreProjet.dll --format markdown --output ./out/catalog.md --service-name demo +# ...soit une solution avec au moins un projet opt-in (GenerateErrorDocumentation=true) : +fce generate --solution chemin/vers/Votre.sln --format markdown --output ./out --service-name demo +# Attendu : un catalogue NON VIDE et ZÉRO « documentation worker could not be located ». + +# 4. Nettoyer. +dotnet tool uninstall --global FirstClassErrors.Cli +``` + +Une cible pratique du dépôt pour l'étape 3 est `FirstClassErrors.Usage` : elle +opte-in (`GenerateErrorDocumentation=true`) et définit des erreurs documentées, +donc après un build Release son assembly, à +`FirstClassErrors.Usage/bin/Release/net10.0/FirstClassErrors.Usage.dll`, produit +un catalogue non vide. Deux pièges de nommage à éviter : le fichier de package est +`FirstClassErrors.Cli..nupkg` (le `PackageId`), pas +`fce..nupkg`, et `dotnet tool install` prend ce même identifiant de +package, pas le nom de commande `fce`. + ## En rapport - [`release`](workflows/release.fr.md) — le workflow que ceci répète, décrit diff --git a/tools/packaging/pack.sh b/tools/packaging/pack.sh index 346ad65..ed5831d 100755 --- a/tools/packaging/pack.sh +++ b/tools/packaging/pack.sh @@ -38,7 +38,7 @@ case "$scope" in ;; cli) # The `fce` .NET tool (PackAsTool; the GenDoc worker it spawns travels bundled inside the tool - # package). Released on its own cadence and version. + # package -- asserted after the pack, below). Released on its own cadence and version. projects='FirstClassErrors.Cli/FirstClassErrors.Cli.csproj' ;; *) @@ -63,3 +63,25 @@ for package in artifacts/*.nupkg; do exit 1 fi done + +# Positive proof that the fce tool ships its GenDoc worker. `fce generate` does not do the whole job +# in-process: it spawns FirstClassErrors.GenDoc.Worker in a child process (dotnet exec) and resolves it +# next to the installed executable (ResolveWorkerAssemblyPath -> AppContext.BaseDirectory). PackAsTool packs +# the CLI's *publish* output, and _PublishDocumentationWorker (AfterTargets="Publish" in the .csproj) drops +# the worker's closure into that publish directory, so it travels under tools//any inside the package. +# That mechanism is easy to break silently: neither `dotnet build` nor `dotnet publish` exercises the .nupkg +# content, so dropping the target (or PackAsTool ceasing to pack the publish output) would pass every local +# check and only surface as "documentation worker could not be located" on a user's first `fce generate`. +# A pack that stops bundling the worker must fail here, loudly, not in the field. This asserts presence; the +# closure is completeness-checked out of band by the tool-install smoke test in maintainers/ReleaseDryRun. +if [ "$scope" = "cli" ]; then + # The fce tool package carries the CLI's PackageId (FirstClassErrors.Cli), not the ToolCommandName (fce). + for package in artifacts/FirstClassErrors.Cli.*.nupkg; do + if unzip -l "$package" | grep -q 'tools/.*/any/.*FirstClassErrors\.GenDoc\.Worker\.dll'; then + echo "ok: GenDoc worker bundled in $package" + else + echo "error: GenDoc worker missing from the fce tool package $package" >&2 + exit 1 + fi + done +fi