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
8 changes: 8 additions & 0 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env sh
# commit-msg: warn (never block) about messages that break the project convention.
# Delegates to the shared checker; advisory only. CI runs the same check on PRs.
# See CONTRIBUTING.md.

hook_dir=$(dirname "$0")
sh "$hook_dir/lib/check-message" < "$1" || true
exit 0
45 changes: 45 additions & 0 deletions .githooks/lib/check-message
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#!/usr/bin/env sh
# check-message: warn about commit messages that stray from the project convention.
#
# Reads the full commit message on stdin; prints any problems to stderr.
# Exit 0 = clean, 1 = has warnings. Nothing here ever blocks a commit: the
# commit-msg hook and CI both treat a non-zero exit as advisory only.
# See CONTRIBUTING.md for the convention this checks.

# Normalize exactly as git does on commit (drop comment lines, trim blank runs).
msg=$(git stripspace --strip-comments)
[ -z "$msg" ] && exit 0

subject=$(printf '%s\n' "$msg" | sed -n '1p')
second=$(printf '%s\n' "$msg" | sed -n '2p')
maxlen=72
rc=0

# Exempt merge, revert, and autosquash (fixup!/squash!) subjects.
case "$subject" in
"Merge "*|"Revert "*|"fixup! "*|"squash! "*) exit 0 ;;
esac

warn() {
echo "⚠ $1" >&2
rc=1
}

# Subject shape: <domain>: <Capitalized description>
printf '%s' "$subject" | grep -qE '^\S+: [A-Z].+' \
|| warn "Subject should match '<Domain>: <Description>' (e.g. 'JS: Add password reset form'). Got: $subject"

# Subject length.
[ "${#subject}" -le "$maxlen" ] \
|| warn "Subject is ${#subject} chars (max $maxlen): $subject"

# No trailing period on the subject.
case "$subject" in
*.) warn "Subject should not end with a period: $subject" ;;
esac

# A body must be separated from the subject by a blank line.
[ -z "$second" ] \
|| warn "Leave a blank line between the subject and the body."

exit $rc
20 changes: 20 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,23 @@ jobs:

- name: Compile check
run: pnpm run build

commit-lint:
runs-on: ubuntu-latest
if: github.event_name == 'pull_request'
steps:
- name: Checkout repository
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Check commit messages (advisory — never fails the build)
run: |
for sha in $(git rev-list \
"${{ github.event.pull_request.base.sha }}".."${{ github.event.pull_request.head.sha }}"); do
subject=$(git log -1 --format=%s "$sha")
if ! git log -1 --format=%B "$sha" | sh .githooks/lib/check-message; then
echo "::warning::Commit ${sha} ('${subject}') doesn't follow the convention — see log above."
fi
done
exit 0
15 changes: 15 additions & 0 deletions .gitmessage
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Format: <Domain>: <Description>
# Example: JS: Add password reset form
# Domains: prefer capitalized (JS, Java, DB, CI, Docs, Infra, Hooks...)
# symbols ok when natural (.git, v2, JS/CSS, API/Auth)
# Rules: imperative mood, capitalize description, ≤72 chars, no trailing period
<Domain>:

# Body (optional): explain WHY, not what. Separate from the subject with a blank line.
# (leave blank to omit)


# Footers (optional git trailers, "Key: value" — add any you need):
# Test: none|manual|compile|auto
# Refs: #12 or a ticket id, e.g. PROJ-123
# Co-authored-by: Name <email>
26 changes: 26 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,32 @@ See `Justfile` at the repo root for all available commands.

---

## Commit Message Guidelines

Use the **domain prefix** format:

```
<Domain>: <Description>
```

- Domain: short label for the area changed, prefer capitalized (`JS`, `Java`, `DB`, `CI`, `Docs`, `Infra`). Symbols are fine when natural (`JS/CSS`, `API/Auth`, `.git`).
- Description: capitalized, imperative mood (`Add login page`, not `Added login page`).
- Subject line ≤ 72 chars, no trailing period.

Examples:

```
JS: Add password reset form
Java: Validate email uniqueness on signup
DB: Add users table migration
CI: Cache pnpm dependencies in workflow
```

AI Agents should add a Co-Authored by with their model when writing a commit message.
See `CONTRIBUTING.md` for the full spec.

---

## Secret Management

Secrets are encrypted with [SOPS](https://github.com/getsops/sops) and committed as `secrets-ro.yaml` (read-only) and `secrets-rw.yaml` (read-write). Never commit plaintext secrets.
Expand Down
95 changes: 95 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
# Contributing to PatChats

## Commit Message Guidelines

Commit messages use a **domain prefix** format that communicates *where* a change lives at a glance.

### Format

```
<Domain>: <Description>

[optional body]

[optional footers]
```

### Rules

- **Domain** — a short label for the area changed. Prefer starting with a capital letter. Symbols are fine when natural (e.g. `.git`, `v2`, `JS/CSS`).
- **Description** — capitalized, imperative mood. Write what the commit *does*, not what you *did*.
- ✅ `JS: Add password reset form`
- ❌ `JS: added password reset form`
- **Subject line** — 72 characters max, no trailing period.
- **Body** (optional) — explain *why*, not *what*. Separate it from the subject with a blank line.
- **Footers** (optional) — structured [git trailers](#footers); see below.

No fixed domain taxonomy — pick whatever makes the change's location obvious to someone scanning the git log.

### Examples

```
JS: Add password reset form
Java: Validate email uniqueness on signup
DB: Add users table migration
CI: Cache pnpm dependencies in workflow
Docs: Document commit message convention
Hooks: Add commit-msg validation hook
Infra: Pin Java version to 25 in pom.xml
JS/CSS: Fix button hover contrast ratio
API/Auth: Restrict admin endpoints to ROLE_ADMIN
```

### Footers

Footers are optional [git trailers](https://git-scm.com/docs/git-interpret-trailers) — `Key: value` lines at the end of the message, after a blank line. Sticking to the trailer format means `git interpret-trailers`, GitHub, and changelog tools can parse them.

| Trailer | Use |
|---|---|
| `Test:` | How the change was verified — `none`, `manual`, `compile`, or `auto` |
| `Refs:` | Link an issue or ticket — `#12`, or a ticket id like `PROJ-123` |
| `Co-authored-by:` | Credit a pair partner or AI assistant — `Name <email>` |
| `BREAKING CHANGE:` | Describe an incompatible change and how to migrate |

Example with body and footers:

```
Java: Validate email uniqueness on signup

Prevents duplicate accounts when two OAuth providers report the same email.

Refs: #42
Test: auto
Co-authored-by: Ada Lovelace <ada@example.com>
```

### Merge strategy & commit hygiene

We **rebase-merge** PRs, so every commit you make lands on `main` as-is. A little hygiene keeps the history readable:

- Keep commits **atomic** — one logical change each.
- Tidy up `wip` / `fix typo` / `address review` commits before merge (`git rebase -i main`, or `git commit --fixup=<sha>` then `git rebase -i --autosquash`).
- Ideally each commit subject follows the convention above — not just the PR title.

### Conventions are advisory

Nothing here is blocking — this repo is a learning playground, and we optimize for development speed.

- The local `commit-msg` hook *warns* at commit time (install via `just install-pre-scripts`).
- CI adds a *non-blocking* check that annotates any PR commits that stray from the convention.

Treat both as friendly nudges: clean history helps everyone learning from it, but a warning will never stop your work.

### Setup

Run:

```sh
just install-pre-scripts
```

This registers the commit message template (so your editor pre-fills it on `git commit`) and points Git at the shared `.githooks` directory. To register just the template manually:

```sh
git config commit.template .gitmessage
```
3 changes: 2 additions & 1 deletion Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ encrypt file *args:
edit file *args:
just install-pre-scripts && sops edit {{ file }} {{ args }}

# Configure Git to use the .githooks directory (idempotent)
# Configure Git to use the .githooks directory and commit message template (idempotent)
install-pre-scripts:
[ "$(git config core.hooksPath)" = ".githooks" ] || git config core.hooksPath .githooks
[ "$(git config commit.template)" = ".gitmessage" ] || git config commit.template .gitmessage