From 7270359d146035f89b14f3c25c18a5f5ebde558d Mon Sep 17 00:00:00 2001 From: Mihaela M Date: Fri, 26 Jun 2026 06:14:24 +0200 Subject: [PATCH] ci(githooks): enforce format, lint, and conventional commits Run SwiftFormat and SwiftLint on staged Swift files in the pre-commit hook (formatting-and-linting.md), gracefully skipping when a tool is absent since CI re-runs both. Enforce the Conventional Commits subject format and the 72-character subject limit in the commit-msg hook (commits.md). Correct the pre-push comment that named the removed ci.yml. --- .githooks/commit-msg | 22 ++++++++++++++++++++++ .githooks/pre-commit | 26 ++++++++++++++++++++++++++ .githooks/pre-push | 2 +- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/.githooks/commit-msg b/.githooks/commit-msg index 8c30b65..4c513a4 100755 --- a/.githooks/commit-msg +++ b/.githooks/commit-msg @@ -123,4 +123,26 @@ for ident in "$(git var GIT_AUTHOR_IDENT 2>/dev/null)" "$(git var GIT_COMMITTER_ fi done +# commits.md Rule 1/2: subject must follow Conventional Commits (lowercase type +# from the allowed set, optional scope, optional ! for breaking) and be at most +# 72 characters. Merge/revert/fixup/squash subjects are tooling-generated and +# exempt. +SUBJECT=$(printf '%s\n' "$CONTENT" | sed '/^[[:space:]]*$/d' | head -1) +case "$SUBJECT" in + "Merge "*|"Revert "*|"fixup! "*|"squash! "*|"") ;; + *) + if [ "${#SUBJECT}" -gt 72 ]; then + printf 'commit-msg: subject is %s chars (max 72).\n' "${#SUBJECT}" >&2 + printf ' Rule: commits.md Rule 1\n' >&2 + FAIL=1 + fi + if ! printf '%s' "$SUBJECT" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|build|ci|chore)(\([a-z0-9._-]+\))?!?: .+'; then + printf 'commit-msg: subject must be Conventional Commits: "type(scope): summary".\n' >&2 + printf ' Allowed types: feat fix docs style refactor perf test build ci chore\n' >&2 + printf ' Rule: commits.md Rule 1, Rule 2\n' >&2 + FAIL=1 + fi + ;; +esac + exit $FAIL diff --git a/.githooks/pre-commit b/.githooks/pre-commit index 01e4ae4..b95a835 100755 --- a/.githooks/pre-commit +++ b/.githooks/pre-commit @@ -224,6 +224,32 @@ while IFS= read -r file; do done <<<"$EMOJI_TELLS" done <<<"$SCAN_FILES" +# SwiftFormat + SwiftLint on staged Swift files (formatting-and-linting.md +# acceptance item 2). Run only when the tool is installed; CI re-runs both as a +# required gate (gates.yml), so a machine lacking the tools cannot land +# unformatted or lint-dirty code regardless. +STAGED_SWIFT=$(printf '%s\n' "$STAGED_FILES" | grep -E '\.swift$' || true) +if [ -n "$STAGED_SWIFT" ]; then + if command -v swiftformat >/dev/null 2>&1; then + if ! printf '%s\n' "$STAGED_SWIFT" | xargs swiftformat --lint --config .swiftformat >/dev/null 2>&1; then + printf 'pre-commit: SwiftFormat found unformatted staged Swift.\n' >&2 + printf ' Fix: swiftformat . --config .swiftformat\n' >&2 + FAIL=1 + fi + else + printf 'pre-commit: swiftformat not installed; format gate skipped (CI enforces).\n' >&2 + fi + if command -v swiftlint >/dev/null 2>&1; then + if ! printf '%s\n' "$STAGED_SWIFT" | xargs swiftlint lint --strict --quiet --config .swiftlint.yml >/dev/null 2>&1; then + printf 'pre-commit: SwiftLint violations in staged Swift.\n' >&2 + printf ' Fix: swiftlint --config .swiftlint.yml --strict\n' >&2 + FAIL=1 + fi + else + printf 'pre-commit: swiftlint not installed; lint gate skipped (CI enforces).\n' >&2 + fi +fi + if [ "$FAIL" -ne 0 ]; then printf '\n' >&2 printf 'Hook scope: only ADDED lines in the staged diff are gated.\n' >&2 diff --git a/.githooks/pre-push b/.githooks/pre-push index c1648f1..d3c5449 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -1,6 +1,6 @@ #!/usr/bin/env bash # pre-push hook: run the mechanical gates locally before a push reaches GitHub. -# These mirror the CI workflow (.github/workflows/ci.yml) so red is caught here +# These mirror the CI workflows (.github/workflows/) so red is caught here # first. PureYAML is a root Swift package, so Swift gates run at repo root. # # Install (per repo): git config core.hooksPath .githooks