at, which mandates a predictable structure: type(scope?): subject.
Architecture Decision: Use husky to manage Git hooks and @commitlint/cli for validation. This combination runs locally before commits are created, providing immediate feedback without blocking CI pipelines.
// commitlint.config.ts
import type { UserConfig } from '@commitlint/types';
const Configuration: UserConfig = {
extends: ['@commitlint/config-conventional'],
rules: {
'type-enum': [
2,
'always',
['feat', 'fix', 'docs', 'style', 'refactor', 'test', 'chore', 'ci', 'perf']
],
'scope-enum': [
2,
'always',
['auth', 'billing', 'inventory', 'ui', 'infra', 'deps']
],
'subject-max-length': [2, 'always', 72],
'body-max-line-length': [1, 'always', 100]
}
};
export default Configuration;
Rationale: Restricting scopes to domain-specific modules prevents vague categorization. Enforcing line length limits ensures readability in terminal outputs and Git log viewers. The perf and ci types extend the base specification to cover performance optimizations and pipeline changes, which are frequently omitted but critical for operational tracking.
Step 2: Standardize Pull Request Context
A pull request is a contract between the author and the reviewer. It must answer three questions: Why does this change exist? How was it validated? What are the operational implications?
Architecture Decision: Use a markdown template enforced at the repository level. Templates should require explicit sections for motivation, verification steps, and rollout strategy. This eliminates ambiguity and reduces review iteration cycles.
## Change Motivation
<!-- What problem does this solve? What user or system need triggered this work? -->
## Implementation Overview
<!-- High-level architectural approach. Avoid line-by-line code summaries. -->
## Verification Strategy
- [ ] Unit tests updated for modified logic
- [ ] Integration tests cover new data flows
- [ ] Manual smoke test performed on staging environment
- [ ] Performance impact measured (if applicable)
## Rollout & Risk Assessment
- [ ] Feature flag required? Yes/No
- [ ] Database migration needed? Yes/No
- [ ] Backward compatibility maintained? Yes/No
- [ ] Rollback procedure documented? Yes/No
## Related Tracking
Closes #[ISSUE_NUMBER]
Refs #[RELATED_ISSUE_NUMBER]
Rationale: Separating verification strategy from implementation overview forces authors to think about testability before merging. The risk assessment checklist surfaces operational concerns that developers often overlook until production incidents occur. Explicit issue linking enables automated traceability in project management tools.
Step 3: Align Merge Strategy with Repository Scale
Merge strategy dictates how history is preserved and how rollback operations function. The choice must align with branch lifecycle and team size.
Architecture Decision:
- Use Squash and Merge for short-lived feature branches where incremental commits represent debugging or formatting noise. This produces a linear, auditable main branch.
- Use Merge Commit (no-ff) for long-running feature branches or complex refactors where preserving commit granularity aids in bisecting regressions.
- Use Rebase and Merge sparingly, only when teams require linear history but must retain individual commit authorship for compliance or attribution requirements.
Rationale: Squashing eliminates history noise but destroys granular rollback points. Merge commits preserve context but can clutter git log with merge bubbles. Rebase rewrites history, which breaks collaborative branch sharing. The strategy must match the team's debugging workflow and CI/CD automation requirements.
Pitfall Guide
1. The "WIP" Trap
Explanation: Developers push work-in-progress commits with vague messages like temp or fixing stuff, assuming they will clean up later. These commits pollute history and confuse automated tooling.
Fix: Use git commit --amend for local iterations. Before pushing, run git rebase -i HEAD~N to squash or fixup intermediate commits. Never push unpolished history to shared branches.
2. Merge Strategy Misalignment
Explanation: Applying squash merge to a long-running feature branch that contains meaningful architectural iterations. When a regression occurs, developers lose the ability to bisect the exact commit that introduced the bug.
Fix: Match strategy to branch lifespan. Short features (<3 days) β squash. Long features (>5 days) or complex refactors β merge commit. Document the policy in CONTRIBUTING.md.
3. PR Description as Diff Summary
Explanation: Authors copy-paste code changes or list modified files instead of explaining intent. Reviewers waste time reconstructing the "why" from implementation details.
Fix: Enforce the "Problem β Solution β Impact" structure. The description should read like a technical memo, not a changelog. Code diffs belong in the diff viewer, not the PR body.
Explanation: Developers modify public APIs or database schemas without marking the commit as breaking. Downstream services or automated release pipelines fail unexpectedly.
Fix: Append BREAKING CHANGE: <description> to the commit footer. Tools like semantic-release parse this token to trigger major version bumps automatically.
5. Over-Scoping Pull Requests
Explanation: Bundling unrelated changes (e.g., UI tweak + dependency update + bug fix) into a single PR. This increases review complexity, raises conflict probability, and complicates rollback.
Fix: Enforce single-concern PRs. If a change touches multiple domains, split into separate branches. Use feature flags to decouple deployment from release.
6. Silent Test Modifications
Explanation: Updating test assertions without explaining why the expected behavior changed. Reviewers cannot verify if the test was fixed or if the underlying logic was incorrectly altered.
Fix: Always document test rationale in the commit body. Explain whether the test was catching a false positive, covering a newly discovered edge case, or aligning with updated business rules.
7. Unlinked Issue References
Explanation: Committing code that resolves a reported bug or implements a requested feature without referencing the tracking ticket. Project management tools lose traceability, and stakeholders cannot verify completion.
Fix: Use standard keywords (Closes #, Fixes #, Refs #) in the commit footer or PR description. Ensure your project management platform syncs with your Git provider for automatic status updates.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Small feature (<3 days, single developer) | Squash and Merge | Eliminates debugging noise; produces clean linear history for main branch | Low (minimal review overhead) |
| Complex refactor or long-running feature (>5 days) | Merge Commit (no-ff) | Preserves granular commit history for git bisect and regression tracing | Medium (slightly noisier log, higher traceability value) |
| Hotfix requiring immediate deployment | Squash and Merge | Fastest path to production; avoids merge conflicts with release branches | Low (optimized for speed and safety) |
| Multi-team collaborative branch | Merge Commit (no-ff) | Prevents history rewriting conflicts; maintains author attribution across teams | High (requires strict branch hygiene and frequent syncs) |
Configuration Template
// package.json
{
"scripts": {
"prepare": "husky install",
"commit": "cz"
},
"devDependencies": {
"@commitlint/cli": "^18.4.0",
"@commitlint/config-conventional": "^18.4.0",
"commitizen": "^4.3.0",
"cz-conventional-changelog": "^3.3.0",
"husky": "^9.0.0"
},
"config": {
"commitizen": {
"path": "./node_modules/cz-conventional-changelog"
}
}
}
# .husky/commit-msg
#!/usr/bin/env sh
. "$(dirname -- "$0")/_/husky.sh"
npx --no -- commitlint --edit ${1}
<!-- .github/PULL_REQUEST_TEMPLATE.md -->
## Context & Motivation
<!-- What business or technical problem does this address? -->
## Implementation Strategy
<!-- High-level approach, architectural decisions, and trade-offs considered -->
## Verification Steps
- [ ] Automated tests pass (unit + integration)
- [ ] Manual verification completed on staging
- [ ] Performance/Security implications reviewed
- [ ] Documentation updated (if applicable)
## Rollout Plan
- [ ] Feature flag configuration: `[FLAG_NAME]`
- [ ] Migration steps: `[NONE / DESCRIBE]`
- [ ] Rollback procedure: `[LINK / DESCRIBE]`
## Tracking
Closes #[ISSUE]
Quick Start Guide
- Initialize hooks: Run
npx husky init in your repository root. This creates the .husky directory and configures the prepare script.
- Add commitlint: Execute
npm install -D @commitlint/cli @commitlint/config-conventional. Create commitlint.config.ts using the template above.
- Attach the hook: Run
npx husky add .husky/commit-msg 'npx --no -- commitlint --edit ${1}'. This validates every commit message before it enters the history.
- Deploy the PR template: Place the markdown template in
.github/PULL_REQUEST_TEMPLATE.md. GitHub/GitLab will automatically populate new pull requests with the structure.
- Enforce via branch protection: Navigate to your repository settings β Branches β Add rule. Require pull request reviews, enable squash/merge or merge commit as your policy dictates, and block direct pushes to protected branches.
Implementing this workflow transforms version control from a passive storage mechanism into an active engineering system. Structured commits enable automation, explicit PR context accelerates reviews, and deliberate merge strategies preserve traceability. The initial setup requires minimal configuration, but the long-term compounding value in reduced incident resolution time, faster onboarding, and reliable release pipelines justifies the investment.