diff --git a/.githooks/commit-msg b/.githooks/commit-msg new file mode 100755 index 0000000..4b58d23 --- /dev/null +++ b/.githooks/commit-msg @@ -0,0 +1,27 @@ +#!/bin/bash +# Commit message hook - validates Conventional Commits format. +# PR title CI remains the merge-blocking guard for GitHub merge subjects. + +set -e + +ROOT="$(git rev-parse --show-toplevel)" +cd "$ROOT" + +if ! command -v npx >/dev/null 2>&1; then + echo "Warning: npx not found; skipping local commitlint check." >&2 + echo "CI still requires a semantic PR title before merge." >&2 + exit 0 +fi + +if ! output=$(npx --yes \ + --package @commitlint/cli \ + --package @commitlint/config-conventional \ + commitlint --edit "$1" 2>&1); then + if printf '%s\n' "$output" | grep -qE '^(npm (ERR!|error) )?(network|code (ENOTFOUND|EAI_AGAIN|ECONNRESET|ECONNREFUSED|ETIMEDOUT)|could not determine executable)|^Error: could not determine executable'; then + echo "Warning: commitlint unavailable; skipping local check." >&2 + echo "CI still requires a semantic PR title before merge." >&2 + exit 0 + fi + printf '%s\n' "$output" >&2 + exit 1 +fi diff --git a/.github/workflows/semantic-pr-title.yml b/.github/workflows/semantic-pr-title.yml new file mode 100644 index 0000000..91c31ca --- /dev/null +++ b/.github/workflows/semantic-pr-title.yml @@ -0,0 +1,35 @@ +name: Semantic PR Title + +on: + pull_request_target: + types: [opened, edited, synchronize, reopened, ready_for_review] + +permissions: + pull-requests: read + +concurrency: + group: semantic-pr-title-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + validate: + name: Semantic PR Title + runs-on: ubuntu-latest + steps: + - uses: amannn/action-semantic-pull-request@48f256284bd46cdaab1048c3721360e808335d50 # v6.1.1 + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + types: | + build + chore + ci + deps + docs + feat + fix + perf + refactor + revert + style + test diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..6282a5e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,25 @@ +# Contributing + +Use Conventional Commit style for commit subjects and PR titles: + +```text +feat: add workspace snapshot filtering +fix(meta_git_cli): preserve child repo commit type +ci: enforce semantic PR titles +docs: clarify plugin installation +``` + +If you are working from an internal GitKB task, include the task wikilink in +commit messages: + +```text +fix: remove legacy meta agent workspace entry [[tasks/harmony-678]] +``` + +GitHub merge subjects are derived from PR titles, so the semantic PR title check +is the merge-blocking guard for release notes. Local hooks catch malformed commit +messages before push when installed with: + +```sh +make install-hooks +``` diff --git a/Makefile b/Makefile index f0dc7c1..f5cb1d8 100644 --- a/Makefile +++ b/Makefile @@ -46,6 +46,19 @@ clean-plugins: rebuild-plugins: clean-plugins build-plugins +# Install git hooks (commit-msg validates release-safe subjects; pre-push runs CI checks locally) +# Uses git rev-parse to handle worktrees and submodules correctly +install-hooks: + @echo "Installing git hooks..." + @chmod +x .githooks/commit-msg + @chmod +x .githooks/pre-commit + @chmod +x .githooks/pre-push + @mkdir -p "$$(git rev-parse --git-path hooks)" + @hooks_dir="$$(git rev-parse --git-path hooks)" && root="$$(git rev-parse --show-toplevel)" && rel="$$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$$root/.githooks/commit-msg" "$$hooks_dir")" && ln -sf "$$rel" "$$hooks_dir/commit-msg" + @hooks_dir="$$(git rev-parse --git-path hooks)" && root="$$(git rev-parse --show-toplevel)" && rel="$$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$$root/.githooks/pre-commit" "$$hooks_dir")" && ln -sf "$$rel" "$$hooks_dir/pre-commit" + @hooks_dir="$$(git rev-parse --git-path hooks)" && root="$$(git rev-parse --show-toplevel)" && rel="$$(python3 -c 'import os, sys; print(os.path.relpath(sys.argv[1], sys.argv[2]))' "$$root/.githooks/pre-push" "$$hooks_dir")" && ln -sf "$$rel" "$$hooks_dir/pre-push" + @echo "Commit-msg, pre-commit, and pre-push hooks installed." + release: cargo build --release @@ -103,4 +116,4 @@ uninstall: cargo uninstall meta_rust_cli 2>/dev/null || true rm -f ~/.meta/plugins/meta-git ~/.meta/plugins/meta-project ~/.meta/plugins/meta-rust -.PHONY: install build run test bats release integration-test +.PHONY: install install-hooks build run test bats release integration-test diff --git a/commitlint.config.cjs b/commitlint.config.cjs new file mode 100644 index 0000000..e0008b2 --- /dev/null +++ b/commitlint.config.cjs @@ -0,0 +1,23 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "deps", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + ], + ], + }, +};