diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml new file mode 100644 index 0000000..12494c9 --- /dev/null +++ b/.github/workflows/commitlint.yml @@ -0,0 +1,69 @@ +name: Commitlint + +on: + pull_request: + types: [opened, synchronize, reopened] + +# Workflow only reads the event payload (SHAs) + local git log. No +# pull-requests API calls, so no pull-requests scope needed. +permissions: + contents: read + +jobs: + commitlint: + name: Validate conventional commits + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + # SHA-pinned per GitHub's supply-chain recommendation. To update, + # find the new SHA: `gh api repos/actions/checkout/git/refs/tags/v5` + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + with: + fetch-depth: 0 + + - name: Validate commit messages + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + # SHAs are hex so safe to interpolate, but use env vars per + # GitHub Actions injection-defense guidance. + COMMITS=$(git log --format=%s "$BASE_SHA".."$HEAD_SHA") + + # Escape user-controlled content before printing inside a + # ::error:: workflow command. Without this, a commit subject + # containing `%`, `\r`, or `\n` could spoof log output or + # inject further workflow commands. + escape_workflow_msg() { + local s="$1" + s="${s//\%/%25}" + s="${s//$'\r'/%0D}" + s="${s//$'\n'/%0A}" + printf '%s' "$s" + } + + FAILED=0 + while IFS= read -r msg; do + [[ -z "$msg" ]] && continue + # Allow merge commits. Single space, not escaped — `Merge ` + # is the literal prefix of `Merge branch …` and + # `Merge pull request …`. + if [[ "$msg" == Merge\ * ]]; then + continue + fi + # Use printf instead of echo to avoid `-n`/`-e` flag parsing + # on subjects that happen to start with a dash. + if ! printf '%s\n' "$msg" | grep -qE '^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|revert)(\([a-z0-9-]+\))?!?: .+'; then + printf '::error::Invalid commit message: %s\n' "$(escape_workflow_msg "$msg")" + printf ' Expected format: type(scope?): subject\n' + printf ' Valid types: feat, fix, docs, style, refactor, perf, test, chore, ci, build, revert\n' + FAILED=1 + fi + done <<< "$COMMITS" + + if [[ "$FAILED" -eq 1 ]]; then + printf '\nSee https://www.conventionalcommits.org/ for the full spec.\n' + exit 1 + fi + + printf '✓ All commits follow Conventional Commits format.\n'