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
89 changes: 81 additions & 8 deletions .githooks/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,95 @@
# Validates commit messages against the Conventional Commits spec.
# Activate with: git config core.hooksPath .githooks

set -e

MSG=$(cat "$1")

# Strip comments and trailing whitespace
SUBJECT=$(echo "$MSG" | sed '/^#/d' | head -1 | sed 's/[[:space:]]*$//')
# Strip git comments (lines starting with #) to get the actual message
SUBJECT=$(echo "$MSG" | grep -v '^#' | head -1)

# Remove trailing whitespace
SUBJECT=$(echo "$SUBJECT" | sed 's/[[:space:]]*$//')

# Validate commit message format: type(scope): subject
# Types: feat, fix, docs, style, refactor, perf, test, chore, ci, revert
# Scope is optional
# Subject must be lowercase, start with lowercase, no period at end
# Max length: 100 characters for full header, 72 for subject line

TYPE_PATTERN='feat|fix|docs|style|refactor|perf|test|chore|ci|revert'
SCOPE_PATTERN='[a-z0-9\-]*'

PATTERN='^(feat|fix|refactor|test|docs|chore|perf|ci)(\(.+\))?(!)?: .{1,72}$'
# Full pattern: type(scope): subject
# Where scope is optional
if echo "$SUBJECT" | grep -qE "^(${TYPE_PATTERN})(\(${SCOPE_PATTERN}\))?: .+\$"; then
# Check length
LENGTH=$(echo "$SUBJECT" | wc -c)
if [ "$LENGTH" -gt 101 ]; then
echo ""
echo " ✗ Commit message rejected — header exceeds 100 characters."
echo ""
echo " Length: $LENGTH (max: 100)"
echo " Your message: $SUBJECT"
echo ""
exit 1
fi

if ! echo "$SUBJECT" | grep -qE "$PATTERN"; then
# Check subject line (after ": ") is not empty and doesn't end with period
SUBJECT_TEXT=$(echo "$SUBJECT" | sed 's/^[^:]*: //')
if [ -z "$SUBJECT_TEXT" ]; then
echo ""
echo " ✗ Commit message rejected — missing description after colon."
echo ""
exit 1
fi

if echo "$SUBJECT_TEXT" | grep -q '\.$'; then
echo ""
echo " ✗ Commit message rejected — subject should not end with a period."
echo ""
echo " Your message: $SUBJECT"
echo ""
exit 1
fi

# Check first letter after colon+space is lowercase
FIRST_CHAR=$(echo "$SUBJECT_TEXT" | sed 's/^.//')
if ! echo "$SUBJECT_TEXT" | grep -qE '^[a-z]'; then
echo ""
echo " ✗ Commit message rejected — subject must start with lowercase letter."
echo ""
echo " Your message: $SUBJECT"
echo ""
exit 1
fi

exit 0
else
echo ""
echo " Commit message rejected — does not follow Conventional Commits."
echo " Commit message rejected — does not follow Conventional Commits."
echo ""
echo " Format : <type>(<scope>): <description>"
echo " Format: <type>(<scope>): <description>"
echo " Example: feat(auth): add refresh token rotation"
echo ""
echo " Allowed types: feat | fix | refactor | test | docs | chore | perf | ci"
echo " Subject must be lowercase and under 72 characters."
echo " Allowed types:"
echo " • feat — new feature"
echo " • fix — bug fix"
echo " • docs — documentation"
echo " • style — code style (formatting, semicolons, etc)"
echo " • refactor — code refactoring without feature change"
echo " • perf — performance improvement"
echo " • test — test changes"
echo " • chore — dependency or tooling change"
echo " • ci — CI/CD changes"
echo " • revert — revert a previous commit"
echo ""
echo " Rules:"
echo " • Type and scope are lowercase"
echo " • Scope is optional"
echo " • Subject starts with lowercase"
echo " • No period at end"
echo " • Max 100 characters"
echo ""
echo " Your message: $SUBJECT"
echo ""
Expand Down
8 changes: 4 additions & 4 deletions .githooks/pre-push
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,10 @@
# Uncomment and adapt the checks below to match your project's tooling.
# Remove checks that don't apply. Keep this hook fast — slow hooks get bypassed.

# echo "Running tests before push..."
# your-test-command || exit 1
echo "Running tests before push..."
make test || exit 1

# echo "Running build check..."
# your-build-command || exit 1
echo "Running build check..."
make build || exit 1

exit 0
6 changes: 5 additions & 1 deletion .github/workflows/commitlint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ name: Commitlint
on:
pull_request:
branches: [main]
push:
branches: [main]

jobs:
commitlint:
Expand All @@ -13,8 +15,10 @@ jobs:
with:
fetch-depth: 0

- name: Check commit messages
- name: Validate commit messages
uses: wagoid/commitlint-github-action@v6
with:
configFile: .commitlintrc.json
failOnWarnings: false
helpURL: https://www.conventionalcommits.org
token: ${{ secrets.GITHUB_TOKEN }}
123 changes: 99 additions & 24 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,74 +72,149 @@ Open a **Feature Request** issue with:

Features that align with the project's scope and architecture are more likely to be accepted.

### Activating the commit-msg hook
### Activating git hooks

A shell-based hook validates commit messages locally against the Conventional Commits spec. Activate it once after cloning — no runtime or dependencies required:
Git hooks validate commit messages locally and prevent commits that don't follow our conventions. Activate them once after cloning:

**Using Make (recommended):**
```bash
make hooks
```

**Or manually:**
```bash
git config core.hooksPath .githooks
chmod +x .githooks/commit-msg
```

Commit messages are also validated in CI on every PR via GitHub Actions.
Commit messages are validated:
- **Locally** — before commit (via `.githooks/commit-msg` hook)
- **In CI** — on every PR (via GitHub Actions with commitlint)

**View commit rules anytime:**
```bash
make commit-lint
```

### Submitting Code Changes

1. Create a branch from `main`:
1. **Create a branch** from `main`:
```bash
git checkout main
git pull upstream main
git checkout -b feat/your-feature-name
```
2. Make your changes following the [code style](#code-style) guidelines
3. Write or update tests
4. Run the validation suite locally (see [Development Setup](docs/getting-started/README.md))
5. Commit following [commit conventions](#commit-conventions)
6. Push and open a Pull Request

2. **Make your changes** following the [code style](#code-style) guidelines

3. **Write or update tests** as needed

4. **Run validation locally**:
```bash
make validate
```
This runs: format → vet → lint → tests (with nice colored output)

5. **Commit following [commit conventions](#commit-conventions)**:
```bash
git commit -m "feat(scope): your message"
```
Your commit message will be validated automatically by the local hook.

6. **Push and open a Pull Request**:
```bash
git push origin feat/your-feature-name
```

**Before submitting the PR, verify:**
- [ ] All validations pass (`make validate`)
- [ ] Commits follow Conventional Commits (`make commit-lint` to review rules)
- [ ] Tests pass (`make test`)
- [ ] Code is formatted (`make fmt`)
- [ ] Documentation is updated if needed

---

## Commit Conventions

We follow **Conventional Commits**. Each commit message should be:
We follow **Conventional Commits** format. Messages are validated both locally (via git hook) and in CI.

### Format

```
<type>(<scope>): <short description>
<type>(<scope>): <description>

[optional body]

[optional footer]
```

**Types:**
### Types

| Type | When to use |
|------------|---------------------------------------|
| `feat` | New feature or behavior |
| `fix` | Bug fix |
| `docs` | Documentation only |
| `style` | Code style (formatting, semicolons) |
| `refactor` | Code change with no behavior change |
| `perf` | Performance improvement |
| `test` | Adding or updating tests |
| `docs` | Documentation only |
| `chore` | Tooling, dependencies, config |
| `perf` | Performance improvement |
| `ci` | CI/CD changes |
| `revert` | Revert a previous commit |

**Scope** (optional): the module or area affected, e.g. `auth`, `api`, `generators`, `docker`.

**Scope** (optional): the module or area affected, e.g. `auth`, `api`, `ui`, `docker`.
### Examples

**Examples:**
✅ **Good examples:**
```
feat(auth): add refresh token revocation on logout
fix(api): return 409 when resource already exists
refactor(core): extract validation to separate utility
test(auth): add unit tests for login use case
docs(setup): add environment variable reference
chore(deps): upgrade dependencies
feat: add user authentication
feat(api): add rate limiting middleware
fix(generators): handle empty project spec
docs(readme): update installation steps
refactor(survey): extract validation logic
test: add test for spec validation
chore: update dependencies
ci: add commitlint to GitHub Actions
```

**Rules:**
❌ **Bad examples (will be rejected):**
```
Add user auth # Missing type
FEAT: add auth # Type not lowercase
feat: Add auth. # Description starts with uppercase, ends with period
feat(api): this is a very long commit message that exceeds the 100 character limit # Too long
```

### Rules

- **Type is required** and must be lowercase
- **Scope is optional** (lowercase) and indicates what part changed
- **Description is required**, starts with lowercase, no period at end
- **Max 100 characters** for the full header (subject line)
- Use the **imperative mood** ("add" not "adds" or "added")
- Keep the first line under **72 characters**
- Reference issues in the footer: `Closes #42`, `Fixes #17`

### Local Validation

Your commits are validated automatically before creation. If the message is invalid, the commit is rejected with a helpful error message.

**To see validation rules:**
```bash
make commit-lint
```

**If a commit is rejected, fix and try again:**
```bash
git commit --amend -m "feat(scope): corrected message"
```

### CI Validation

Commits are also validated in GitHub Actions using commitlint with the `.commitlintrc.json` configuration. This ensures consistency across all contributions.

---

## Pull Request Process
Expand Down
35 changes: 22 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -65,25 +65,34 @@ See [docs/getting-started](docs/getting-started/README.md) for the full setup gu

## Development

We use a **Makefile** for convenient command execution with clean, colored output:

```bash
# Build the CLI binary
go build -o scaffold ./cmd/scaffold
# See all available commands
make help

# Run the CLI directly (interactive questionnaire)
go run ./cmd/scaffold new
# Build and run the CLI
make scaffold

# Run tests
go test ./...
# Run validation suite (fmt → vet → lint → test)
make validate

# Run tests with coverage
go test -v -coverprofile=coverage.out ./...
go tool cover -html=coverage.out
# Individual commands
make build # Build to bin/scaffold
make test # Run tests
make fmt # Format code
make lint # Lint code
make clean # Remove build artifacts
```

# Format code
go fmt ./...
**Or use raw Go commands:**

# Lint code
golangci-lint run ./...
```bash
go build -o scaffold ./cmd/scaffold # Build
go run ./cmd/scaffold # Run
go test ./... # Test
go fmt ./... # Format
golangci-lint run ./... # Lint
```

---
Expand Down
Loading
Loading