Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .claude/hooks/coding-rules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ display="${file##*/}"
# (documentation routinely shows `var` in sample code), an anonymous type (C#
# gives no other spelling), and an occurrence inside a string literal (the
# analyzer suites embed C# fixtures that deliberately use `var`).
# shellcheck disable=SC2317 # reached through the `"rule_${rule}"` dispatch in the run section below
rule_explicit_types() {
awk -v name="$display" '
{
Expand Down Expand Up @@ -93,6 +94,7 @@ rule_explicit_types() {
' "$file"
}

# shellcheck disable=SC2317 # reached through the `"${rule}_hint"` dispatch in the run section below
explicit_types_hint() {
printf '%s' "Coding rule — explicit types (CLAUDE.md, \"Coding rules\"). This file now declares
inferred types:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/adr-check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ jobs:
if [ "$needs" = "true" ] && [ -n "$report" ]; then
printf '%s\n' "$report" >> "$GITHUB_STEP_SUMMARY"
else
# shellcheck disable=SC2016 # the backticks are Markdown, not command substitution
printf '### 🏛️ ADR check (advisory)\n\nNo architectural decision detected on `%s` versus `%s`. Nothing to flag.\n' \
"$BRANCH" "$BASE" >> "$GITHUB_STEP_SUMMARY"
fi
Expand Down
3 changes: 3 additions & 0 deletions .github/workflows/dependabot-autofix.yml
Original file line number Diff line number Diff line change
Expand Up @@ -302,13 +302,16 @@ jobs:
patch="$(jq -r '.patch // ""' verdict.json)"
msg="$(jq -r '.commit_message // ""' verdict.json)"
if [ -n "$patch" ]; then
# shellcheck disable=SC2016 # the backticks are Markdown, not command substitution
printf 'Apply it yourself:\n\n```diff\n%s\n```\n\n' "$patch"
# shellcheck disable=SC2016 # the backticks are Markdown, not command substitution
[ -n "$msg" ] && printf 'Commit it as:\n\n```\n%s\n```\n\n' "$msg"
fi
else
printf '**🧑 Needs a human** — %s.\n\n' "$category"
[ -n "$explanation" ] && printf '%s\n\n' "$explanation"
fi
# shellcheck disable=SC2016 # the backticks are Markdown, not command substitution
printf '_No dependency version was changed. See `%s`._\n' "$doc"
} > comment-body.md

Expand Down
86 changes: 86 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
name: lint

# Static analysis for the files the C# compiler never sees: the POSIX shell scripts under
# tools/ and .claude/hooks/, and the workflow definitions themselves.
#
# Why this workflow exists. Every other analysis in this repository runs inside a build —
# the Roslyn analyzers, the style rule ADR-0055 restated in .editorconfig, the warning
# ratchet in Directory.Build.props — so a contributor meets it at the moment they write the
# code. Shell and YAML had no such moment: the only thing reading them was the SonarQube
# Cloud scan, which reports AFTER the merge and enforces nothing (its workflow is green as
# soon as the upload succeeds, whatever the Quality Gate says). Two findings typed
# VULNERABILITY reached `main` that way. This closes the gap with tools that run on our own
# runners, so the signal does not depend on a third-party service being up.

on:
push:
branches:
- main
pull_request:
branches:
- main
workflow_dispatch:

# Cancel superseded runs on the same branch / PR.
concurrency:
group: lint-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
scripts-and-workflows:
name: Lint scripts and workflows
runs-on: ubuntu-latest
# Both tools are static and finish in seconds; the cap only guards a hung download.
timeout-minutes: 10
# Declared per job rather than at workflow level, so a job added later inherits nothing
# it did not ask for (Sonar githubactions:S8264). This job only reads the checkout.
permissions:
contents: read
steps:
- name: Checkout
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7

- name: Lint the shell scripts
# shellcheck ships preinstalled on the ubuntu runner image, so there is nothing to
# fetch and no third-party action in the supply chain. The version is printed so a
# future failure caused by a runner-image upgrade is readable from the log alone.
#
# Every script here is `#!/bin/sh`; shellcheck reads the shebang and applies the
# POSIX dialect, which is what should be checked — `local`, arrays and `[[` are not
# available on the shells these run on (decision on the POSIX rules: ADR-0060).
#
# The bar is ZERO findings at every severity, `info` included. The tree is clean at
# that bar today: the two SC1091 findings were fixed with `source=` directives, and
# the SC2016/SC2317 false positives carry an inline `disable` naming its reason. A
# lower bar would let `info` accumulate exactly the way the Sonar report did.
run: |
shellcheck --version | sed -n 's/^version: /shellcheck /p'
find . -name '*.sh' -not -path './.git/*' -not -path '*/bin/*' -not -path '*/obj/*' \
-print0 | xargs -0 --no-run-if-empty shellcheck

- name: Lint the workflow definitions
# actionlint checks what YAML alone cannot: `${{ }}` expression types, action inputs
# against each action's own schema, `needs`/matrix references, cron syntax, and the
# shell of every `run:` block (it embeds shellcheck, which is why the run blocks
# carry the same inline `disable` directives the scripts do).
#
# Pinned to a release tarball and verified by checksum rather than run through a
# third-party action: an unpinned action is exactly what OpenSSF Scorecard's
# Pinned-Dependencies check counts against this repository. Bump both lines together.
#
# NOT covered by this step: actionlint audits correctness, not security posture. It
# does not flag over-broad permissions, spoofable actor checks or dangerous triggers
# — the class that produced this repository's two VULNERABILITY findings. A dedicated
# auditor (zizmor) is the tool for that and is a separate decision.
env:
ACTIONLINT_VERSION: '1.7.7'
ACTIONLINT_SHA256: '023070a287cd8cccd71515fedc843f1985bf96c436b7effaecce67290e7e0757'
run: |
set -eu
archive="actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz"
curl -sSfL -o "$archive" \
"https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/${archive}"
echo "${ACTIONLINT_SHA256} ${archive}" | sha256sum --check --strict
tar xzf "$archive" actionlint
./actionlint --version
./actionlint -color
1 change: 1 addition & 0 deletions doc/handwritten/for-maintainers/workflows/README.fr.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ documentées une seule fois ici plutôt que répétées sur chaque page.
| [`justdummies-mutation`](justdummies-mutation.fr.md) | Idem pour les packages JustDummies, avec son propre check obligatoire — séparé pour que la future séparation de dépôt soit un déplacement de fichier. |
| [`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. |
| [`lint`](lint.fr.md) | shellcheck et actionlint sur les fichiers que le compilateur C# ne voit jamais — les scripts POSIX et les définitions de workflow. Zéro constat, `info` compris. |
| [`adr-check`](adr-check.fr.md) | Consultatif, dispatch manuel : confronte une branche à la base d'ADR (nouvelle décision / remplacement / conflit). Le repli pour les contributeurs sans Claude Code ; ne bloque jamais. |

### Sécurité & chaîne d'approvisionnement
Expand Down
1 change: 1 addition & 0 deletions doc/handwritten/for-maintainers/workflows/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ here instead of being repeated on every page.
| [`justdummies-mutation`](justdummies-mutation.en.md) | The same, for the JustDummies packages, with its own required check — kept separate so the future repository split is a file move. |
| [`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. |
| [`lint`](lint.en.md) | shellcheck and actionlint over the files the C# compiler never sees — the POSIX scripts and the workflow definitions. Zero findings, `info` included. |
| [`adr-check`](adr-check.en.md) | Advisory, manual dispatch: check a branch against the ADR base (new decision / supersede / conflict). The fallback for contributors without Claude Code; never blocks. |

### Security & supply chain
Expand Down
89 changes: 89 additions & 0 deletions doc/handwritten/for-maintainers/workflows/lint.en.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# `lint` workflow

🌍 🇬🇧 English (this file) · 🇫🇷 [Français](lint.fr.md)

> Maintainer documentation — part of the [workflow reference](README.md).
> Not part of the user documentation under `doc/`.

**Workflow file:** [`.github/workflows/lint.yml`](../../../../.github/workflows/lint.yml)

## What it is for

It statically analyses the files the C# compiler never sees: the POSIX shell
scripts under `tools/` and `.claude/hooks/`, and the workflow definitions in
`.github/workflows/` themselves.

Every other analysis in this repository runs **inside a build** — the Roslyn
analyzers, the explicit-type rule ADR-0055 restated in `.editorconfig`, the
warning ratchet in `Directory.Build.props` — so a contributor meets it at the
moment they write the code. Shell and YAML had no such moment. The only thing
reading them was the [`sonar`](sonar.en.md) scan, which reports **after** the
merge and enforces nothing: its job is green as soon as the analysis uploads,
whatever the Quality Gate says. Two findings typed VULNERABILITY reached `main`
that way, and 21 shell findings accumulated unseen.

This workflow closes that gap with tools that run on our own runners, so the
signal arrives before the merge and does not depend on a third-party service
being reachable.

## When it runs

- On every **push to `main`**.
- On every **pull request targeting `main`**.
- On demand via **`workflow_dispatch`**.

## How it runs

One job, `Lint scripts and workflows`, on Linux:

1. **shellcheck** over every `*.sh` in the repository. It ships preinstalled on
the runner image, so there is nothing to fetch and no third-party action in
the supply chain.
2. **actionlint** over `.github/workflows/`. It checks what YAML alone cannot:
`${{ }}` expression types, action inputs against each action's own schema,
`needs` and matrix references, cron syntax, and — through an embedded
shellcheck — the shell of every `run:` block.

## Permissions & security

`contents: read`, declared **on the job** rather than at workflow level, so a job
added later inherits nothing it did not ask for (this is Sonar
`githubactions:S8264`, and the reason the two mutation workflows were changed the
same way).

actionlint is fetched as a **pinned release tarball verified by SHA-256**, not
run through a third-party action: an unpinned action is what OpenSSF Scorecard's
Pinned-Dependencies check counts against this repository. The version and the
checksum sit next to each other in the workflow and are bumped together.

## Handle with care

- **The bar is zero findings, `info` included.** The tree is clean at that bar,
so anything new is genuinely new. A lower bar would let `info` accumulate
exactly the way the Sonar report did — which is the problem this workflow
exists to prevent, not to reproduce.
- **False positives are annotated in place, never disabled globally.** Three
patterns are silenced with an inline `# shellcheck disable=` naming its reason:
`SC2016` where a `printf` format carries Markdown backticks (read as command
substitution), and `SC2317` on the two hook functions reached through the
`"rule_${rule}"` dispatch shellcheck cannot follow. A repository-wide
`.shellcheckrc` would blind the rules everywhere, including where they are
right.
- **The scripts are `#!/bin/sh`, and shellcheck applies the POSIX dialect.**
That is deliberate: `local`, arrays and `[[` are not available on the shells
these run on, and the POSIX rules the scripts are held to are a recorded
decision (ADR-0060).
- **actionlint audits correctness, not security posture.** It does not flag
over-broad permissions, spoofable actor checks or dangerous triggers — the very
class that produced this repository's two VULNERABILITY findings. A dedicated
auditor (`zizmor`) covers that and is a separate decision, not something this
workflow quietly provides.
- **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

- [`sonar`](sonar.en.md) — the analysis this workflow brings forward. It stays
the reporting and coverage view; it is not, and was never, an enforcement gate.
- [`ci`](ci.en.md) — where the warning ratchet enforces the equivalent bar on the
C# side.
93 changes: 93 additions & 0 deletions doc/handwritten/for-maintainers/workflows/lint.fr.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
# Workflow `lint`

🌍 🇫🇷 Français (ce fichier) · 🇬🇧 [English](lint.en.md)

> 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/lint.yml`](../../../../.github/workflows/lint.yml)

## À quoi il sert

Il analyse statiquement les fichiers que le compilateur C# ne voit jamais : les
scripts shell POSIX sous `tools/` et `.claude/hooks/`, et les définitions de
workflow de `.github/workflows/` elles-mêmes.

Toute autre analyse de ce dépôt tourne **dans une compilation** — les analyseurs
Roslyn, la règle du type explicite redite dans `.editorconfig` par l'ADR-0055, le
ratchet de warnings de `Directory.Build.props` — si bien qu'un contributeur la
rencontre au moment où il écrit le code. Le shell et le YAML n'avaient pas ce
moment. La seule chose qui les lisait était l'analyse [`sonar`](sonar.fr.md), qui
rapporte **après** le merge et n'applique rien : son job est vert dès que
l'analyse est téléversée, quoi que dise le Quality Gate. Deux constats typés
VULNERABILITY ont atteint `main` par ce chemin, et 21 constats shell s'y sont
accumulés sans être vus.

Ce workflow referme ce trou avec des outils qui tournent sur nos propres
*runners* : le signal arrive avant le merge et ne dépend pas de la disponibilité
d'un service tiers.

## Quand il tourne

- À chaque **push sur `main`**.
- À chaque **pull request visant `main`**.
- À la demande via **`workflow_dispatch`**.

## Comment il tourne

Un job, `Lint scripts and workflows`, sous Linux :

1. **shellcheck** sur chaque `*.sh` du dépôt. Il est préinstallé sur l'image du
*runner* : rien à télécharger, aucune action tierce dans la chaîne
d'approvisionnement.
2. **actionlint** sur `.github/workflows/`. Il vérifie ce que le YAML seul ne
peut pas : le typage des expressions `${{ }}`, les entrées d'actions face au
schéma de chaque action, les références `needs` et matrice, la syntaxe cron
et — via un shellcheck embarqué — le shell de chaque bloc `run:`.

## Permissions & sécurité

`contents: read`, déclaré **sur le job** plutôt qu'au niveau du workflow, pour
qu'un job ajouté plus tard n'hérite de rien qu'il n'ait demandé (c'est la règle
Sonar `githubactions:S8264`, et la raison pour laquelle les deux workflows de
mutation ont été modifiés de la même façon).

actionlint est récupéré comme **archive de version épinglée et vérifiée par
SHA-256**, et non exécuté via une action tierce : une action non épinglée est
précisément ce que le contrôle Pinned-Dependencies d'OpenSSF Scorecard retient
contre ce dépôt. La version et l'empreinte se suivent dans le workflow et se
mettent à jour ensemble.

## À manier avec précaution

- **La barre est à zéro constat, `info` compris.** L'arbre est propre à cette
barre : tout nouveau constat est donc réellement nouveau. Une barre plus basse
laisserait les `info` s'accumuler exactement comme dans le rapport Sonar — ce
que ce workflow existe pour empêcher, non pour reproduire.
- **Les faux positifs sont annotés sur place, jamais désactivés globalement.**
Trois motifs sont tus par un `# shellcheck disable=` en ligne portant sa
raison : `SC2016` là où un format `printf` contient des *backticks* Markdown
(lus comme une substitution de commande), et `SC2317` sur les deux fonctions de
*hook* atteintes par la répartition `"rule_${rule}"` que shellcheck ne sait pas
suivre. Un `.shellcheckrc` à l'échelle du dépôt aveuglerait ces règles partout,
y compris là où elles ont raison.
- **Les scripts sont en `#!/bin/sh`, et shellcheck applique le dialecte POSIX.**
C'est délibéré : `local`, les tableaux et `[[` ne sont pas disponibles sur les
shells qui les exécutent, et les règles POSIX auxquelles ces scripts sont tenus
sont une décision consignée (ADR-0060).
- **actionlint audite la correction, pas la posture de sécurité.** Il ne signale
ni permissions trop larges, ni vérification d'acteur usurpable, ni déclencheur
dangereux — la classe même qui a produit les deux constats VULNERABILITY de ce
dépôt. Un auditeur dédié (`zizmor`) couvre cela et relève d'une décision à
part, que ce workflow ne fournit pas en douce.
- **Ce contrôle ne sert que s'il est requis.** Comme les autres contrôles de
qualité, il ne bloque un merge que si la protection de branche de `main` le
marque **required**.

## Voir aussi

- [`sonar`](sonar.fr.md) — l'analyse que ce workflow ramène en amont. Elle reste
la vue de rapport et de couverture ; elle n'est pas, et n'a jamais été, un
garde-fou.
- [`ci`](ci.fr.md) — là où le ratchet de warnings applique la barre équivalente
côté C#.
1 change: 1 addition & 0 deletions tools/analyzer-count-check/check-analyzer-count.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ fi

if [ "$status" -ne 0 ]; then
printf '\nUpdate the bullet in FirstClassErrors/README.nuget.md to read:\n' >&2
# shellcheck disable=SC2016 # the backticks are Markdown code spans in the format string, not command substitution
printf ' **%s Roslyn analyzers in the box (`%s`-`%s`).**\n' "$count" "$low" "$high" >&2
exit 1
fi
Expand Down
1 change: 1 addition & 0 deletions tools/changelog/collect-prs.sh
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ set -eu
component="${1:-}"
# Tag prefix and train scopes come from tools/trains.sh (the single source of truth
# shared with release-notes.sh), so the changelog and the release notes never diverge.
# shellcheck source=tools/trains.sh
. "$(dirname "$0")/../trains.sh"
prefix="$(prefix_of "$component")"
train_scopes="$(scopes_of "$component")"
Expand Down
1 change: 1 addition & 0 deletions tools/packaging/release-notes.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ end_ref="${3:-$current_tag}"

# Tag prefix and train scopes come from tools/trains.sh (the single source of truth
# shared with the changelog tooling), so the two can never disagree on the partition.
# shellcheck source=tools/trains.sh
. "$(dirname "$0")/../trains.sh"
prefix="$(prefix_of "$scope")"
train_scopes="$(scopes_of "$scope")"
Expand Down
Loading