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: 1 addition & 1 deletion .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -159,4 +159,4 @@ jobs:
-
env:
FETCH_BASE_REF: "true"
run: ./ci/conventional_commits.sh
run: ./ci/validate_commit_range.sh
11 changes: 5 additions & 6 deletions app/usage-rules.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,12 @@
- Mark breaking changes with `!` before the colon (e.g. `feat!: ...`).
- Bare `Merge branch ...` subjects are rejected — reword as `chore: Merge branch ...`.
- Enforced locally by the `commit-msg` hook at `git-hooks/commit-msg` (each
commit's own subject, via its shared subject validator) and the `pre-push`
hook at `git-hooks/pre-push` (`mix precommit`, which validates
every commit about to be pushed and runs the dependency security audits,
formatting, static analysis, and tests) — run `mix setup` once per clone
to activate both.
commit's own subject, via `ci/validate_conventional_subject.sh`) and the
`pre-push` hook at `git-hooks/pre-push` (every non-deletion ref update,
via `ci/validate_push_refs.sh`) — run `mix setup` once per clone to
activate both.
- Enforced in CI across a whole PR's commit range by
`ci/conventional_commits.sh` (skips GitHub's own auto-generated
`ci/validate_commit_range.sh` (skips GitHub's own auto-generated
update-branch merge commits).

## Dogfooding: use `lc`, not a Linear MCP server or skill
Expand Down
4 changes: 0 additions & 4 deletions ci/conventional_commits.sh

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ git_or_die() {
}

repo_top=$(git_or_die rev-parse --show-toplevel)
validator="$repo_top/git-hooks/validate-conventional-subject"
validator="$repo_top/ci/validate_conventional_subject.sh"

[ -x "$validator" ] || die "validator is not executable: $validator"

Expand Down
4 changes: 0 additions & 4 deletions ci/validate_conventional_commit.sh

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ then
fi

repo_top=$(git rev-parse --show-toplevel) || exit 1
validator="$repo_top/git-hooks/validate-conventional-subject"
validator="$repo_top/ci/validate_conventional_subject.sh"

if [ ! -x "$validator" ]
then
Expand Down
90 changes: 90 additions & 0 deletions ci/validate_push_refs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
# Validates commit subjects for every non-deletion ref update received on
# Git's pre-push stdin. Each input line has the format:
# <local-ref> <local-sha1> <remote-ref> <remote-sha1>
#
# Deletions (local SHA is all zeros) are skipped. For new branches (remote
# SHA is all zeros) the base is the merge base with origin/main or main.
# For existing remote branches the exact remote SHA is used as the base.
#
# Activated automatically by git-hooks/pre-push. May also be invoked
# directly for testing or manual audits with push-ref lines on stdin.

repo_top=$(git rev-parse --show-toplevel) || exit 1
validator="$repo_top/ci/validate_conventional_subject.sh"

if [ ! -x "$validator" ]
then
printf "ERROR: validator not executable: %s\n" "$validator" >&2
exit 1
fi

zero_sha="0000000000000000000000000000000000000000"
github_merge_pattern="^Merge branch '[^']+' into .+"
validation_status=0

while read -r local_ref local_sha remote_ref remote_sha
do
[ "$local_sha" = "$zero_sha" ] && continue

if [ "$remote_sha" = "$zero_sha" ]
then
base_ref=""
for candidate in origin/main main
do
if git rev-parse --verify --quiet "$candidate" >/dev/null
then
base_ref=$candidate
break
fi
done

if [ -z "$base_ref" ]
then
printf "ERROR: unable to resolve base ref for new branch %s\n" "$local_ref" >&2
validation_status=1
continue
fi

base_sha=$(git merge-base "$local_sha" "$base_ref" 2>&1)
if [ $? -ne 0 ]
then
printf "ERROR: git merge-base %s %s failed: %s\n" "$local_sha" "$base_ref" "$base_sha" >&2
validation_status=1
continue
fi
else
base_sha="$remote_sha"
fi

while IFS= read -r -d '' entry
do
IFS=$'\x01' read -r parents committer_name committer_email subject <<< "$entry"

if [ -n "$parents" ]
then
IFS=' ' read -ra parents_array <<< "$parents"
parent_count=${#parents_array[@]}
else
parent_count=0
fi

if [ "$parent_count" -eq 2 ] \
&& [ "$committer_name" = "GitHub" ] \
&& [ "$committer_email" = "noreply@github.com" ] \
&& [[ "$subject" =~ $github_merge_pattern ]]
then
continue
fi

"$validator" --subject "$subject"
status=$?

if [ "$status" -ne 0 ]
then
validation_status=$status
fi
done < <(git log -z --format='%P%x01%cn%x01%ce%x01%s' "$base_sha..$local_sha")
done

exit "$validation_status"
2 changes: 1 addition & 1 deletion git-hooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@
# mix git_hooks

repo_top=$(git rev-parse --show-toplevel) || exit 1
exec "$repo_top/git-hooks/validate-conventional-subject" "$1"
exec "$repo_top/ci/validate_conventional_subject.sh" "$1"
9 changes: 3 additions & 6 deletions git-hooks/pre-push
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
#!/bin/sh
# Runs the same complete validation gate as CI before every push, including
# Hex's retired/vulnerable-package audit. It also validates every commit since
# the branch diverged from its base (catching a commit made before the hooks
# were installed, an amend, a rebase, etc.). Activate with:
# Validates every commit subject introduced by this push.
# See app/usage-rules.md for the rule. Activate with:
# mix git_hooks

repo_top=$(git rev-parse --show-toplevel) || exit 1
cd "$repo_top" || exit 1
exec mix precommit
exec "$repo_top/ci/validate_push_refs.sh" "$@"
12 changes: 6 additions & 6 deletions lib/mix/tasks/precommit.ex
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ defmodule Mix.Tasks.Precommit do
GitHub Actions. Cheap metadata guards run first so an invalid pull request
title or commit subject fails before dependency setup and the test suite:

1. `git-hooks/validate-pull-request-title` — require a Conventional
Commits pull request title when `PULL_REQUEST_TITLE_REQUIRED=true`
2. `git-hooks/validate-commit-range` — validate every commit since the
branch diverged from its base
1. `ci/validate_pull_request_title.sh` — require a Conventional Commits
pull request title when `PULL_REQUEST_TITLE_REQUIRED=true`
2. `ci/validate_commit_range.sh` — validate every commit since the branch
diverged from its base
3. `mix deps.get` — ensure app dependencies are present
4. `mix hex.audit` — reject retired or vulnerable Hex packages
5. `mix deps.audit` — scan dependencies for known security advisories
Expand Down Expand Up @@ -42,8 +42,8 @@ defmodule Mix.Tasks.Precommit do

@doc false
def run([], shell) do
shell.("./git-hooks/validate-pull-request-title", [], [])
shell.("./git-hooks/validate-commit-range", [], [])
shell.("./ci/validate_pull_request_title.sh", [], [])
shell.("./ci/validate_commit_range.sh", [], [])
shell.("mix", ["deps.get"], cd: "app")
shell.("mix", ["hex.audit"], cd: "app")
shell.("mix", ["deps.audit"], cd: "app")
Expand Down
Loading
Loading