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
91 changes: 91 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
# 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
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 -
# 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

- 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
13 changes: 13 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
86 changes: 86 additions & 0 deletions prek.toml
Original file line number Diff line number Diff line change
@@ -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