From 6a61b266f01d0dc8cc2490b24171456f5b0d9dd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20LIARD?= Date: Fri, 24 Jul 2026 12:47:20 +0200 Subject: [PATCH 1/2] ci: gate on prek, shared between pre-commit and CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The repo had no automated checks: every regression found so far was found by hand. Add prek.toml as the single description of the gate, and a workflow that installs the tools and defers to `prek run --all-files`. CI owns no rules of its own, so the local hook and the pipeline cannot drift; tightening a check is one line in one file. Hooks: whitespace and line-ending hygiene, YAML/TOML validity, merge markers, shellcheck, actionlint, gitleaks, and a parse of the script under sh, bash, dash, zsh and ksh. That last one carries the weight: the script is sourced, so a syntax error breaks the user's login shell rather than failing a command, and the five-shell claim in the file header was until now unverified. shellcheck runs at --severity=warning with two exclusions. SC3043 (`local` is not POSIX) fires on nearly every helper and is accepted: every shell the project targets implements it, and removing it would leak helper variables into the user's interactive shell. SC3045 (`type -t`) is a false positive — its only call site is inside an `elif [ -n "${BASH_VERSION-}" ]` branch. No third-party action is used. j178/prek-action does this in one line but executes 2.4 MB of bundled JavaScript from both a `main:` and a `post:` entry point, behind a mutable tag; the release tarball plus its published SHA-256 gives the same result with a reviewable surface. actions/checkout is pinned by commit SHA for the same reason. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 88 ++++++++++++++++++++++++++++++++++++++++ README.md | 13 ++++++ prek.toml | 86 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 .github/workflows/ci.yml create mode 100644 prek.toml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..b3fcaac --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,88 @@ +# Quality gate. +# +# This workflow owns no rules of its own: it installs the tools the hooks +# need, then hands over to `prek run --all-files`, which reads prek.toml — +# the same file `prek install` wires into the local pre-commit hook. Adding +# or tightening a check is a one-line edit to prek.toml and takes effect in +# both places, so the local gate and CI cannot drift. +# +# Supply chain: no third-party action runs here. `j178/prek-action` would do +# the job in one line, but it executes 2.4 MB of bundled JavaScript from a +# `main:` *and* a `post:` entry point, behind a mutable tag. Fetching the +# upstream release tarball and checking its published SHA-256 gives the same +# result with a reviewable surface. actions/checkout is pinned by commit +# SHA rather than by tag for the same reason: tags can be repointed. +# +# Every hook is `language = "system"`, meaning prek shells out to binaries +# rather than building an environment. The runner image supplies bash, dash +# and shellcheck; everything else is installed below at a pinned version, so +# an upstream release cannot silently change what this gate enforces. + +name: CI + +on: + push: + branches: [main] + pull_request: + branches: [main] + +concurrency: + group: ci-${{ github.ref }} + cancel-in-progress: true + +permissions: + contents: read + +jobs: + gate: + name: prek gate + runs-on: ubuntu-latest + timeout-minutes: 10 + env: + PREK_VERSION: 0.4.10 + PREK_SHA256: d31168fcbfe9921ad63572dc7ddbee07be5d5d6befe11e6e7397e3861a4edd49 + GITLEAKS_VERSION: 8.30.1 + ACTIONLINT_VERSION: 1.7.12 + steps: + # actions/checkout v6, pinned to its commit. + - uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 + + # zsh and ksh are absent from the runner image. Without them the + # shell-syntax hook would quietly shrink to the three shells that + # happen to be preinstalled — exactly the coverage gap it exists to + # close, and it would still report success. + - name: Install shells + run: | + set -euo pipefail + sudo apt-get update -qq + sudo apt-get install -y --no-install-recommends zsh ksh + for s in sh bash dash zsh ksh; do + printf '%-5s %s\n' "$s" "$(command -v "$s")" + done + + - name: Install prek + run: | + set -euo pipefail + url="https://github.com/j178/prek/releases/download/v${PREK_VERSION}/prek-x86_64-unknown-linux-musl.tar.gz" + curl -fsSL --retry 3 -o prek.tar.gz "$url" + echo "${PREK_SHA256} prek.tar.gz" | sha256sum -c - + sudo tar -xzf prek.tar.gz -C /usr/local/bin prek + rm -f prek.tar.gz + prek --version + + - name: Install hook tools + run: | + set -euo pipefail + curl -fsSL --retry 3 "https://github.com/gitleaks/gitleaks/releases/download/v${GITLEAKS_VERSION}/gitleaks_${GITLEAKS_VERSION}_linux_x64.tar.gz" \ + | sudo tar -xz -C /usr/local/bin gitleaks + curl -fsSL --retry 3 "https://github.com/rhysd/actionlint/releases/download/v${ACTIONLINT_VERSION}/actionlint_${ACTIONLINT_VERSION}_linux_amd64.tar.gz" \ + | sudo tar -xz -C /usr/local/bin actionlint + gitleaks version + actionlint --version + shellcheck --version + + # --show-diff-on-failure so a hook that rewrites a file + # (end-of-file-fixer, mixed-line-ending) reports the exact diff rather + # than an opaque failure. + - name: Run prek + run: prek run --all-files --show-diff-on-failure --color=always diff --git a/README.md b/README.md index 2070b00..bba15fa 100644 --- a/README.md +++ b/README.md @@ -73,6 +73,19 @@ Toutes optionnelles, à exporter avant le `source`. | `GIT_THEME_BASE_PALETTE` | `dracula` | Palette du profil iTerm2 de base, retirée de l'auto-assignation | | `GIT_THEME_ITERM2_PARENT_PROFILE` | `git-theme:base` | Profil iTerm2 dont héritent les profils générés | +## Développement + +Le contrôle qualité est décrit une seule fois, dans `prek.toml` : + +```bash +prek install # branche le hook git local +prek run --all-files +``` + +La CI ne redéfinit aucune règle : elle installe les outils puis lance ce même `prek run --all-files`. Un pre-commit vert vaut donc une CI verte, et resserrer une règle se fait à un seul endroit. + +Ce que le gate vérifie : espaces traînants, newline finale, fins de ligne, YAML et TOML valides, marqueurs de conflit, `shellcheck`, `actionlint`, détection de secrets (`gitleaks`), et surtout le parsing du script sous les **cinq shells** annoncés en tête de fichier — le script étant *sourcé*, une erreur de syntaxe casse le shell de login, pas une commande. + ## Comment ça marche 1. Vous entrez dans un repo git diff --git a/prek.toml b/prek.toml new file mode 100644 index 0000000..ad51f06 --- /dev/null +++ b/prek.toml @@ -0,0 +1,86 @@ +# Local quality gate. +# +# `.github/workflows/ci.yml` runs this exact file through `prek run +# --all-files`, so there is one hook list rather than two that drift apart: +# a green pre-commit is a green CI, and tightening a rule here tightens it +# in both places at once. +# +# Every hook is `language = "system"` — no environment is provisioned, the +# tools come from the developer's machine and, in CI, from an explicit +# install step. `prek install` wires the git hook locally. + +fail_fast = false +default_install_hook_types = ["pre-commit"] + +# Rust-native hooks: no network, no interpreter, no environment to build. +[[repos]] +repo = "builtin" +hooks = [ + { id = "trailing-whitespace" }, + { id = "end-of-file-fixer" }, + { id = "check-yaml" }, + { id = "check-toml" }, + { id = "check-merge-conflict" }, + { id = "mixed-line-ending", args = ["--fix", "lf"] }, +] + +[[repos]] +repo = "local" + +# ShellCheck. The shebang is `#!/bin/sh`, so the default ruleset is POSIX. +# +# SC3043 ("In POSIX sh, 'local' is undefined") fires on every helper and is +# accepted deliberately: `local` is implemented by all five shells this +# project targets, and dropping it would leak every helper variable into the +# user's interactive shell — a worse outcome than the standards deviation. +# +# SC3045 ("type -t is undefined") is a false positive in context: its only +# call site sits inside an `elif [ -n "${BASH_VERSION-}" ]` branch, so it +# never executes outside bash, where `type -t` is well defined. +# +# What actually backs the portability claim is the shell-syntax hook below, +# which parses the script under each advertised shell rather than reasoning +# about the standard. +[[repos.hooks]] +id = "shellcheck" +name = "shellcheck" +language = "system" +entry = "shellcheck --severity=warning --exclude=SC3043,SC3045" +types = ["shell"] + +# The script is *sourced*, not executed: a parse error takes down the user's +# login shell instead of failing one command. Parsing it under every shell +# named in the file header is the cheapest gate that catches a syntax +# regression before it reaches a shell that cannot start. +# +# A shell that is absent is skipped rather than failing the hook, so the gate +# stays usable on a machine without ksh; CI installs all five, so nothing is +# silently skipped where it counts. +[[repos.hooks]] +id = "shell-syntax" +name = "shell syntax (sh, bash, dash, zsh, ksh)" +language = "system" +entry = "sh" +args = [ + "-c", + 'status=0; for f in "$@"; do for s in sh bash dash zsh ksh; do command -v "$s" >/dev/null 2>&1 || continue; "$s" -n "$f" || { echo " ^ $s cannot parse $f"; status=1; }; done; done; exit "$status"', + "--", +] +types = ["shell"] + +# Scans the working tree rather than git history, so the hook behaves +# identically on staged changes and on a full CI checkout. +[[repos.hooks]] +id = "gitleaks" +name = "gitleaks (secret detection)" +language = "system" +entry = "gitleaks dir . --no-banner --redact" +pass_filenames = false + +[[repos.hooks]] +id = "actionlint" +name = "actionlint" +language = "system" +entry = "actionlint" +files = '^\.github/workflows/.*\.ya?ml$' +pass_filenames = false From 69860215d763544f39fb6c01cec9bcda73c93012 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20LIARD?= Date: Fri, 24 Jul 2026 13:22:21 +0200 Subject: [PATCH 2/2] fix(ci): extract prek from its target-triple directory The install step assumed prek ships its binary at the archive root, as gitleaks and actionlint do. It nests it under prek-x86_64-unknown-linux-musl/, so tar exited 2 with "prek: Not found in archive" after the checksum had already verified. Name the member and strip the leading component. Co-Authored-By: Claude Opus 4.8 (1M context) --- .github/workflows/ci.yml | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b3fcaac..acc7064 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -63,10 +63,13 @@ jobs: - name: Install prek run: | set -euo pipefail - url="https://github.com/j178/prek/releases/download/v${PREK_VERSION}/prek-x86_64-unknown-linux-musl.tar.gz" - curl -fsSL --retry 3 -o prek.tar.gz "$url" + target=prek-x86_64-unknown-linux-musl + curl -fsSL --retry 3 -o prek.tar.gz \ + "https://github.com/j178/prek/releases/download/v${PREK_VERSION}/${target}.tar.gz" echo "${PREK_SHA256} prek.tar.gz" | sha256sum -c - - sudo tar -xzf prek.tar.gz -C /usr/local/bin prek + # prek nests its binary under the target-triple directory, unlike + # gitleaks and actionlint which ship theirs at the archive root. + sudo tar -xzf prek.tar.gz -C /usr/local/bin --strip-components=1 "${target}/prek" rm -f prek.tar.gz prek --version