Skip to content
Open
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
77 changes: 77 additions & 0 deletions .github/workflows/commit-email.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
name: commit-email

# Reusable check for PUBLIC repos: every commit a pull request (or push) adds
# must be authored and committed with a work address. Git identity is published
# in every commit and cannot be corrected without rewriting history other people
# have cloned, so on a public repo it is checked before merge.
#
# It checks only the commits being added, never existing history, so adopting
# it does not require a rewrite. Bot identities GitHub itself assigns (Renovate
# and other `[bot]` apps, Copilot, and `noreply@github.com` as the committer of a
# web-UI merge) are allowed.
#
# Consume from a public repo with a caller workflow:
# on: [pull_request, push]
# jobs:
# commit-email:
# uses: FlowMatrix-AI/.github/.github/workflows/commit-email.yml@main
#
# Standard: FlowMatrix-AI eng-governance, commit identity on public repos.

on:
workflow_call:
inputs:
domain:
description: Required email domain for human authors and committers
type: string
default: "flowmatrixai.com"

permissions:
contents: read

jobs:
check:
name: commit-email
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
fetch-depth: 0

- name: Check commit author and committer emails
env:
DOMAIN: ${{ inputs.domain }}
BASE: ${{ github.event.pull_request.base.sha || github.event.before }}
HEAD: ${{ github.event.pull_request.head.sha || github.sha }}
run: |
set -euo pipefail
# A new branch's first push has an all-zero "before"; check HEAD alone.
if [ -z "$BASE" ] || [ "$BASE" = "0000000000000000000000000000000000000000" ]; then
range="$HEAD -1"
else
range="$BASE..$HEAD"
fi
allowed() {
case "$1" in
*@"$DOMAIN") return 0 ;;
noreply@github.com) return 0 ;;
*"[bot]@users.noreply.github.com") return 0 ;;
*+Copilot@users.noreply.github.com) return 0 ;;
esac
return 1
}
bad=0
# shellcheck disable=SC2086 # range is "A..B" or "SHA -1" by design
while IFS='|' read -r sha ae ce; do
for e in "$ae" "$ce"; do
if ! allowed "$e"; then
echo "::error::${sha:0:7} uses $e (want *@$DOMAIN)"
bad=1
fi
done
done < <(git log --format='%H|%ae|%ce' $range)
if [ "$bad" -ne 0 ]; then
echo "Fix: git config user.email <name>@$DOMAIN in this repo, then amend or rebase the listed commits before merging."
exit 1
fi
echo "All commits in $range use a *@$DOMAIN or bot identity."
54 changes: 54 additions & 0 deletions tests/commit-email.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
#!/usr/bin/env bash
# Runs the exact `run:` block from commit-email.yml against throwaway git repos
# in a temp dir. Usage: bash tests/commit-email.test.sh
set -uo pipefail
WF="${1:-$(dirname "$0")/../.github/workflows/commit-email.yml}"
T="$(mktemp -d)"
trap 'rm -rf "$T"' EXIT
python3 -c "import yaml,sys;d=yaml.safe_load(open(sys.argv[1]));print(d['jobs']['check']['steps'][1]['run'])" "$WF" > "$T/check.sh"

mk() { # dir
git init -q "$1"; git -C "$1" -c user.email=cb@flowmatrixai.com -c user.name=base commit -q --allow-empty -m base
}
add() { # dir author-email committer-email
GIT_AUTHOR_NAME=a GIT_AUTHOR_EMAIL="$2" GIT_COMMITTER_NAME=c GIT_COMMITTER_EMAIL="$3" \
git -C "$1" commit -q --allow-empty -m "c $2"
}
run() { # name dir base expect
local base="$3" head out rc
head=$(git -C "$2" rev-parse HEAD)
out=$(cd "$2" && DOMAIN=flowmatrixai.com BASE="$base" HEAD="$head" bash "$T/check.sh" 2>&1); rc=$?
if { [ "$4" = pass ] && [ $rc -eq 0 ]; } || { [ "$4" = fail ] && [ $rc -ne 0 ]; }; then
echo "ok - $1 (exit $rc)"
else
echo "FAIL - $1 (exit $rc, wanted $4)"; echo "$out"; FAILED=1
fi
}
FAILED=0

mk "$T/a"; b=$(git -C "$T/a" rev-parse HEAD); add "$T/a" cb@flowmatrixai.com cb@flowmatrixai.com
run "work-address commit passes" "$T/a" "$b" pass

mk "$T/b"; b=$(git -C "$T/b" rev-parse HEAD); add "$T/b" someone@gmail.com someone@gmail.com
run "personal-address commit fails" "$T/b" "$b" fail

mk "$T/c"; b=$(git -C "$T/c" rev-parse HEAD); add "$T/c" cb@flowmatrixai.com someone@gmail.com
run "work author, personal committer fails" "$T/c" "$b" fail

mk "$T/d"; b=$(git -C "$T/d" rev-parse HEAD)
add "$T/d" "29139614+renovate[bot]@users.noreply.github.com" noreply@github.com
add "$T/d" "198982749+Copilot@users.noreply.github.com" "198982749+Copilot@users.noreply.github.com"
run "bot identities pass" "$T/d" "$b" pass

mk "$T/e"; git -C "$T/e" -c user.email=old@gmail.com -c user.name=o commit -q --allow-empty -m old
b=$(git -C "$T/e" rev-parse HEAD); add "$T/e" cb@flowmatrixai.com cb@flowmatrixai.com
run "existing personal history is not re-checked" "$T/e" "$b" pass

mk "$T/f"; add "$T/f" someone@gmail.com someone@gmail.com
run "new-branch push (zero base) checks HEAD" "$T/f" 0000000000000000000000000000000000000000 fail

mk "$T/g"; b=$(git -C "$T/g" rev-parse HEAD)
add "$T/g" someone@gmail.com someone@gmail.com; add "$T/g" cb@flowmatrixai.com cb@flowmatrixai.com
run "personal commit below a work HEAD in the range fails" "$T/g" "$b" fail

exit $FAILED
Loading