diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 4e59841..61b0680 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -334,7 +334,8 @@ Ordered as the rules are: type, scope, description, body, breaking, issue. ### Adoption This guide is the rule for commits in this repository. Deviating from it -requires a justification — an ADR under `doc/adr/`, or an update to this guide. +requires a justification — an ADR under `maintainers/adr/`, or an update to this +guide. It applies from its adoption on, to every commit created after. Prior history is not rewritten. diff --git a/maintainers/README.fr.md b/maintainers/README.fr.md new file mode 100644 index 0000000..70f92c1 --- /dev/null +++ b/maintainers/README.fr.md @@ -0,0 +1,43 @@ +# Documentation mainteneur + +🌍 🇬🇧 [English](README.md) · 🇫🇷 Français (ce fichier) + +> Documentation pour les **mainteneurs et opérateurs** de FirstClassErrors — +> comment le projet est construit, publié et maintenu en bonne santé. Elle ne +> fait **pas** partie de la documentation utilisateur de la librairie, sous +> [`doc/`](../doc/). La version anglaise est canonique ; les pages françaises sont +> tenues à jour en parallèle. + +## Sommaire + +### [Référence des workflows CI/CD](workflows/README.fr.md) + +Une page par workflow GitHub Actions — à quoi il sert, quand et comment il +s'exécute, ses permissions, et les décisions non évidentes qu'il ne faut pas +modifier sans en comprendre la raison. Commencez par l'[index](workflows/README.fr.md) ; +il documente aussi les conventions transverses (actions épinglées par SHA, +permissions au moindre privilège, timeouts par job, checks *required* comme vrai +barrage). + +### [Répétition de release à blanc (« dry run » manuel)](ReleaseDryRun.fr.md) + +Le runbook opérationnel du dry run manuel via le dispatch `release` : comment le +lancer, ce qu'il touche (et ce qu'il ne touche volontairement pas), et quand +l'utiliser. Il complète les pages [`release`](workflows/release.fr.md) et +[`release-dryrun`](workflows/release-dryrun.fr.md) de la référence, qui décrivent +ces workflows structurellement. Aussi en [anglais](ReleaseDryRun.en.md). + +### [Registres de décision d'architecture (ADR)](adr/) + +Des enregistrements datés des décisions importantes — leur contexte, l'option +retenue et les conséquences. Un ADR est un journal historique : il est *superseded* +par un ADR plus récent, pas édité sur place. + +- [ADR 0001 — Verrouiller le floor Roslyn de l'analyzer](adr/0001-lock-the-analyzer-roslyn-floor.md) + — pourquoi la version de Roslyn de l'analyzer est gelée, ce que le workflow + [`analyzers`](workflows/analyzers.fr.md) fait respecter. *(En anglais uniquement.)* + +## En rapport + +- [`CONTRIBUTING.md`](../CONTRIBUTING.md) — conventions de commit et de pull + request (imposées par le workflow [`commit-lint`](workflows/commit-lint.fr.md)). diff --git a/maintainers/README.md b/maintainers/README.md new file mode 100644 index 0000000..31adedc --- /dev/null +++ b/maintainers/README.md @@ -0,0 +1,41 @@ +# Maintainer documentation + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](README.fr.md) + +> Documentation for **maintainers and operators** of FirstClassErrors — how the +> project is built, released, and kept healthy. It is **not** part of the +> library's user documentation, which lives under [`doc/`](../doc/). The English +> version is canonical; the French pages are kept in sync. + +## Contents + +### [CI/CD workflow reference](workflows/README.md) + +A page per GitHub Actions workflow — what it is for, when and how it runs, its +permissions, and the non-obvious decisions you must not change without +understanding why. Start at the [index](workflows/README.md); it also documents +the cross-cutting conventions (SHA-pinned actions, least-privilege permissions, +per-job timeouts, required checks as the real gate). + +### [Release dry run (manual)](ReleaseDryRun.en.md) + +The operational runbook for the manual `release` dispatch dry run: how to launch +it, what it touches (and what it deliberately does not), and when to use it. It +complements the workflow reference's [`release`](workflows/release.en.md) and +[`release-dryrun`](workflows/release-dryrun.en.md) pages, which describe those +workflows structurally. Also in [French](ReleaseDryRun.fr.md). + +### [Architecture Decision Records](adr/) + +Dated records of significant decisions — their context, the option chosen, and +the consequences. An ADR is a historical log: it is superseded by a newer ADR, not +edited in place. + +- [ADR 0001 — Lock the analyzer Roslyn floor](adr/0001-lock-the-analyzer-roslyn-floor.md) + — why the analyzer's Roslyn version is frozen, enforced by the + [`analyzers`](workflows/analyzers.en.md) workflow. *(English only.)* + +## Related + +- [`CONTRIBUTING.md`](../CONTRIBUTING.md) — commit and pull-request conventions + (enforced by the [`commit-lint`](workflows/commit-lint.en.md) workflow). diff --git a/maintainers/ReleaseDryRun.en.md b/maintainers/ReleaseDryRun.en.md index 79e0fa5..9f0d8f2 100644 --- a/maintainers/ReleaseDryRun.en.md +++ b/maintainers/ReleaseDryRun.en.md @@ -1,5 +1,11 @@ # Release dry run (manual) +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](ReleaseDryRun.fr.md) + +↑ Part of the [maintainer documentation](README.md) · see also the workflow +reference for [`release`](workflows/release.en.md) and +[`release-dryrun`](workflows/release-dryrun.en.md). + > Maintainer / operational documentation. This is **not** part of the library's > user documentation under `doc/`. @@ -99,3 +105,11 @@ the **attestation / OIDC** path that the automatic one deliberately leaves out. 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. + +## Related + +- [`release`](workflows/release.en.md) — the workflow this rehearses, described + structurally (triggers, jobs, the traps in its design). +- [`release-dryrun`](workflows/release-dryrun.en.md) — the automatic, + side-effect-free dry run that runs on every PR and push. +- [Maintainer documentation](README.md) — the index of all maintainer docs. diff --git a/maintainers/ReleaseDryRun.fr.md b/maintainers/ReleaseDryRun.fr.md index bbbea78..717ddf0 100644 --- a/maintainers/ReleaseDryRun.fr.md +++ b/maintainers/ReleaseDryRun.fr.md @@ -1,5 +1,11 @@ # Répétition de release à blanc (« dry run » manuel) +🌍 🇬🇧 [English](ReleaseDryRun.en.md) · 🇫🇷 Français (ce fichier) + +↑ Fait partie de la [documentation mainteneur](README.fr.md) · voir aussi la +référence des workflows pour [`release`](workflows/release.fr.md) et +[`release-dryrun`](workflows/release-dryrun.fr.md). + > Documentation technique / mainteneur. Elle ne fait **pas** partie de la > documentation utilisateur de la librairie, sous `doc/`. @@ -106,3 +112,12 @@ Le **push réel vers nuget.org** et les **octets re-signés par le dépôt** que nuget.org sert ne peuvent pas être exercés sans publier — nuget.org n'a pas de « push à blanc ». Ce dernier maillon n'est jamais validé que par une vraie release. + +## En rapport + +- [`release`](workflows/release.fr.md) — le workflow que ceci répète, décrit + structurellement (déclencheurs, jobs, pièges de sa conception). +- [`release-dryrun`](workflows/release-dryrun.fr.md) — le dry run automatique et + sans effet de bord qui tourne sur chaque PR et push. +- [Documentation mainteneur](README.fr.md) — l'index de toutes les docs + mainteneur. diff --git a/maintainers/workflows/README.fr.md b/maintainers/workflows/README.fr.md new file mode 100644 index 0000000..f950682 --- /dev/null +++ b/maintainers/workflows/README.fr.md @@ -0,0 +1,103 @@ +# Référence des workflows CI/CD + +🌍 🇬🇧 [English](README.md) · 🇫🇷 Français (ce fichier) + +↑ Fait partie de la [documentation mainteneur](../README.fr.md). + +> Documentation mainteneur. Elle décrit les workflows GitHub Actions qui +> construisent, vérifient et publient FirstClassErrors. Elle ne fait **pas** +> partie de la documentation utilisateur de la librairie, sous `doc/`. + +## De quoi il s'agit + +Chaque workflow sous [`.github/workflows/`](../../.github/workflows/) porte une +intention qu'il est facile de casser en « faisant le ménage » : une permission +volontairement étroite, un ordre d'étapes qui protège d'une panne précise, une +version gelée pour une raison produit. Les fichiers de workflow eux-mêmes +contiennent le raisonnement ligne à ligne dans leurs commentaires — ces +commentaires sont la source de vérité la plus proche du code. **Ces pages sont la +couche pédagogique au-dessus :** à quoi sert chaque workflow, quand et comment il +s'exécute, et les quelques points qu'il ne faut pas modifier sans en comprendre +la raison. + +Lisez la page d'un workflow avant d'y toucher. Si la page et le YAML se +contredisent, c'est le YAML qui fait foi — et la page doit être corrigée. + +## Les conventions transverses + +Quelques décisions sont partagées par (presque) tous les workflows. Elles sont +documentées une seule fois ici plutôt que répétées sur chaque page. + +- **Les actions sont épinglées par SHA de commit, pas par tag.** Un tag comme + `@v4` peut être déplacé par son propriétaire vers du nouveau code ; un SHA de + 40 caractères hexadécimaux, non. Chaque `uses:` épingle donc un SHA avec le tag + lisible en commentaire de fin (`# v4`). Quand vous montez une action, changez + **les deux**. L'écosystème `github-actions` de Dependabot propose ces montées. +- **Les `permissions:` partent en lecture seule et s'élargissent par job.** Le + bloc au niveau du workflow est le moindre privilège nécessaire (en général + `contents: read`) ; un job qui doit écrire quelque chose (uploader un SARIF, + publier une release, activer l'auto-merge) redéclare un bloc `permissions:` qui + ajoute *uniquement* ce périmètre. N'élargissez jamais le bloc de haut niveau + pour satisfaire un seul job. +- **Chaque job fixe `timeout-minutes`.** Le défaut GitHub est de six heures ; une + étape bloquée retiendrait sinon un runner tout ce temps. Chaque plafond est + fixé à quelques fois le temps observé, noté en commentaire à côté. +- **`concurrency` annule les runs remplacés.** Pousser deux fois sur la même + branche ou PR annule le run en cours. La seule exception est `release`, qui met + `cancel-in-progress: false` — on ne veut jamais annuler une publication à + moitié faite. +- **Les scanners de sécurité tournent aussi chaque semaine via `schedule`.** + `codeql` et `scorecard` se relancent sur du code inchangé pour que les + requêtes/checks nouvellement livrés soient appliqués même sans push. +- **Les forks ne peuvent pas lire les secrets.** Les workflows qui ont besoin + d'un secret (p. ex. `sonar`) détectent une PR issue d'un fork et s'abstiennent + au lieu d'échouer ; GitHub n'expose pas les secrets du dépôt à une PR ouverte + depuis un fork. +- **Ce sont les checks *required* qui font barrage.** Plusieurs workflows + (`dependency-review`, `dependabot-automerge`) ne font que *signaler* ou + *activer* — ils ne mergent rien seuls. Ce qui bloque réellement un mauvais + merge, c'est la configuration de protection de branche / ruleset sur `main` qui + marque ces checks comme **required**. C'est un réglage de dépôt, pas quelque + chose qu'un workflow peut imposer pour lui-même. + +## Les workflows + +### Build & qualité + +| Workflow | Rôle | +| --- | --- | +| [`ci`](ci.fr.md) | Construit et teste toute la solution sous Linux et Windows, avec couverture. Le barrage principal. | +| [`sonar`](sonar.fr.md) | Analyse SonarQube Cloud — quality gate et remontée de couverture. | +| [`analyzers`](analyzers.fr.md) | Dogfood des analyzers Roslyn embarqués, y compris sur le plus vieux compilateur supporté (le floor Roslyn). | +| [`commit-lint`](commit-lint.fr.md) | Impose la convention Conventional Commits sur chaque commit de PR, via le même script que le hook local. | + +### Sécurité & chaîne d'approvisionnement + +| Workflow | Rôle | +| --- | --- | +| [`codeql`](codeql.fr.md) | Analyse statique GitHub CodeQL pour C#, résultats sur le tableau de bord code-scanning. | +| [`dependency-review`](dependency-review.fr.md) | Bloque une PR qui introduit une dépendance vulnérable connue. | +| [`scorecard`](scorecard.fr.md) | OpenSSF Scorecard — note la posture de sécurité du dépôt et alimente le badge du README. | + +### Release + +| Workflow | Rôle | +| --- | --- | +| [`release`](release.fr.md) | Construit, atteste et publie les packages NuGet sur un tag de version (avec un dry run manuel). | +| [`release-dryrun`](release-dryrun.fr.md) | Répète en continu la partie sans effet de bord de la release (pack + SBOM) sur chaque PR et push. | + +### Maintenance des dépendances + +| Workflow | Rôle | +| --- | --- | +| [`dependabot-automerge`](dependabot-automerge.fr.md) | Active l'auto-merge sur les mises à jour patch/minor de Dependabot ; laisse les majeures à un humain. | + +## Docs mainteneur en rapport + +- [Répétition de release à blanc (« dry run » manuel)](../ReleaseDryRun.fr.md) — + le guide opérationnel du dry run manuel via le dispatch `release`. +- [ADR 0001 — Verrouiller le floor Roslyn de l'analyzer](../adr/0001-lock-the-analyzer-roslyn-floor.md) + — pourquoi la version de Roslyn de l'analyzer est gelée, ce que le workflow + `analyzers` fait respecter. *(Rédigé en anglais.)* +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) — les conventions de commit et de PR + que le workflow `commit-lint` vérifie. diff --git a/maintainers/workflows/README.md b/maintainers/workflows/README.md new file mode 100644 index 0000000..5dd4dbd --- /dev/null +++ b/maintainers/workflows/README.md @@ -0,0 +1,99 @@ +# CI/CD workflow reference + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](README.fr.md) + +↑ Part of the [maintainer documentation](../README.md). + +> Maintainer documentation. This describes the GitHub Actions workflows that +> build, check, and publish FirstClassErrors. It is **not** part of the +> library's user documentation under `doc/`. + +## What this is + +Each workflow under [`.github/workflows/`](../../.github/workflows/) carries a +fair amount of intent that is easy to break by "cleaning it up": a permission +that is narrow on purpose, a step ordering that guards a specific failure, a +version that is frozen for a product reason. The workflow files themselves hold +the line-by-line rationale in comments — those comments are the source of truth +closest to the code. **These pages are the pedagogical layer above them:** what +each workflow is *for*, when and how it runs, and the handful of things you must +not change without understanding why. + +Read the page for a workflow before you touch it. When the page and the YAML +disagree, the YAML wins — and the page should be corrected. + +## The cross-cutting conventions + +A few decisions are shared by (almost) every workflow. They are documented once +here instead of being repeated on every page. + +- **Actions are pinned by commit SHA, not by tag.** A tag like `@v4` can be + moved by its owner to point at new code; a 40-hex SHA cannot. Every `uses:` + therefore pins a SHA with the human-readable tag in a trailing comment + (`# v4`). When you bump an action, change **both**. Dependabot's + `github-actions` ecosystem proposes these bumps. +- **`permissions:` start read-only and widen per job.** The workflow-level block + is the least privilege the workflow needs (usually `contents: read`); a job + that must write something (upload SARIF, publish a release, enable auto-merge) + re-declares a `permissions:` block that adds *only* that scope. Never widen the + top-level block to satisfy one job. +- **Every job sets `timeout-minutes`.** The GitHub default is six hours; a hung + step would otherwise hold a runner for that long. Each cap is set a few times + the observed run time, noted in a comment next to it. +- **`concurrency` cancels superseded runs.** Pushing twice to the same branch or + PR cancels the in-flight run. The one exception is `release`, which sets + `cancel-in-progress: false` — you never want to cancel a half-finished + publish. +- **Security scanners also run weekly on a `schedule`.** `codeql` and + `scorecard` re-run against unchanged code so newly shipped queries/checks are + applied even when nothing was pushed. +- **Forks cannot read secrets.** Workflows that need a secret (e.g. `sonar`) + detect a fork PR and skip rather than fail; GitHub does not expose repository + secrets to a PR raised from a fork. +- **Required checks are the real gate.** Several workflows (`dependency-review`, + `dependabot-automerge`) only *signal* or *enable* — they merge nothing on their + own. What actually blocks a bad merge is the branch-protection / ruleset + configuration on `main` marking these checks as **required**. That is a + repository setting, not something a workflow can enforce for itself. + +## The workflows + +### Build & quality + +| Workflow | Purpose | +| --- | --- | +| [`ci`](ci.en.md) | Build and test the whole solution on Linux and Windows, with coverage. The primary gate. | +| [`sonar`](sonar.en.md) | SonarQube Cloud analysis — quality gate and coverage reporting. | +| [`analyzers`](analyzers.en.md) | Dogfood the bundled Roslyn analyzers, including on the oldest supported compiler (the Roslyn floor). | +| [`commit-lint`](commit-lint.en.md) | Enforce the Conventional Commits convention on every PR commit, using the same script as the local hook. | + +### Security & supply chain + +| Workflow | Purpose | +| --- | --- | +| [`codeql`](codeql.en.md) | GitHub CodeQL static analysis for C#, results on the code-scanning dashboard. | +| [`dependency-review`](dependency-review.en.md) | Block a PR that introduces a known-vulnerable dependency. | +| [`scorecard`](scorecard.en.md) | OpenSSF Scorecard — scores the repo's security posture and powers the README badge. | + +### Release + +| Workflow | Purpose | +| --- | --- | +| [`release`](release.en.md) | Build, attest, and publish the NuGet packages on a version tag (with a manual dry run). | +| [`release-dryrun`](release-dryrun.en.md) | Continuously rehearse the side-effect-free part of the release (pack + SBOM) on every PR and push. | + +### Dependency maintenance + +| Workflow | Purpose | +| --- | --- | +| [`dependabot-automerge`](dependabot-automerge.en.md) | Enable auto-merge on Dependabot patch/minor updates; leave majors for a human. | + +## Related maintainer docs + +- [Release dry run (manual)](../ReleaseDryRun.en.md) — the operational guide to + the manual `release` dispatch dry run. +- [ADR 0001 — Lock the analyzer Roslyn floor](../adr/0001-lock-the-analyzer-roslyn-floor.md) + — why the analyzer's Roslyn version is frozen, which the `analyzers` workflow + enforces. +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) — the commit and PR conventions the + `commit-lint` workflow checks. diff --git a/maintainers/workflows/analyzers.en.md b/maintainers/workflows/analyzers.en.md new file mode 100644 index 0000000..8ac4150 --- /dev/null +++ b/maintainers/workflows/analyzers.en.md @@ -0,0 +1,86 @@ +# `analyzers` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](analyzers.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/analyzers.yml`](../../.github/workflows/analyzers.yml) + +## What it is for + +The library ships Roslyn analyzers (`FCExxx`) **bundled inside the NuGet +package**. This workflow proves two things that ordinary CI does not: + +1. **Dogfood** — the analyzers actually run and flag nothing they shouldn't when + built against the sample project that references them. +2. **Floor check** — the *shipped* analyzer, packed exactly as a consumer + receives it, **loads and runs under the oldest supported compiler** (Roslyn + 4.8.0 == .NET 8 SDK / Visual Studio 2022 17.8). + +The floor check exists because a bundled analyzer compiled against a newer +Roslyn fails to load (`CS8032`) on older SDKs/IDEs — silently degrading the +product for those users. See +[ADR 0001 — Lock the analyzer Roslyn floor](../adr/0001-lock-the-analyzer-roslyn-floor.md) +for the full rationale; this workflow is its CI enforcement. + +## When it runs + +- On every **push to `main`**, **pull request targeting `main`**, and on demand + via **`workflow_dispatch`**. + +## How it runs + +Two jobs: + +- **`dogfood`** — builds `FirstClassErrors.Usage` with the analyzers wired in + (`OutputItemType=Analyzer`); fails on any `Error`-severity `FCExxx` + diagnostic. (The analyzer *unit tests* run inside [`ci`](ci.en.md), which + builds and tests the whole solution.) +- **`floor`** — the real contract test: + 1. Set up **two SDKs**: the release SDK (10.0.x, the one `release` packs with) + and the floor SDK (8.0.100, Roslyn 4.8). + 2. **Pack** `FirstClassErrors` under the *release* SDK — the exact artifact a + consumer restores. + 3. **Consume** that package from `tools/floor-check/`, whose nested + `global.json` pins the build to the *floor* SDK. + 4. **Prove the analyzer loaded** by grepping the `ReportAnalyzer` log for a + fully-qualified analyzer *type*. + +## Permissions & security + +`contents: read` only — it builds and packs locally, publishes nothing. + +## Handle with care + +This job is dense because each line closes a specific hole. Before editing it, +read the comments in the YAML — they are the source of truth. The traps: + +- **The pack runs under the release SDK, the consume under the floor SDK.** + Packing under the floor SDK would test an analyzer nobody ships and would pin + the library to C# 12. The split is the whole point. +- **`FLOORCHECK_VERSION` carries a `run_number.run_attempt` suffix** so every run + produces a version NuGet has never cached, forcing the consume step to restore + *this run's* freshly packed `.nupkg` instead of a stale copy. `FloorCheck.csproj` + pins that **exact** version (not a float), so it can never silently resolve a + future stable `FirstClassErrors` from nuget.org. +- **SDK selection is CWD-based.** The consume step picks the floor SDK *because* + it runs from `tools/floor-check/` with a nested `global.json` (`rollForward: + disable`). Move the step out of that directory and it silently builds on the + wrong SDK. +- **`ReportAnalyzer=true` + `-v detailed` + `--no-incremental` are all + required** to make Roslyn's per-analyzer table reach the log. Drop any one and + the "prove it loaded" grep has nothing to match. +- **The grep matches an analyzer *type* (`…SomethingAnalyzer`), not the assembly + name.** The assembly name appears in ordinary build lines even when the + analyzer never loaded; only the type appears in the `ReportAnalyzer` table. + A never-loaded analyzer would otherwise leave the build green. + +**To raise the floor deliberately**, follow the procedure in ADR 0001 — it is a +product decision, not a routine bump (which is why Dependabot is configured to +ignore `Microsoft.CodeAnalysis.*`). + +## Related + +- [ADR 0001 — Lock the analyzer Roslyn floor](../adr/0001-lock-the-analyzer-roslyn-floor.md) +- [`ci`](ci.en.md) — runs the analyzer unit-test suite as part of the full solution. diff --git a/maintainers/workflows/analyzers.fr.md b/maintainers/workflows/analyzers.fr.md new file mode 100644 index 0000000..0e0188d --- /dev/null +++ b/maintainers/workflows/analyzers.fr.md @@ -0,0 +1,90 @@ +# Workflow `analyzers` + +🌍 🇬🇧 [English](analyzers.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/analyzers.yml`](../../.github/workflows/analyzers.yml) + +## À quoi il sert + +La librairie livre des analyzers Roslyn (`FCExxx`) **embarqués dans le package +NuGet**. Ce workflow prouve deux choses que la CI ordinaire ne prouve pas : + +1. **Dogfood** — les analyzers tournent réellement et ne signalent rien + d'indu quand on construit le projet d'exemple qui les référence. +2. **Floor check** — l'analyzer *tel que livré*, packagé exactement comme un + consommateur le reçoit, **se charge et s'exécute sous le plus vieux + compilateur supporté** (Roslyn 4.8.0 == SDK .NET 8 / Visual Studio 2022 17.8). + +Le floor check existe parce qu'un analyzer embarqué compilé contre un Roslyn plus +récent ne se charge pas (`CS8032`) sur les SDK/IDE plus anciens — dégradant +silencieusement le produit pour ces utilisateurs. Voir +[ADR 0001 — Verrouiller le floor Roslyn de l'analyzer](../adr/0001-lock-the-analyzer-roslyn-floor.md) +pour le raisonnement complet *(rédigé en anglais)* ; ce workflow en est la mise +en application côté CI. + +## Quand il s'exécute + +- À chaque **push sur `main`**, **pull request visant `main`**, et à la demande + via **`workflow_dispatch`**. + +## Comment il s'exécute + +Deux jobs : + +- **`dogfood`** — construit `FirstClassErrors.Usage` avec les analyzers branchés + (`OutputItemType=Analyzer`) ; échoue sur tout diagnostic `FCExxx` de sévérité + `Error`. (Les *tests unitaires* de l'analyzer tournent dans [`ci`](ci.fr.md), + qui build et teste toute la solution.) +- **`floor`** — le vrai test de contrat : + 1. Installer **deux SDK** : le SDK de release (10.0.x, celui avec lequel + `release` packe) et le SDK floor (8.0.100, Roslyn 4.8). + 2. **Packer** `FirstClassErrors` sous le SDK *de release* — l'artefact exact + qu'un consommateur restaure. + 3. **Consommer** ce package depuis `tools/floor-check/`, dont le `global.json` + imbriqué épingle le build au SDK *floor*. + 4. **Prouver que l'analyzer s'est chargé** en grep-ant le log `ReportAnalyzer` + à la recherche d'un *type* d'analyzer pleinement qualifié. + +## Permissions & sécurité + +`contents: read` seulement — il build et packe localement, ne publie rien. + +## À manipuler avec précaution + +Ce job est dense parce que chaque ligne bouche un trou précis. Avant de +l'éditer, lisez les commentaires du YAML — ils font foi. Les pièges : + +- **Le pack tourne sous le SDK de release, la consommation sous le SDK floor.** + Packer sous le SDK floor testerait un analyzer que personne ne livre et + épinglerait la librairie à C# 12. Ce découpage est tout l'intérêt. +- **`FLOORCHECK_VERSION` porte un suffixe `run_number.run_attempt`** pour que + chaque run produise une version que NuGet n'a jamais mise en cache, forçant + l'étape de consommation à restaurer le `.nupkg` fraîchement packé *de ce run* + plutôt qu'une copie périmée. `FloorCheck.csproj` épingle cette version + **exacte** (pas un flottant), donc il ne peut jamais résoudre silencieusement + un futur `FirstClassErrors` stable depuis nuget.org. +- **La sélection du SDK dépend du répertoire courant.** L'étape de consommation + choisit le SDK floor *parce qu*'elle tourne depuis `tools/floor-check/` avec un + `global.json` imbriqué (`rollForward: disable`). Sortez l'étape de ce + répertoire et elle build silencieusement sur le mauvais SDK. +- **`ReportAnalyzer=true` + `-v detailed` + `--no-incremental` sont tous + requis** pour faire remonter la table par-analyzer de Roslyn dans le log. + Retirez-en un et le grep « prouve qu'il s'est chargé » n'a plus rien à + matcher. +- **Le grep matche un *type* d'analyzer (`…QuelqueChoseAnalyzer`), pas le nom de + l'assembly.** Le nom de l'assembly apparaît dans des lignes de build ordinaires + même si l'analyzer ne s'est jamais chargé ; seul le type apparaît dans la table + `ReportAnalyzer`. Un analyzer jamais chargé laisserait sinon le build vert. + +**Pour relever le floor délibérément**, suivez la procédure de l'ADR 0001 — +c'est une décision produit, pas une montée de routine (raison pour laquelle +Dependabot est configuré pour ignorer `Microsoft.CodeAnalysis.*`). + +## En rapport + +- [ADR 0001 — Verrouiller le floor Roslyn de l'analyzer](../adr/0001-lock-the-analyzer-roslyn-floor.md) +- [`ci`](ci.fr.md) — exécute la suite de tests unitaires de l'analyzer dans le + cadre de la solution complète. diff --git a/maintainers/workflows/ci.en.md b/maintainers/workflows/ci.en.md new file mode 100644 index 0000000..e547720 --- /dev/null +++ b/maintainers/workflows/ci.en.md @@ -0,0 +1,64 @@ +# `ci` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](ci.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) + +## What it is for + +`ci` is the primary gate: it builds the whole solution and runs the entire test +suite, on **both Linux and Windows**, and collects code coverage. If a change +breaks the build or a test on either platform, this is where it shows. + +The cross-platform matrix is not ceremony. The documentation generator spawns +its worker as a **separate process** and manipulates file-system paths, so the +solution genuinely behaves differently on Windows and Linux — both legs are +exercised on purpose. + +## When it runs + +- On every **push to `main`**. +- On every **pull request targeting `main`**. +- On demand via **`workflow_dispatch`**. + +## How it runs + +One job, `build-test`, on a `[ubuntu-latest, windows-latest]` matrix: + +1. Checkout, then set up the .NET SDK. +2. `dotnet restore` → `dotnet build -c Release` → `dotnet test -c Release`. +3. Tests collect OpenCover coverage via `coverlet.collector`, configured by + [`coverage.runsettings`](../../coverage.runsettings), into + `artifacts/coverage//coverage.opencover.xml`. +4. The coverage report is uploaded as a per-OS artifact. + +## Permissions & security + +`contents: read` only — the workflow just checks out and builds. It stores no +secrets and needs no write scope. + +## Handle with care + +- **`fail-fast: false` is deliberate.** It forces both matrix legs to run to + completion so a platform-specific failure is always visible; do not remove it + to "save minutes". +- **The coverage artifact name is per-OS** (`coverage-${{ matrix.os }}`). + Uploading two matrix legs under the same artifact name would clash — keep the + name parameterised. +- **`if-no-files-found: error`** is intentional: a silent "no coverage produced" + would let a broken coverage setup pass unnoticed. It should fail. +- **This is the enforcement point for the warning ratchet.** The + `TreatWarningsAsErrors` / `MSBuildTreatWarningsAsErrors` promotion (see + [`Directory.Build.props`](../../Directory.Build.props)) is scoped to CI and is + *enforced here*, on both OS legs. The `sonar` workflow deliberately disables it + for its own analysis build — so `ci`, not `sonar`, is the gate that must stay + green on warnings. + +## Related + +- [`sonar`](sonar.en.md) reuses the same `coverage.runsettings` so its coverage + report matches this one. +- [`analyzers`](analyzers.en.md) covers the analyzer dogfood that `ci` does not. diff --git a/maintainers/workflows/ci.fr.md b/maintainers/workflows/ci.fr.md new file mode 100644 index 0000000..3c0b267 --- /dev/null +++ b/maintainers/workflows/ci.fr.md @@ -0,0 +1,67 @@ +# Workflow `ci` + +🌍 🇬🇧 [English](ci.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/ci.yml`](../../.github/workflows/ci.yml) + +## À quoi il sert + +`ci` est le barrage principal : il construit toute la solution et exécute toute +la suite de tests, sous **Linux et Windows**, et collecte la couverture de code. +Si un changement casse le build ou un test sur l'une des deux plateformes, c'est +ici que ça se voit. + +La matrice multiplateforme n'est pas décorative. Le générateur de documentation +lance son worker dans un **processus séparé** et manipule des chemins de système +de fichiers ; la solution se comporte donc réellement différemment sous Windows +et Linux — les deux branches sont exercées à dessein. + +## Quand il s'exécute + +- À chaque **push sur `main`**. +- À chaque **pull request visant `main`**. +- À la demande via **`workflow_dispatch`**. + +## Comment il s'exécute + +Un seul job, `build-test`, sur une matrice `[ubuntu-latest, windows-latest]` : + +1. Checkout, puis installation du SDK .NET. +2. `dotnet restore` → `dotnet build -c Release` → `dotnet test -c Release`. +3. Les tests collectent la couverture OpenCover via `coverlet.collector`, + configuré par [`coverage.runsettings`](../../coverage.runsettings), dans + `artifacts/coverage//coverage.opencover.xml`. +4. Le rapport de couverture est uploadé en artefact, un par OS. + +## Permissions & sécurité + +`contents: read` seulement — le workflow ne fait que checkout et build. Il ne +stocke aucun secret et n'a besoin d'aucun périmètre en écriture. + +## À manipuler avec précaution + +- **`fail-fast: false` est volontaire.** Il force les deux branches de la matrice + à aller au bout, pour qu'une panne propre à une plateforme soit toujours + visible ; ne le retirez pas pour « gagner des minutes ». +- **Le nom de l'artefact de couverture est par OS** (`coverage-${{ matrix.os }}`). + Uploader les deux branches sous le même nom d'artefact provoquerait une + collision — gardez le nom paramétré. +- **`if-no-files-found: error`** est intentionnel : un « aucune couverture + produite » silencieux laisserait passer une configuration de couverture cassée. + Ça doit échouer. +- **C'est ici que le cliquet de warnings est imposé.** La promotion + `TreatWarningsAsErrors` / `MSBuildTreatWarningsAsErrors` (voir + [`Directory.Build.props`](../../Directory.Build.props)) est cantonnée à la CI et + *imposée ici*, sur les deux branches OS. Le workflow `sonar` la désactive + volontairement pour son propre build d'analyse — c'est donc `ci`, et non + `sonar`, le barrage qui doit rester vert sur les warnings. + +## En rapport + +- [`sonar`](sonar.fr.md) réutilise le même `coverage.runsettings` pour que son + rapport de couverture corresponde à celui-ci. +- [`analyzers`](analyzers.fr.md) couvre le dogfood des analyzers que `ci` ne fait + pas. diff --git a/maintainers/workflows/codeql.en.md b/maintainers/workflows/codeql.en.md new file mode 100644 index 0000000..cf9ae08 --- /dev/null +++ b/maintainers/workflows/codeql.en.md @@ -0,0 +1,60 @@ +# `codeql` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](codeql.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/codeql.yml`](../../.github/workflows/codeql.yml) + +## What it is for + +`codeql` runs GitHub's CodeQL static analysis over the C# code and uploads the +findings to the repository's **code-scanning dashboard** (the Security tab). It +is the semantic security scanner: it looks for vulnerability patterns +(injection, unsafe deserialization, etc.) rather than style issues. It powers +the `codeql` badge in the README. + +## When it runs + +- On every **push to `main`** and **pull request targeting `main`**. +- **Weekly** on a `schedule` (`cron: 17 6 * * 1`), so newly shipped CodeQL + queries run against unchanged code. +- On demand via **`workflow_dispatch`**. + +## How it runs + +One job, `analyze`: + +1. Checkout. +2. **Initialize CodeQL** for `csharp` with **`build-mode: none`** (buildless + extraction). +3. **Perform CodeQL analysis** and upload the results. + +## Permissions & security + +The workflow defaults to `contents: read`; the `analyze` job adds +`security-events: write` (to upload results to the dashboard) and `actions: +read` (needed by the action on private repositories, harmless on public ones). + +## Handle with care + +- **`build-mode: none` is a deliberate choice.** Buildless extraction needs no + .NET SDK or build step, and it sidesteps compiler-tracing problems on a very + new SDK. If you ever want deeper data-flow analysis, switch to `manual` and add + an explicit `dotnet build` step — do not expect `autobuild` to be a free + upgrade. +- **The two CodeQL steps must stay on the same action SHA.** `init` and + `analyze` come from `github/codeql-action`; bump them together (and the + `upload-sarif` reference in [`scorecard`](scorecard.en.md), which uses the same + action family). +- **The weekly `schedule` is not redundant.** It applies query updates to code + that has not changed; removing it means new query classes are never run until + the next push. + +## Related + +- [`scorecard`](scorecard.en.md) — also uploads SARIF to code-scanning, via the + same `github/codeql-action/upload-sarif` action. +- [`dependency-review`](dependency-review.en.md) — the dependency-side security + gate, complementary to this code-side one. diff --git a/maintainers/workflows/codeql.fr.md b/maintainers/workflows/codeql.fr.md new file mode 100644 index 0000000..287f17c --- /dev/null +++ b/maintainers/workflows/codeql.fr.md @@ -0,0 +1,62 @@ +# Workflow `codeql` + +🌍 🇬🇧 [English](codeql.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/codeql.yml`](../../.github/workflows/codeql.yml) + +## À quoi il sert + +`codeql` exécute l'analyse statique CodeQL de GitHub sur le code C# et remonte +les résultats au **tableau de bord code-scanning** du dépôt (l'onglet Security). +C'est le scanner de sécurité sémantique : il cherche des motifs de vulnérabilité +(injection, désérialisation non sûre, etc.) plutôt que des problèmes de style. Il +alimente le badge `codeql` du README. + +## Quand il s'exécute + +- À chaque **push sur `main`** et **pull request visant `main`**. +- **Chaque semaine** via `schedule` (`cron: 17 6 * * 1`), pour que les requêtes + CodeQL nouvellement livrées tournent sur du code inchangé. +- À la demande via **`workflow_dispatch`**. + +## Comment il s'exécute + +Un seul job, `analyze` : + +1. Checkout. +2. **Initialize CodeQL** pour `csharp` avec **`build-mode: none`** (extraction + sans build). +3. **Perform CodeQL analysis** et upload des résultats. + +## Permissions & sécurité + +Le workflow est par défaut en `contents: read` ; le job `analyze` ajoute +`security-events: write` (pour uploader les résultats au tableau de bord) et +`actions: read` (nécessaire à l'action sur les dépôts privés, inoffensif sur les +publics). + +## À manipuler avec précaution + +- **`build-mode: none` est un choix délibéré.** L'extraction sans build ne + demande aucun SDK .NET ni étape de build, et elle contourne les problèmes de + traçage du compilateur sur un SDK très récent. Si un jour vous voulez une + analyse de flux de données plus poussée, passez à `manual` et ajoutez une étape + `dotnet build` explicite — ne comptez pas sur `autobuild` comme sur une + amélioration gratuite. +- **Les deux étapes CodeQL doivent rester sur le même SHA d'action.** `init` et + `analyze` viennent de `github/codeql-action` ; montez-les ensemble (ainsi que + la référence `upload-sarif` dans [`scorecard`](scorecard.fr.md), qui utilise la + même famille d'action). +- **Le `schedule` hebdomadaire n'est pas redondant.** Il applique les mises à + jour de requêtes à du code qui n'a pas changé ; le retirer signifie que les + nouvelles classes de requêtes ne tournent qu'au prochain push. + +## En rapport + +- [`scorecard`](scorecard.fr.md) — uploade aussi du SARIF vers code-scanning, via + la même action `github/codeql-action/upload-sarif`. +- [`dependency-review`](dependency-review.fr.md) — le barrage de sécurité côté + dépendances, complémentaire de celui-ci côté code. diff --git a/maintainers/workflows/commit-lint.en.md b/maintainers/workflows/commit-lint.en.md new file mode 100644 index 0000000..8c8e42b --- /dev/null +++ b/maintainers/workflows/commit-lint.en.md @@ -0,0 +1,60 @@ +# `commit-lint` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](commit-lint.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/commit-lint.yml`](../../.github/workflows/commit-lint.yml) + +## What it is for + +It enforces the repository's commit-message convention (Conventional Commits, as +specified in [`CONTRIBUTING.md`](../../CONTRIBUTING.md)) on **every non-merge +commit of a pull request**. It is the server-side backstop for the local +`commit-msg` hook: a contributor who bypasses the hook with +`git commit --no-verify` is still caught here. + +Both the hook and this workflow call the **same script**, +[`tools/commit-lint/lint-commit-message.sh`](../../tools/commit-lint/lint-commit-message.sh), +so there is exactly one definition of "a valid commit message" and CI can never +drift from the local check. + +## When it runs + +- On every **pull request** (any base branch). + +## How it runs + +One job, `Conventional commits`: + +1. Checkout with **`fetch-depth: 0`** — the full history is needed to enumerate + the PR's commits. +2. Enumerate non-merge commits in `base..head` (`git rev-list --no-merges`) and + pipe each message through the lint script with `--ci`. Merge commits are + generated by GitHub and are exempt. +3. If any commit fails, the job prints which ones and exits non-zero, pointing + the contributor at an interactive rebase to fix them. + +## Permissions & security + +`contents: read` only. + +## Handle with care + +- **The script is shared with the local hook — change the rule in one place.** + If you adjust the accepted types/scopes, edit + `tools/commit-lint/lint-commit-message.sh`; both the hook and this workflow + pick it up. Do not encode a second, divergent rule in the workflow. +- **`--no-merges` is intentional.** GitHub's own merge commits do not follow the + convention and must stay exempt. +- **`fetch-depth: 0` is required.** A shallow checkout would not contain the + PR's commit range, so the lint would silently pass over nothing. +- **This check only helps if it is required.** As with the other quality checks, + it blocks a merge only when branch protection on `main` marks it **required**. + +## Related + +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) — the commit and PR conventions this + workflow enforces. Enable the local hook once per clone with + `git config core.hooksPath .githooks`. diff --git a/maintainers/workflows/commit-lint.fr.md b/maintainers/workflows/commit-lint.fr.md new file mode 100644 index 0000000..8058921 --- /dev/null +++ b/maintainers/workflows/commit-lint.fr.md @@ -0,0 +1,61 @@ +# Workflow `commit-lint` + +🌍 🇬🇧 [English](commit-lint.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/commit-lint.yml`](../../.github/workflows/commit-lint.yml) + +## À quoi il sert + +Il impose la convention de message de commit du dépôt (Conventional Commits, +telle que spécifiée dans [`CONTRIBUTING.md`](../../CONTRIBUTING.md)) sur **chaque +commit non-merge d'une pull request**. C'est le filet de sécurité côté serveur du +hook local `commit-msg` : un contributeur qui contourne le hook avec +`git commit --no-verify` est quand même rattrapé ici. + +Le hook et ce workflow appellent le **même script**, +[`tools/commit-lint/lint-commit-message.sh`](../../tools/commit-lint/lint-commit-message.sh), +donc il existe exactement une définition de « message de commit valide » et la CI +ne peut jamais diverger du contrôle local. + +## Quand il s'exécute + +- À chaque **pull request** (quelle que soit la branche de base). + +## Comment il s'exécute + +Un seul job, `Conventional commits` : + +1. Checkout avec **`fetch-depth: 0`** — l'historique complet est nécessaire pour + énumérer les commits de la PR. +2. Énumérer les commits non-merge de `base..head` (`git rev-list --no-merges`) et + passer chaque message au script de lint avec `--ci`. Les commits de merge sont + générés par GitHub et sont exemptés. +3. Si un commit échoue, le job affiche lesquels et sort en non-zéro, en orientant + le contributeur vers un rebase interactif pour les corriger. + +## Permissions & sécurité + +`contents: read` seulement. + +## À manipuler avec précaution + +- **Le script est partagé avec le hook local — changez la règle à un seul + endroit.** Si vous ajustez les types/scopes acceptés, éditez + `tools/commit-lint/lint-commit-message.sh` ; le hook comme ce workflow le + reprennent. N'encodez pas une seconde règle divergente dans le workflow. +- **`--no-merges` est intentionnel.** Les commits de merge de GitHub ne suivent + pas la convention et doivent rester exemptés. +- **`fetch-depth: 0` est requis.** Un checkout superficiel ne contiendrait pas la + plage de commits de la PR ; le lint passerait alors silencieusement sur rien. +- **Ce check n'aide que s'il est *required*.** Comme les autres checks de + qualité, il ne bloque un merge que lorsque la protection de branche sur `main` + le marque **required**. + +## En rapport + +- [`CONTRIBUTING.md`](../../CONTRIBUTING.md) — les conventions de commit et de PR + que ce workflow impose. Activez le hook local une fois par clone avec + `git config core.hooksPath .githooks`. diff --git a/maintainers/workflows/dependabot-automerge.en.md b/maintainers/workflows/dependabot-automerge.en.md new file mode 100644 index 0000000..05796be --- /dev/null +++ b/maintainers/workflows/dependabot-automerge.en.md @@ -0,0 +1,61 @@ +# `dependabot-automerge` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](dependabot-automerge.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/dependabot-automerge.yml`](../../.github/workflows/dependabot-automerge.yml) + +## What it is for + +For Dependabot pull requests, this workflow **enables GitHub auto-merge on patch +and minor updates**, so they merge on their own once the required checks pass. +**Major** updates are deliberately left untouched, to wait for human review. It +is the low-friction lane of the dependency-update policy: routine bumps do not +need a human, risky ones do. + +The Dependabot configuration itself (which ecosystems, schedule, ignored +packages) lives in [`.github/dependabot.yml`](../../.github/dependabot.yml), not +here. + +## When it runs + +- On every **pull request targeting `main`**, but the job is gated on + `github.actor == 'dependabot[bot]'`, so it acts only on Dependabot's PRs. + +## How it runs + +One job, `automerge`: + +1. `dependabot/fetch-metadata` reads the update type (patch / minor / major). +2. For **patch or minor** updates, `gh pr merge --auto` enables auto-merge. Major + updates fall through the condition and stay open. + +## Permissions & security + +Workflow default `contents: read`; the job widens to `contents: write` and +`pull-requests: write` — the scopes needed to enable auto-merge on the PR. + +## Handle with care + +- **This workflow only *enables* auto-merge; it does not decide when to merge.** + GitHub merges the PR only once the branch's **required** status checks pass. + **Without a branch-protection rule on `main` that marks the CI checks + required, auto-merge would merge immediately** — before CI. The required checks + are the safety gate, not this workflow. This is the single most important thing + to understand before relying on it. +- **The `major` exclusion is intentional.** Only `semver-patch` and + `semver-minor` get auto-merge; majors are left for a human because they are the + ones most likely to break. Do not broaden the condition to majors. +- **The actor guard matters.** `if: github.actor == 'dependabot[bot]'` keeps the + elevated `contents: write` / `pull-requests: write` path from running on + human PRs. + +## Related + +- [`.github/dependabot.yml`](../../.github/dependabot.yml) — what Dependabot + updates and what it ignores (e.g. the frozen `Microsoft.CodeAnalysis.*`; see + [`analyzers`](analyzers.en.md)). +- [`dependency-review`](dependency-review.en.md) — the PR-time vulnerability gate + that a Dependabot PR also passes through. diff --git a/maintainers/workflows/dependabot-automerge.fr.md b/maintainers/workflows/dependabot-automerge.fr.md new file mode 100644 index 0000000..abdd0d0 --- /dev/null +++ b/maintainers/workflows/dependabot-automerge.fr.md @@ -0,0 +1,65 @@ +# Workflow `dependabot-automerge` + +🌍 🇬🇧 [English](dependabot-automerge.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/dependabot-automerge.yml`](../../.github/workflows/dependabot-automerge.yml) + +## À quoi il sert + +Pour les pull requests de Dependabot, ce workflow **active l'auto-merge de GitHub +sur les mises à jour patch et minor**, pour qu'elles se mergent d'elles-mêmes une +fois les checks required passés. Les mises à jour **majeures** sont +volontairement laissées telles quelles, en attente d'une revue humaine. C'est la +voie à faible friction de la politique de mise à jour des dépendances : les +montées de routine n'ont pas besoin d'un humain, les risquées si. + +La configuration de Dependabot elle-même (quels écosystèmes, planning, packages +ignorés) vit dans [`.github/dependabot.yml`](../../.github/dependabot.yml), pas +ici. + +## Quand il s'exécute + +- À chaque **pull request visant `main`**, mais le job est conditionné à + `github.actor == 'dependabot[bot]'`, donc il n'agit que sur les PR de + Dependabot. + +## Comment il s'exécute + +Un seul job, `automerge` : + +1. `dependabot/fetch-metadata` lit le type de mise à jour (patch / minor / major). +2. Pour les mises à jour **patch ou minor**, `gh pr merge --auto` active + l'auto-merge. Les majeures ne passent pas la condition et restent ouvertes. + +## Permissions & sécurité + +Défaut du workflow `contents: read` ; le job élargit à `contents: write` et +`pull-requests: write` — les périmètres nécessaires pour activer l'auto-merge sur +la PR. + +## À manipuler avec précaution + +- **Ce workflow ne fait qu'*activer* l'auto-merge ; il ne décide pas quand + merger.** GitHub ne merge la PR qu'une fois les checks de statut **required** + de la branche passés. **Sans une règle de protection de branche sur `main` qui + marque les checks CI comme required, l'auto-merge mergerait immédiatement** — + avant la CI. Les checks required sont le garde-fou de sécurité, pas ce workflow. + C'est le point le plus important à comprendre avant de s'y fier. +- **L'exclusion des `major` est intentionnelle.** Seuls `semver-patch` et + `semver-minor` obtiennent l'auto-merge ; les majeures sont laissées à un humain + parce que ce sont elles qui risquent le plus de casser. N'élargissez pas la + condition aux majeures. +- **Le garde-fou sur l'acteur compte.** `if: github.actor == 'dependabot[bot]'` + empêche le chemin élevé `contents: write` / `pull-requests: write` de tourner + sur des PR humaines. + +## En rapport + +- [`.github/dependabot.yml`](../../.github/dependabot.yml) — ce que Dependabot met + à jour et ce qu'il ignore (p. ex. les `Microsoft.CodeAnalysis.*` gelés ; voir + [`analyzers`](analyzers.fr.md)). +- [`dependency-review`](dependency-review.fr.md) — le barrage de vulnérabilité au + moment de la PR, par lequel une PR Dependabot passe aussi. diff --git a/maintainers/workflows/dependency-review.en.md b/maintainers/workflows/dependency-review.en.md new file mode 100644 index 0000000..461b061 --- /dev/null +++ b/maintainers/workflows/dependency-review.en.md @@ -0,0 +1,57 @@ +# `dependency-review` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](dependency-review.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/dependency-review.yml`](../../.github/workflows/dependency-review.yml) + +## What it is for + +`dependency-review` blocks a pull request that **introduces a known-vulnerable +dependency**. It diffs the base-vs-head dependency graph and fails the PR if a +change pulls in a package with an advisory at or above the configured severity. + +It is the **PR-time** complement to Dependabot: Dependabot only reacts *after* a +vulnerable dependency is already on `main`, whereas this catches the problem at +the moment a PR would add it — which is exactly when it is cheapest to fix. + +## When it runs + +- On every **pull request targeting `main`**. + +## How it runs + +One job, `review`: checkout, then run +`actions/dependency-review-action` with `fail-on-severity: moderate`. + +## Permissions & security + +`contents: read` only — the action reads the repository's dependency graph. It +posts **no** PR comment (that would need `pull-requests: write`); the failed +check is the signal. + +## Handle with care + +- **It requires the repository's Dependency graph to be enabled.** This is a + GitHub repository setting, not something the workflow can turn on. If it is + off, the action fails with *"Dependency review is not supported on this + repository… ensure that Dependency graph is enabled"* — that is a + configuration error, not a workflow bug. (On a private repo it also needs + Advanced Security.) +- **It only sees changes the PR makes.** A CVE published overnight against an + *existing* dependency does not fail this check — that stays a warning through + the NuGet audit (see [`Directory.Build.props`](../../Directory.Build.props)). + This workflow blocks only at the point of introduction, on purpose. +- **`fail-on-severity: moderate` is the tuning knob.** Lower it to `low` to be + stricter, raise it to `high` to be laxer. Because it only inspects the PR's own + dependency changes, `moderate` is a genuine gate rather than noise. +- **It gates nothing unless it is required.** As with the other checks, it must + be marked **required** in branch protection on `main` to actually block a merge. + +## Related + +- [`codeql`](codeql.en.md) — the code-side security scanner. +- [`dependabot-automerge`](dependabot-automerge.en.md) — the post-merge + dependency-update side. diff --git a/maintainers/workflows/dependency-review.fr.md b/maintainers/workflows/dependency-review.fr.md new file mode 100644 index 0000000..03c9225 --- /dev/null +++ b/maintainers/workflows/dependency-review.fr.md @@ -0,0 +1,61 @@ +# Workflow `dependency-review` + +🌍 🇬🇧 [English](dependency-review.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/dependency-review.yml`](../../.github/workflows/dependency-review.yml) + +## À quoi il sert + +`dependency-review` bloque une pull request qui **introduit une dépendance +vulnérable connue**. Il compare le graphe de dépendances base-vs-head et fait +échouer la PR si un changement tire un package portant un avis de sécurité à la +sévérité configurée ou au-dessus. + +C'est le complément **au moment de la PR** de Dependabot : Dependabot ne réagit +qu'*après* qu'une dépendance vulnérable est déjà sur `main`, alors que celui-ci +attrape le problème au moment où une PR l'ajouterait — exactement quand il est le +moins coûteux à corriger. + +## Quand il s'exécute + +- À chaque **pull request visant `main`**. + +## Comment il s'exécute + +Un seul job, `review` : checkout, puis exécution de +`actions/dependency-review-action` avec `fail-on-severity: moderate`. + +## Permissions & sécurité + +`contents: read` seulement — l'action lit le graphe de dépendances du dépôt. Elle +ne poste **aucun** commentaire de PR (cela demanderait `pull-requests: write`) ; +le check en échec est le signal. + +## À manipuler avec précaution + +- **Il exige que le Dependency graph du dépôt soit activé.** C'est un réglage de + dépôt GitHub, pas quelque chose que le workflow peut activer. S'il est éteint, + l'action échoue avec *« Dependency review is not supported on this repository… + ensure that Dependency graph is enabled »* — c'est une erreur de configuration, + pas un bug de workflow. (Sur un dépôt privé, il faut aussi Advanced Security.) +- **Il ne voit que les changements apportés par la PR.** Une CVE publiée pendant + la nuit contre une dépendance *existante* ne fait pas échouer ce check — cela + reste un warning via l'audit NuGet (voir + [`Directory.Build.props`](../../Directory.Build.props)). Ce workflow ne bloque + qu'au point d'introduction, à dessein. +- **`fail-on-severity: moderate` est le bouton de réglage.** Abaissez-le à `low` + pour être plus strict, montez-le à `high` pour être plus laxiste. Comme il + n'inspecte que les changements de dépendances de la PR, `moderate` est un vrai + barrage plutôt que du bruit. +- **Il ne fait barrage que s'il est *required*.** Comme les autres checks, il + doit être marqué **required** dans la protection de branche sur `main` pour + réellement bloquer un merge. + +## En rapport + +- [`codeql`](codeql.fr.md) — le scanner de sécurité côté code. +- [`dependabot-automerge`](dependabot-automerge.fr.md) — le versant mise à jour de + dépendances après merge. diff --git a/maintainers/workflows/release-dryrun.en.md b/maintainers/workflows/release-dryrun.en.md new file mode 100644 index 0000000..a7ca3a2 --- /dev/null +++ b/maintainers/workflows/release-dryrun.en.md @@ -0,0 +1,66 @@ +# `release-dryrun` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](release-dryrun.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/release-dryrun.yml`](../../.github/workflows/release-dryrun.yml) + +## What it is for + +`release-dryrun` continuously rehearses the **side-effect-free** portion of the +release — build, pack, and embed the SBOM for the two published projects — on +every push and pull request. Because [`release`](release.en.md) itself runs only +on a tag or a manual dispatch, its packaging path would otherwise be exercised +for the first time in production, on a tag, once. This catches packaging/SBOM +regressions in ordinary CI instead. + +It is the automatic counterpart to the **manual** dry run documented in +[Release dry run (manual)](../ReleaseDryRun.en.md): this one deliberately leaves +out the attestation/OIDC path (which has side effects), the manual one adds it. + +## When it runs + +- On every **push to `main`**, **pull request targeting `main`**, and on demand + via **`workflow_dispatch`**. + +## How it runs + +One job, `pack`: checkout → setup the release SDK (10.0.x) → `dotnet build` → +**`tools/packaging/pack.sh`**, which packs the two published projects with their +SBOM and asserts the SBOM is actually embedded. + +## Permissions & security + +`contents: read` only. It stops before every step that has a side effect, so it +needs none of `release`'s write scopes: + +- **no provenance attestation** — that writes a permanent public record + (Sigstore/Rekor + the attestation store); kept to the manual dry run and real + releases; +- **no NuGet login / push** — nuget.org has no "dry-run push", so the publish + stays release-only; +- **no GitHub Release** — no tag or release is ever created here. + +## Handle with care + +- **It shares `tools/packaging/pack.sh` with `release`, and that is the point.** + There is exactly one definition of "pack the release artifacts", so this + rehearsal cannot drift from the real release. Do not inline a separate pack + command here — change `pack.sh` and both follow. +- **Keep it side-effect-free.** The value of this workflow is that it runs on + *every* PR with no attestation and no publish. Do not add the attestation or a + login step here; those belong to the manual dry run in `release`, which runs + deliberately, not on every push. +- **The `DRYRUN_VERSION` is a throwaway.** Nothing is published, so the exact + value only has to be a valid SemVer the pack accepts. The real version comes + from the tag and is validated in `release`. +- **This job's unique contribution is the *packaging*.** The unit/integration + tests run in [`ci`](ci.en.md); do not duplicate them here. + +## Related + +- [`release`](release.en.md) — the real publish path, sharing the same `pack.sh`. +- [Release dry run (manual)](../ReleaseDryRun.en.md) — the manual dry run that + additionally rehearses the attestation/OIDC path. diff --git a/maintainers/workflows/release-dryrun.fr.md b/maintainers/workflows/release-dryrun.fr.md new file mode 100644 index 0000000..aed1d5c --- /dev/null +++ b/maintainers/workflows/release-dryrun.fr.md @@ -0,0 +1,68 @@ +# Workflow `release-dryrun` + +🌍 🇬🇧 [English](release-dryrun.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/release-dryrun.yml`](../../.github/workflows/release-dryrun.yml) + +## À quoi il sert + +`release-dryrun` répète en continu la portion **sans effet de bord** de la +release — build, pack et embarquement du SBOM pour les deux projets publiés — sur +chaque push et pull request. Comme [`release`](release.fr.md) ne tourne lui-même +que sur un tag ou un dispatch manuel, son chemin d'empaquetage serait autrement +exercé pour la première fois en production, sur un tag, une seule fois. Celui-ci +attrape les régressions d'empaquetage/SBOM dans la CI ordinaire à la place. + +C'est le pendant automatique du dry run **manuel** documenté dans +[Répétition de release à blanc (« dry run » manuel)](../ReleaseDryRun.fr.md) : +celui-ci laisse volontairement de côté le chemin attestation/OIDC (qui a des +effets de bord), le manuel l'ajoute. + +## Quand il s'exécute + +- À chaque **push sur `main`**, **pull request visant `main`**, et à la demande + via **`workflow_dispatch`**. + +## Comment il s'exécute + +Un seul job, `pack` : checkout → setup du SDK de release (10.0.x) → +`dotnet build` → **`tools/packaging/pack.sh`**, qui packe les deux projets +publiés avec leur SBOM et assert que le SBOM est bien embarqué. + +## Permissions & sécurité + +`contents: read` seulement. Il s'arrête avant chaque étape qui a un effet de +bord, donc il n'a besoin d'aucun des périmètres en écriture de `release` : + +- **pas d'attestation de provenance** — cela écrit un enregistrement public + permanent (Sigstore/Rekor + le magasin d'attestations) ; réservé au dry run + manuel et aux vraies releases ; +- **pas de login / push NuGet** — nuget.org n'a pas de « push à blanc », donc la + publication reste réservée à la release ; +- **pas de GitHub Release** — aucun tag ni release n'est jamais créé ici. + +## À manipuler avec précaution + +- **Il partage `tools/packaging/pack.sh` avec `release`, et c'est le but.** Il + existe exactement une définition de « packer les artefacts de release », donc + cette répétition ne peut pas diverger de la vraie release. N'écrivez pas une + commande de pack séparée ici — changez `pack.sh` et les deux suivent. +- **Gardez-le sans effet de bord.** L'intérêt de ce workflow est qu'il tourne sur + *chaque* PR sans attestation ni publication. N'ajoutez pas ici l'attestation ni + une étape de login ; celles-ci appartiennent au dry run manuel de `release`, + qui tourne délibérément, pas à chaque push. +- **Le `DRYRUN_VERSION` est jetable.** Rien n'est publié, donc la valeur exacte + doit seulement être un SemVer valide que le pack accepte. La vraie version vient + du tag et est validée dans `release`. +- **La contribution unique de ce job est l'*empaquetage*.** Les tests + unitaires/intégration tournent dans [`ci`](ci.fr.md) ; ne les dupliquez pas ici. + +## En rapport + +- [`release`](release.fr.md) — le vrai chemin de publication, partageant le même + `pack.sh`. +- [Répétition de release à blanc (« dry run » manuel)](../ReleaseDryRun.fr.md) — + le dry run manuel qui répète en plus le chemin attestation/OIDC. diff --git a/maintainers/workflows/release.en.md b/maintainers/workflows/release.en.md new file mode 100644 index 0000000..b68522d --- /dev/null +++ b/maintainers/workflows/release.en.md @@ -0,0 +1,90 @@ +# `release` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](release.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/release.yml`](../../.github/workflows/release.yml) + +## What it is for + +`release` builds, attests, and publishes the NuGet packages. It is the only +workflow whose full path is otherwise **never exercised before a real tag** — +version resolution, pack, SBOM, OIDC and attestation permissions all run for the +first time in production conditions. To make that testable, it doubles as a +**manual dry run** that runs everything up to and including the attestation while +skipping the publish steps. + +> For the operational side of the manual dry run — how to launch it, what it +> touches, and when to use it — see the dedicated guide: +> **[Release dry run (manual)](../ReleaseDryRun.en.md)**. This page covers what +> the workflow *is* and the traps in its structure. + +## When it runs + +- On **push of a version tag** `v*.*.*` (e.g. `v1.2.3`, `v1.2.3-beta.1`) — this + publishes. +- On **`workflow_dispatch`** with two inputs: `version` and `dry_run` (**default + `true`**). A manual run publishes only if `dry_run` is explicitly unticked. + +## How it runs + +One job, `pack-push`: checkout → setup .NET → **resolve & validate version** → +restore → build → test → **pack** (via `tools/packaging/pack.sh`, embedding the +SPDX SBOM) → upload artifacts → **attest build provenance** → **NuGet login +(OIDC)** → **push to NuGet** → **publish GitHub Release**. The last two steps +(and only those) are gated off on a dry run. + +## Permissions & security + +The workflow needs three write scopes: `contents: write` (create the Release and +upload assets), `id-token: write` (mint the short-lived NuGet key via trusted +publishing), and `attestations: write` (store the signed provenance). No +long-lived `NUGET_API_KEY` is stored. + +## Handle with care + +This workflow encodes several hard-won fixes. Each of the following is +deliberate: + +- **Version input is validated against a strict SemVer allowlist, read via the + environment.** The tag/input is attacker-controllable (a tag like `v1.2.3;id` + is a valid ref matching the trigger). It is passed through `env:` rather than + interpolated into the shell, and rejected if it does not match the regex — + otherwise it could inject commands into every step that uses it. +- **Build metadata (`+…`) is rejected even though SemVer allows it.** NuGet + strips `+build` from the package identity, so `v1.2.3+build5` would pack as + `1.2.3`; combined with `--skip-duplicate` on push, an already-published `1.2.3` + would turn the release into a green no-op that publishes nothing. Failing + loudly is the point. +- **`Attest build provenance` runs *before* both publications, and runs even in + a dry run.** Only attested artifacts are ever released or pushed; and OIDC / + attestation-permission failures are exactly what the dry run is there to catch. +- **The attestation deliberately does not match the nuget.org copy.** nuget.org + repository-signs every upload (adds `.signature.p7s` inside the `.nupkg`), + changing the checksum. The attested bytes are therefore published as **GitHub + Release assets**, and consumers verify provenance against *those* with `gh + attestation verify` — the nuget.org copy is verified with `dotnet nuget verify` + instead. Do not "simplify" by attesting only the nuget.org copy. +- **`NuGet login (OIDC)` runs on every trigger, including dry run — only the push + and the Release are gated.** The token exchange is what validates the + trusted-publishing policy, so a dry run fails red when the policy or + `NUGET_USER` is missing. It mints a single-use key the dry run never spends. + Requires a trusted-publishing policy on nuget.org and the `NUGET_USER` secret + (the profile **username**, not the email). +- **The Release step pins `--target "$GITHUB_SHA"`.** On `workflow_dispatch` the + tag does not exist yet and `gh` would otherwise create it from the default + branch's latest state; pinning the SHA ties the tag, source archive and + attested packages to the commit this job actually built. The `|| … upload + --clobber` fallback keeps a re-run idempotent. +- **`concurrency` sets `cancel-in-progress: false`.** Never cancel a + half-finished publish. + +## Related + +- [Release dry run (manual)](../ReleaseDryRun.en.md) — the operational guide. +- [`release-dryrun`](release-dryrun.en.md) — the automatic, side-effect-free + rehearsal that runs on every PR and push, sharing the same `pack.sh`. +- The README's **Supply chain** section documents how a consumer verifies the + provenance and SBOM this workflow produces. diff --git a/maintainers/workflows/release.fr.md b/maintainers/workflows/release.fr.md new file mode 100644 index 0000000..8f8d392 --- /dev/null +++ b/maintainers/workflows/release.fr.md @@ -0,0 +1,95 @@ +# Workflow `release` + +🌍 🇬🇧 [English](release.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/release.yml`](../../.github/workflows/release.yml) + +## À quoi il sert + +`release` construit, atteste et publie les packages NuGet. C'est le seul workflow +dont le chemin complet n'est autrement **jamais exercé avant un vrai tag** — +résolution de version, pack, SBOM, OIDC et permissions d'attestation ne tournent +pour la première fois qu'en conditions de production. Pour rendre tout cela +testable, il joue aussi le rôle de **dry run manuel** qui exécute tout jusqu'à +l'attestation incluse tout en sautant les étapes de publication. + +> Pour le versant opérationnel du dry run manuel — comment le lancer, ce qu'il +> touche et quand l'utiliser — voir le guide dédié : +> **[Répétition de release à blanc (« dry run » manuel)](../ReleaseDryRun.fr.md)**. +> Cette page couvre ce que le workflow *est* et les pièges de sa structure. + +## Quand il s'exécute + +- Sur **push d'un tag de version** `v*.*.*` (p. ex. `v1.2.3`, `v1.2.3-beta.1`) — + cela publie. +- Sur **`workflow_dispatch`** avec deux entrées : `version` et `dry_run` (**défaut + `true`**). Un run manuel ne publie que si `dry_run` est explicitement décoché. + +## Comment il s'exécute + +Un seul job, `pack-push` : checkout → setup .NET → **résoudre & valider la +version** → restore → build → test → **pack** (via `tools/packaging/pack.sh`, qui +embarque le SBOM SPDX) → upload des artefacts → **attester la provenance de +build** → **login NuGet (OIDC)** → **push vers NuGet** → **publier la GitHub +Release**. Les deux dernières étapes (et elles seules) sont désactivées en dry +run. + +## Permissions & sécurité + +Le workflow a besoin de trois périmètres en écriture : `contents: write` (créer la +Release et uploader les assets), `id-token: write` (générer la clé NuGet +éphémère via trusted publishing) et `attestations: write` (stocker la provenance +signée). Aucun `NUGET_API_KEY` durable n'est stocké. + +## À manipuler avec précaution + +Ce workflow encode plusieurs corrections durement acquises. Chacun des points +suivants est délibéré : + +- **L'entrée de version est validée contre une allowlist SemVer stricte, lue via + l'environnement.** Le tag/entrée est contrôlable par un attaquant (un tag comme + `v1.2.3;id` est une ref valide qui matche le trigger). Il passe par `env:` + plutôt que d'être interpolé dans le shell, et il est rejeté s'il ne matche pas + la regex — sinon il pourrait injecter des commandes dans chaque étape qui + l'utilise. +- **Les métadonnées de build (`+…`) sont rejetées bien que SemVer les autorise.** + NuGet retire `+build` de l'identité du package, donc `v1.2.3+build5` packerait + en `1.2.3` ; combiné à `--skip-duplicate` au push, un `1.2.3` déjà publié + transformerait la release en no-op vert qui ne publie rien. Échouer bruyamment + est le but. +- **`Attest build provenance` tourne *avant* les deux publications, et tourne + même en dry run.** Seuls des artefacts attestés sont jamais publiés ou poussés ; + et les échecs d'OIDC / de permission d'attestation sont exactement ce que le dry + run sert à détecter. +- **L'attestation ne correspond volontairement pas à la copie de nuget.org.** + nuget.org re-signe chaque upload côté dépôt (ajoute un `.signature.p7s` dans le + `.nupkg`), changeant le checksum. Les octets attestés sont donc publiés comme + **assets de GitHub Release**, et les consommateurs vérifient la provenance + contre *ceux-là* avec `gh attestation verify` — la copie de nuget.org se + vérifie avec `dotnet nuget verify`. Ne « simplifiez » pas en attestant + seulement la copie de nuget.org. +- **`NuGet login (OIDC)` tourne à chaque trigger, dry run inclus — seuls le push + et la Release sont conditionnés.** L'échange de token est ce qui valide la + policy trusted-publishing, donc un dry run échoue (rouge) quand la policy ou + `NUGET_USER` est absent. Il génère une clé à usage unique que le dry run ne + dépense jamais. Nécessite une policy trusted-publishing sur nuget.org et le + secret `NUGET_USER` (le **nom d'utilisateur** du profil, pas l'e-mail). +- **L'étape Release épingle `--target "$GITHUB_SHA"`.** Sur `workflow_dispatch` le + tag n'existe pas encore et `gh` le créerait sinon depuis le dernier état de la + branche par défaut ; épingler le SHA lie le tag, l'archive source et les + packages attestés au commit que ce job a réellement construit. Le repli + `|| … upload --clobber` garde un re-run idempotent. +- **`concurrency` met `cancel-in-progress: false`.** Ne jamais annuler une + publication à moitié faite. + +## En rapport + +- [Répétition de release à blanc (« dry run » manuel)](../ReleaseDryRun.fr.md) — + le guide opérationnel. +- [`release-dryrun`](release-dryrun.fr.md) — la répétition automatique et sans + effet de bord qui tourne sur chaque PR et push, partageant le même `pack.sh`. +- La section **Supply chain** du README documente comment un consommateur vérifie + la provenance et le SBOM que ce workflow produit. diff --git a/maintainers/workflows/scorecard.en.md b/maintainers/workflows/scorecard.en.md new file mode 100644 index 0000000..234ac85 --- /dev/null +++ b/maintainers/workflows/scorecard.en.md @@ -0,0 +1,74 @@ +# `scorecard` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](scorecard.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/scorecard.yml`](../../.github/workflows/scorecard.yml) + +## What it is for + +`scorecard` runs [OpenSSF Scorecard](https://securityscorecards.dev), which +scores the repository's **security posture** against a set of automated checks — +pinned actions, token permissions, branch protection, signed releases, +dependency-update tooling, and more. It uploads the findings to the +code-scanning dashboard and **publishes the score to securityscorecards.dev**, +which powers the OpenSSF Scorecard badge in the README. + +Where `codeql` scores the *code* and `dependency-review` scores the +*dependencies*, Scorecard scores the *project's practices* — including several +of the very conventions the other workflows implement. + +## When it runs + +It runs on the **default branch only** — it assesses the repository, not a PR +diff, so there is **no `pull_request` trigger**: + +- On **`branch_protection_rule`** — re-score when a branch-protection rule + changes (this feeds Scorecard's Branch-Protection check). +- **Weekly** on a `schedule` (`cron: 23 5 * * 1`). +- On **push to `main`**. + +## How it runs + +One job, `analysis`: credential-free checkout → run `ossf/scorecard-action` with +`publish_results: true` → upload the SARIF both as a build artifact and to the +code-scanning dashboard. + +## Permissions & security + +Top-level `permissions: read-all`; the job widens only two write scopes: + +- `security-events: write` — upload the SARIF to code-scanning. +- `id-token: write` — publish results to the public OpenSSF API via OIDC; **this + is what enables the badge**. + +The checkout uses **`persist-credentials: false`** — a credential-free checkout +that Scorecard's own Token-Permissions check rewards. + +## Handle with care + +- **It does not run on pull requests, by design.** Do not add a `pull_request` + trigger to "test it on a PR" — Scorecard evaluates the whole repository, and it + needs the default-branch context and the OIDC identity to publish. A PR run + would be meaningless and could not publish. To exercise a change, merge it and + watch the first push-to-`main` run. +- **The badge shows "no data" until the first `main` run publishes.** That is + expected on first setup, not a failure. +- **`publish_results: true` is public-repo-appropriate.** It publishes the score + to securityscorecards.dev for transparency and the badge. Keep it as-is for a + public repo; if the repo ever goes private, drop it (and the badge). +- **The permission comment is load-bearing.** This repo is public, so + `contents: read` / `actions: read` are not needed in the token. The commented + lines in the YAML explain what to **uncomment for a private repository** — do + not delete them. +- **Improving the score is done in the *other* workflows and in repo settings**, + not here. A low Branch-Protection sub-score, for instance, is fixed by marking + checks required on `main`, not by editing this file. + +## Related + +- [`codeql`](codeql.en.md) — shares the `github/codeql-action/upload-sarif` + action used to push results to code-scanning. +- The README badge links to the public viewer at `securityscorecards.dev`. diff --git a/maintainers/workflows/scorecard.fr.md b/maintainers/workflows/scorecard.fr.md new file mode 100644 index 0000000..b3ce880 --- /dev/null +++ b/maintainers/workflows/scorecard.fr.md @@ -0,0 +1,76 @@ +# Workflow `scorecard` + +🌍 🇬🇧 [English](scorecard.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/scorecard.yml`](../../.github/workflows/scorecard.yml) + +## À quoi il sert + +`scorecard` exécute [OpenSSF Scorecard](https://securityscorecards.dev), qui note +la **posture de sécurité** du dépôt selon un ensemble de checks automatisés — +actions épinglées, permissions des tokens, protection de branche, releases +signées, outillage de mise à jour des dépendances, et plus. Il remonte les +résultats au tableau de bord code-scanning et **publie le score sur +securityscorecards.dev**, ce qui alimente le badge OpenSSF Scorecard du README. + +Là où `codeql` note le *code* et `dependency-review` note les *dépendances*, +Scorecard note les *pratiques du projet* — dont plusieurs des conventions mêmes +que les autres workflows mettent en œuvre. + +## Quand il s'exécute + +Il tourne sur la **branche par défaut uniquement** — il évalue le dépôt, pas un +diff de PR, donc il n'y a **pas de trigger `pull_request`** : + +- Sur **`branch_protection_rule`** — re-note quand une règle de protection de + branche change (cela alimente le check Branch-Protection de Scorecard). +- **Chaque semaine** via `schedule` (`cron: 23 5 * * 1`). +- Sur **push vers `main`**. + +## Comment il s'exécute + +Un seul job, `analysis` : checkout sans credentials → exécution de +`ossf/scorecard-action` avec `publish_results: true` → upload du SARIF à la fois +en artefact de build et vers le tableau de bord code-scanning. + +## Permissions & sécurité + +`permissions: read-all` au niveau supérieur ; le job n'élargit que deux +périmètres en écriture : + +- `security-events: write` — uploader le SARIF vers code-scanning. +- `id-token: write` — publier les résultats vers l'API publique OpenSSF via OIDC ; + **c'est ce qui active le badge**. + +Le checkout utilise **`persist-credentials: false`** — un checkout sans +credentials que le propre check Token-Permissions de Scorecard récompense. + +## À manipuler avec précaution + +- **Il ne tourne pas sur les pull requests, à dessein.** N'ajoutez pas de trigger + `pull_request` pour « le tester sur une PR » — Scorecard évalue le dépôt entier, + et il a besoin du contexte de la branche par défaut et de l'identité OIDC pour + publier. Un run de PR serait dénué de sens et ne pourrait pas publier. Pour + exercer un changement, mergez-le et observez le premier run push-sur-`main`. +- **Le badge affiche « no data » jusqu'au premier run sur `main` qui publie.** + C'est attendu lors de la mise en place initiale, pas un échec. +- **`publish_results: true` convient à un dépôt public.** Il publie le score sur + securityscorecards.dev par transparence et pour le badge. Laissez-le tel quel + pour un dépôt public ; si le dépôt passait un jour en privé, retirez-le (et le + badge). +- **Le commentaire sur les permissions porte du sens.** Ce dépôt est public, donc + `contents: read` / `actions: read` ne sont pas nécessaires dans le token. Les + lignes commentées dans le YAML expliquent ce qu'il faut **décommenter pour un + dépôt privé** — ne les supprimez pas. +- **On améliore le score dans les *autres* workflows et dans les réglages du + dépôt**, pas ici. Un sous-score Branch-Protection faible, par exemple, se + corrige en marquant des checks required sur `main`, pas en éditant ce fichier. + +## En rapport + +- [`codeql`](codeql.fr.md) — partage l'action `github/codeql-action/upload-sarif` + utilisée pour pousser les résultats vers code-scanning. +- Le badge du README pointe vers le visualiseur public sur `securityscorecards.dev`. diff --git a/maintainers/workflows/sonar.en.md b/maintainers/workflows/sonar.en.md new file mode 100644 index 0000000..9ace285 --- /dev/null +++ b/maintainers/workflows/sonar.en.md @@ -0,0 +1,63 @@ +# `sonar` workflow + +🌍 🇬🇧 English (this file) · 🇫🇷 [Français](sonar.fr.md) + +> Maintainer documentation — part of the [workflow reference](README.md). +> Not part of the user documentation under `doc/`. + +**Workflow file:** [`.github/workflows/sonar.yml`](../../.github/workflows/sonar.yml) + +## What it is for + +`sonar` runs the SonarQube Cloud analysis: it feeds the **Quality Gate** and the +**coverage** metric shown on the two SonarCloud badges in the README. It is the +static-analysis-plus-coverage view of the codebase, hosted off GitHub. + +## When it runs + +- On every **push to `main`**. +- On every **pull request targeting `main`** — **except PRs from forks** (see + below). +- On demand via **`workflow_dispatch`**. + +## How it runs + +One job, `analyze`, on Linux: + +1. Checkout with **`fetch-depth: 0`** — full history, so Sonar can attribute + issues via `git blame` and distinguish new code from old. +2. Set up .NET **and Java 17** — the SonarScanner for .NET runs on the JVM. +3. `dotnet-sonarscanner begin` → **build** → test with coverage → + `dotnet-sonarscanner end`. + +## Permissions & security + +`contents: read` only. PR decoration (the inline Sonar comments) is delivered by +the **SonarQube Cloud GitHub App**, not by this workflow's token, so no +`pull-requests: write` is needed here. The analysis authenticates with the +`SONAR_TOKEN` secret. + +## Handle with care + +- **The build must sit *between* `begin` and `end`.** The scanner hooks MSBuild + to observe the compilation; it cannot analyse a pre-built or `--no-build` + output. Do not reorder these steps or add `--no-build` to the analysis build. +- **The analysis build disables the warning ratchet on purpose.** It passes + `-p:TreatWarningsAsErrors=false -p:MSBuildTreatWarningsAsErrors=false`. The + scanner needs the compilation to **complete** so it can collect the + `SonarAnalyzer` diagnostics and upload them in `end`; a Sonar-rule warning + promoted to an error would fail the build before results are reported. The + ratchet stays enforced by [`ci`](ci.en.md) on both OS legs — that is the gate, + this analysis leg is not. +- **The fork guard is required, not optional.** The + `if: … head.repo.full_name == github.repository` condition skips the analysis + on PRs from forks, because a fork PR cannot read `SONAR_TOKEN` and would fail + on a missing secret rather than a real problem. Branches inside this repository + (the normal contributor flow) run normally. +- **`fetch-depth: 0` matters.** A shallow checkout would break Sonar's new-code + detection and blame attribution. + +## Related + +- [`ci`](ci.en.md) — produces the same OpenCover coverage shape via the shared + `coverage.runsettings`, and is where the warning ratchet is actually enforced. diff --git a/maintainers/workflows/sonar.fr.md b/maintainers/workflows/sonar.fr.md new file mode 100644 index 0000000..c7727de --- /dev/null +++ b/maintainers/workflows/sonar.fr.md @@ -0,0 +1,67 @@ +# Workflow `sonar` + +🌍 🇬🇧 [English](sonar.en.md) · 🇫🇷 Français (ce fichier) + +> Documentation mainteneur — fait partie de la [référence des workflows](README.fr.md). +> Ne fait pas partie de la documentation utilisateur sous `doc/`. + +**Fichier du workflow :** [`.github/workflows/sonar.yml`](../../.github/workflows/sonar.yml) + +## À quoi il sert + +`sonar` exécute l'analyse SonarQube Cloud : il alimente le **Quality Gate** et la +métrique de **couverture** affichés par les deux badges SonarCloud du README. +C'est la vue analyse-statique-plus-couverture du code, hébergée hors de GitHub. + +## Quand il s'exécute + +- À chaque **push sur `main`**. +- À chaque **pull request visant `main`** — **sauf les PR issues de forks** (voir + plus bas). +- À la demande via **`workflow_dispatch`**. + +## Comment il s'exécute + +Un seul job, `analyze`, sous Linux : + +1. Checkout avec **`fetch-depth: 0`** — historique complet, pour que Sonar puisse + attribuer les problèmes via `git blame` et distinguer le code neuf de + l'ancien. +2. Installation de .NET **et de Java 17** — le SonarScanner for .NET tourne sur la + JVM. +3. `dotnet-sonarscanner begin` → **build** → test avec couverture → + `dotnet-sonarscanner end`. + +## Permissions & sécurité + +`contents: read` seulement. La décoration des PR (les commentaires Sonar en +ligne) est livrée par la **GitHub App SonarQube Cloud**, pas par le token de ce +workflow, donc aucun `pull-requests: write` n'est requis ici. L'analyse +s'authentifie avec le secret `SONAR_TOKEN`. + +## À manipuler avec précaution + +- **Le build doit se trouver *entre* `begin` et `end`.** Le scanner s'accroche à + MSBuild pour observer la compilation ; il ne peut pas analyser une sortie + pré-construite ou `--no-build`. Ne réordonnez pas ces étapes et n'ajoutez pas + `--no-build` au build d'analyse. +- **Le build d'analyse désactive volontairement le cliquet de warnings.** Il + passe `-p:TreatWarningsAsErrors=false -p:MSBuildTreatWarningsAsErrors=false`. Le + scanner a besoin que la compilation **aille au bout** pour collecter les + diagnostics `SonarAnalyzer` et les uploader dans `end` ; un warning de règle + Sonar promu en erreur ferait échouer le build avant que les résultats ne soient + remontés. Le cliquet reste imposé par [`ci`](ci.fr.md) sur les deux branches OS + — c'est ça le barrage, pas cette branche d'analyse. +- **Le garde-fou fork est nécessaire, pas optionnel.** La condition + `if: … head.repo.full_name == github.repository` saute l'analyse sur les PR de + forks, parce qu'une PR de fork ne peut pas lire `SONAR_TOKEN` et échouerait sur + un secret absent plutôt que sur un vrai problème. Les branches internes au dépôt + (le flux contributeur normal) tournent normalement. +- **`fetch-depth: 0` compte.** Un checkout superficiel casserait la détection de + code neuf et l'attribution par blame de Sonar. + +## En rapport + +- [`ci`](ci.fr.md) — produit la même forme de couverture OpenCover via le + `coverage.runsettings` partagé, et c'est là que le cliquet de warnings est + réellement imposé.