commit-sheriff 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,325 @@
1
+ # commit-sheriff
2
+
3
+ Shared [Husky](https://typicode.github.io/husky/) git hooks for any project — enforces a consistent **branch naming format** and **commit message format** (ticket + type, optionally module), and runs **lint-staged** / type-check / related tests before every commit.
4
+
5
+ One command installs the same hooks in any repo:
6
+
7
+ ```bash
8
+ npx commit-sheriff init
9
+ ```
10
+
11
+ ## Table of contents
12
+
13
+ - [Why](#why)
14
+ - [Requirements](#requirements)
15
+ - [Install](#install)
16
+ - [Usage](#usage)
17
+ - [What `init` does](#what-init-does)
18
+ - [Commit message format](#commit-message-format)
19
+ - [Branch name format](#branch-name-format)
20
+ - [Configuration (`commitGuard`)](#configuration-commitguard)
21
+ - [`useModules`](#usemodules)
22
+ - [`project`](#project)
23
+ - [`modules`](#modules)
24
+ - [`branchTypes`](#branchtypes)
25
+ - [`types`](#types)
26
+ - [Pre-commit checks](#pre-commit-checks)
27
+ - [lint-staged](#lint-staged)
28
+ - [Examples](#examples)
29
+ - [Updating](#updating)
30
+ - [Skipping / bypassing hooks](#skipping--bypassing-hooks)
31
+ - [Troubleshooting](#troubleshooting)
32
+ - [Releasing this package](#releasing-this-package)
33
+ - [License](#license)
34
+
35
+ ## Why
36
+
37
+ Different repos tend to drift into different commit-message and branch-naming conventions, which makes changelogs, ticket tracing, and code review harder. `commit-sheriff` gives every project the same two git hooks (`commit-msg`, `pre-commit`) driven by one small config block in `package.json`, so:
38
+
39
+ - Every commit message references a ticket and a change type.
40
+ - Every branch name reflects the same ticket and change type.
41
+ - Lint/format/type-check/tests run automatically before a commit is allowed, but only for the tools the project actually has installed.
42
+
43
+ ## Requirements
44
+
45
+ - Node.js (used to read `package.json` and evaluate the config — no runtime dependencies are installed by `commit-sheriff` itself)
46
+ - Git
47
+ - [`husky`](https://www.npmjs.com/package/husky) v9+ as a devDependency of your project
48
+
49
+ ## Install
50
+
51
+ ```bash
52
+ npm install --save-dev commit-sheriff husky
53
+ ```
54
+
55
+ `lint-staged`, `eslint`, and `prettier` are optional — install them too if you want the pre-commit hook to lint/format staged files:
56
+
57
+ ```bash
58
+ npm install --save-dev lint-staged eslint prettier
59
+ ```
60
+
61
+ ## Usage
62
+
63
+ ```bash
64
+ npx commit-sheriff init
65
+ ```
66
+
67
+ Run this once per repo, from the repo root (where `package.json` lives). This works once `commit-sheriff` is installed as a devDependency (it exposes a `commit-sheriff` binary via `node_modules/.bin`), or directly without installing first via `npx commit-sheriff init`.
68
+
69
+ ## What `init` does
70
+
71
+ 1. Initializes Husky if it isn't already set up (runs `npx husky init` when `.husky/_/husky.sh` is missing).
72
+ 2. Copies the hook scripts into `.husky/commit-msg` and `.husky/pre-commit` (overwriting any existing files with those exact names) and makes them executable.
73
+ 3. Adds a default `commitGuard` block to `package.json` **only if one doesn't already exist** — re-running `init` never overwrites your customized config.
74
+ 4. Adds a default `lint-staged` block to `package.json` **only if one doesn't already exist**.
75
+ 5. Adds a `"prepare": "husky"` npm script if missing, so hooks are (re)installed automatically after `npm install`.
76
+
77
+ Re-running `npx commit-sheriff init` later (e.g. after updating the package) is safe: it refreshes the two hook scripts to the latest version but leaves your `commitGuard` / `lint-staged` config alone.
78
+
79
+ ## Commit message format
80
+
81
+ ```
82
+ (TICKET) type(scope): message
83
+ ```
84
+
85
+ or, when [`useModules`](#usemodules) is `true`:
86
+
87
+ ```
88
+ (TICKET) [MODULE] type(scope): message
89
+ ```
90
+
91
+ - **`TICKET`** — `PROJECT-NUMBER`, e.g. `PROJ-1191` (uppercase letters, a dash, digits)
92
+ - **`MODULE`** — required only if `useModules: true` (see [`modules`](#modules))
93
+ - **`type`** — one of the words configured in [`types`](#types)
94
+ - **`(scope)`** — optional, free text in parentheses
95
+ - **`message`** — free text description
96
+
97
+ Merge/revert/fixup/squash commits (`Merge ...`, `Revert ...`, `fixup! ...`, `squash! ...`) are always allowed through unchanged — no need to reformat what Git itself generates.
98
+
99
+ ### Examples
100
+
101
+ ```
102
+ (PROJ-1077) feat: implement organization management
103
+ (PROJ-1191) fix(register): reset registration session on menu reopen
104
+ (PROJ-1077) [SET] feat: implement organization management # only with useModules: true
105
+ ```
106
+
107
+ If the commit message doesn't match, the commit is rejected and the hook prints the required format, an example, the ticket pattern, and the allowed types (and modules, if enabled).
108
+
109
+ When `useModules: true`, the hook also checks that the `[MODULE]` in the commit message matches the module encoded in the current branch name (see below) — so you can't accidentally tag a commit for a different module than the branch you're on.
110
+
111
+ ## Branch name format
112
+
113
+ ```
114
+ <type>/<PROJECT>-<NUMBER>-<description>
115
+ ```
116
+
117
+ or, when `useModules: true`:
118
+
119
+ ```
120
+ <type>/<MODULE>-<PROJECT>-<NUMBER>-<description>
121
+ ```
122
+
123
+ - **`type`** — one of the words configured in [`branchTypes`](#branchtypes)
124
+ - **`MODULE`** — required only if `useModules: true`
125
+ - **`PROJECT-NUMBER`** — same ticket reference as in commit messages
126
+ - **`description`** — lowercase, starts with a letter, only lowercase letters/digits/dashes after that
127
+
128
+ ### Examples
129
+
130
+ ```
131
+ bugfix/PROJ-1093-fix-validation
132
+ improvement/PROJ-609-correct-husky-pre-commit-validation
133
+ improvement/PRO-PROJ-609-correct-husky-pre-commit-validation # only with useModules: true
134
+ ```
135
+
136
+ The check is skipped on a detached `HEAD` (e.g. mid-rebase, mid-cherry-pick), so it never blocks those operations.
137
+
138
+ ## Configuration (`commitGuard`)
139
+
140
+ Add/edit this block in your project's `package.json` (the `init` command adds a default one for you):
141
+
142
+ ```json
143
+ {
144
+ "commitGuard": {
145
+ "useModules": false,
146
+ "project": "PROJ",
147
+ "branchTypes": [
148
+ "feature",
149
+ "bugfix",
150
+ "hotfix",
151
+ "improvement",
152
+ "refactor",
153
+ "release",
154
+ "chore",
155
+ "docs",
156
+ "test",
157
+ "spike"
158
+ ],
159
+ "types": [
160
+ "feat",
161
+ "feature",
162
+ "fix",
163
+ "docs",
164
+ "style",
165
+ "refactor",
166
+ "test",
167
+ "chore",
168
+ "perf",
169
+ "ci",
170
+ "build",
171
+ "revert"
172
+ ]
173
+ }
174
+ }
175
+ ```
176
+
177
+ All keys are optional; anything you omit falls back to the default shown above.
178
+
179
+ ### `useModules`
180
+
181
+ `boolean`, default `false`.
182
+
183
+ Turns the `[MODULE]` tag on or off in both the commit message and the branch name. Leave this `false` unless your project is split into named modules/domains that you want tracked per commit.
184
+
185
+ ### `project`
186
+
187
+ `string`, default `"PROJ"`.
188
+
189
+ The project key used in the **branch name** check (`<type>/<PROJECT>-<NUMBER>-...`). Note this only constrains the branch name — the commit message ticket itself accepts any uppercase project key (`^\([A-Z]+-[0-9]+\)`), so it doesn't need to be repeated here for the commit-msg hook to work, but keeping it consistent with your Jira/Linear/etc. project key is recommended.
190
+
191
+ ### `modules`
192
+
193
+ `string[]`, default: none — falls back to accepting **any** uppercase code (`[A-Z]+`).
194
+
195
+ Only relevant when `useModules: true`. If you want to restrict commits/branches to a specific, known set of module codes, list them explicitly:
196
+
197
+ ```json
198
+ "commitGuard": {
199
+ "useModules": true,
200
+ "project": "PROJ",
201
+ "modules": ["AUTH", "BILLING", "REPORTS"]
202
+ }
203
+ ```
204
+
205
+ If you omit `modules` (or leave it as `[]`), **any** uppercase code is accepted (e.g. `[AUTH]`, `[XYZ]`, `[SET]`) — useful early in a project before the final module list is settled.
206
+
207
+ ### `branchTypes`
208
+
209
+ `string[]`, default: `["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"]`.
210
+
211
+ Allowed prefixes for **branch names**. This is intentionally a coarser, workflow-level vocabulary (what kind of branch is this?) — it does not need to match `types` word-for-word, since a single `feature/...` branch will typically contain several kinds of commits (`feat`, `test`, `docs`, `chore`, ...) as the feature is built.
212
+
213
+ ### `types`
214
+
215
+ `string[]`, default: `["feat", "feature", "fix", "docs", "style", "refactor", "test", "chore", "perf", "ci", "build", "revert"]`.
216
+
217
+ Allowed `type` words in **commit messages**, following [Conventional Commits](https://www.conventionalcommits.org/) style.
218
+
219
+ ## Pre-commit checks
220
+
221
+ In addition to the branch name check, `pre-commit` conditionally runs (skipped when the project doesn't have the relevant tool):
222
+
223
+ - `npm run type-check` — if a `type-check` script exists in `package.json`
224
+ - `npx lint-staged` — if `lint-staged` is listed as a dependency
225
+ - `npx vitest related --run --passWithNoTests <staged .ts/.tsx files>` — if `vitest` is listed as a dependency and staged `.ts`/`.tsx` files exist (bounded to 60s via `timeout`/`gtimeout` when available)
226
+
227
+ Any failure here aborts the commit.
228
+
229
+ ## lint-staged
230
+
231
+ The default config added by `init` (only if you don't already have one):
232
+
233
+ ```json
234
+ {
235
+ "lint-staged": {
236
+ "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
237
+ "*.{json,md,css,scss,html}": ["prettier --write"]
238
+ }
239
+ }
240
+ ```
241
+
242
+ Adjust freely — `commit-sheriff` doesn't overwrite it once present.
243
+
244
+ ## Examples
245
+
246
+ **Minimal project (no modules):**
247
+
248
+ ```json
249
+ "commitGuard": {
250
+ "useModules": false,
251
+ "project": "PROJ",
252
+ "branchTypes": ["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"],
253
+ "types": ["feat", "fix", "docs", "chore", "test"]
254
+ }
255
+ ```
256
+
257
+ ```bash
258
+ git checkout -b bugfix/PROJ-1093-fix-validation
259
+ git commit -m "(PROJ-1093) fix: correct validation on empty input"
260
+ ```
261
+
262
+ **Project split into modules, with a locked list:**
263
+
264
+ ```json
265
+ "commitGuard": {
266
+ "useModules": true,
267
+ "project": "ACME",
268
+ "modules": ["AUTH", "BILLING", "REPORTS"],
269
+ "branchTypes": ["feature", "bugfix", "hotfix", "improvement", "refactor", "release", "chore", "docs", "test", "spike"],
270
+ "types": ["feat", "fix", "docs", "chore", "test"]
271
+ }
272
+ ```
273
+
274
+ ```bash
275
+ git checkout -b feature/AUTH-ACME-42-add-sso
276
+ git commit -m "(ACME-42) [AUTH] feat: add SSO login"
277
+ ```
278
+
279
+ ## Updating
280
+
281
+ When a new version of `commit-sheriff` is published:
282
+
283
+ ```bash
284
+ npm install commit-sheriff@latest --save-dev
285
+ npx commit-sheriff init
286
+ ```
287
+
288
+ `npm install` updates the package; `npx commit-sheriff init` re-copies the (possibly changed) `.husky/commit-msg` and `.husky/pre-commit` scripts. It will **not** touch your existing `commitGuard` or `lint-staged` config in `package.json`.
289
+
290
+ ## Skipping / bypassing hooks
291
+
292
+ Not recommended as a habit, but for emergencies Git supports:
293
+
294
+ ```bash
295
+ git commit --no-verify -m "..."
296
+ ```
297
+
298
+ Merge, revert, `fixup!`, and `squash!` commits are already exempted from the commit-message format check automatically.
299
+
300
+ ## Troubleshooting
301
+
302
+ **"Could not load commitGuard config — is Node.js installed and is this running from the repo root?"**
303
+ The hook runs `node -e "..."` against `./package.json`. Make sure Node.js is on your `PATH` and that you're committing from the repository root (or that your Git client runs hooks with the repo root as the working directory — some GUI clients get this wrong).
304
+
305
+ **A commit is accepted even though the message looks wrong**
306
+ Check which branch/tool you actually committed from — hooks only run for the local Git client that has Husky's `core.hooksPath` configured (`git config core.hooksPath` should print `.husky/_`). GUI clients or CI systems that bypass local hooks (or commit via the GitHub/GitLab API) will not trigger them.
307
+
308
+ **Branch name rejected right after `git checkout -b ...`**
309
+ Detached HEAD is exempt, but a normal new branch is checked immediately on first commit — rename it with `git branch -m <valid-name>` and try again.
310
+
311
+ **`useModules: true` but every module code is accepted**
312
+ That's expected if `commitGuard.modules` is empty/omitted — see [`modules`](#modules). Add an explicit list to restrict it.
313
+
314
+ ## Releasing this package
315
+
316
+ ```bash
317
+ npm version patch # or minor / major
318
+ npm publish
319
+ ```
320
+
321
+ (`npm version` runs on a properly named branch per this repo's own hooks, then fast-forward it into `main`.)
322
+
323
+ ## License
324
+
325
+ ISC
@@ -0,0 +1,119 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("fs");
4
+ const path = require("path");
5
+ const { execSync } = require("child_process");
6
+
7
+ const cwd = process.cwd();
8
+ const command = process.argv[2];
9
+
10
+ const DEFAULT_COMMIT_GUARD = {
11
+ useModules: false,
12
+ project: "PROJ",
13
+ branchTypes: [
14
+ "feature",
15
+ "bugfix",
16
+ "hotfix",
17
+ "improvement",
18
+ "refactor",
19
+ "release",
20
+ "chore",
21
+ "docs",
22
+ "test",
23
+ "spike",
24
+ ],
25
+ types: [
26
+ "feat",
27
+ "feature",
28
+ "fix",
29
+ "docs",
30
+ "style",
31
+ "refactor",
32
+ "test",
33
+ "chore",
34
+ "perf",
35
+ "ci",
36
+ "build",
37
+ "revert",
38
+ ],
39
+ };
40
+
41
+ const DEFAULT_LINT_STAGED = {
42
+ "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"],
43
+ "*.{json,md,css,scss,html}": ["prettier --write"],
44
+ };
45
+
46
+ function readPackageJson() {
47
+ const pkgPath = path.join(cwd, "package.json");
48
+ if (!fs.existsSync(pkgPath)) {
49
+ console.error(" package.json tapılmadı. Əvvəlcə `npm init` işlədin.");
50
+ process.exit(1);
51
+ }
52
+ return { pkgPath, pkg: JSON.parse(fs.readFileSync(pkgPath, "utf8")) };
53
+ }
54
+
55
+ function ensureHusky() {
56
+ const huskyDir = path.join(cwd, ".husky");
57
+ const shimPath = path.join(huskyDir, "_", "husky.sh");
58
+ if (!fs.existsSync(shimPath)) {
59
+ console.log("→ husky quraşdırılır (npx husky init)...");
60
+ execSync("npx husky init", { cwd, stdio: "inherit" });
61
+ }
62
+ }
63
+
64
+ function copyHook(name) {
65
+ const src = path.join(__dirname, "..", "templates", name);
66
+ const dest = path.join(cwd, ".husky", name);
67
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
68
+ fs.copyFileSync(src, dest);
69
+ fs.chmodSync(dest, 0o755);
70
+ console.log(`✔ .husky/${name} yazıldı`);
71
+ }
72
+
73
+ function mergeConfig() {
74
+ const { pkgPath, pkg } = readPackageJson();
75
+ let changed = false;
76
+
77
+ if (!pkg.commitGuard) {
78
+ pkg.commitGuard = DEFAULT_COMMIT_GUARD;
79
+ changed = true;
80
+ console.log("✔ package.json → commitGuard konfiqurasiyası əlavə olundu");
81
+ } else {
82
+ console.log("• package.json-da commitGuard artıq var, toxunulmadı");
83
+ }
84
+
85
+ if (!pkg["lint-staged"]) {
86
+ pkg["lint-staged"] = DEFAULT_LINT_STAGED;
87
+ changed = true;
88
+ console.log("✔ package.json → lint-staged konfiqurasiyası əlavə olundu");
89
+ } else {
90
+ console.log("• package.json-da lint-staged artıq var, toxunulmadı");
91
+ }
92
+
93
+ if (!pkg.scripts) pkg.scripts = {};
94
+ if (!pkg.scripts.prepare) {
95
+ pkg.scripts.prepare = "husky";
96
+ changed = true;
97
+ }
98
+
99
+ if (changed) {
100
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + "\n");
101
+ }
102
+ }
103
+
104
+ function init() {
105
+ ensureHusky();
106
+ copyHook("commit-msg");
107
+ copyHook("pre-commit");
108
+ mergeConfig();
109
+ console.log(
110
+ "\nHazırdır. package.json → commitGuard bölməsində 'project' və lazım olsa 'modules' dəyərlərini tənzimləyin.\n",
111
+ );
112
+ }
113
+
114
+ if (command === "init") {
115
+ init();
116
+ } else {
117
+ console.log("İstifadə: npx commit-sheriff init");
118
+ process.exit(command ? 1 : 0);
119
+ }
package/package.json ADDED
@@ -0,0 +1,79 @@
1
+ {
2
+ "name": "commit-sheriff",
3
+ "version": "1.0.0",
4
+ "description": "Shared husky commit-msg / pre-commit hooks enforcing ticket-based commit messages and branch naming",
5
+ "main": "index.js",
6
+ "bin": {
7
+ "commit-sheriff": "bin/commit-sheriff.js"
8
+ },
9
+ "files": [
10
+ "bin",
11
+ "templates"
12
+ ],
13
+ "scripts": {
14
+ "test": "echo \"Error: no test specified\" && exit 1",
15
+ "prepare": "husky"
16
+ },
17
+ "keywords": [
18
+ "husky",
19
+ "git-hooks",
20
+ "commit-msg",
21
+ "pre-commit",
22
+ "commitlint",
23
+ "conventional-commits",
24
+ "lint-staged",
25
+ "branch-naming",
26
+ "ticket"
27
+ ],
28
+ "author": "",
29
+ "license": "ISC",
30
+ "type": "commonjs",
31
+ "devDependencies": {
32
+ "@eslint/js": "^10.0.1",
33
+ "eslint": "^10.8.0",
34
+ "eslint-config-prettier": "^10.1.8",
35
+ "globals": "^17.8.0",
36
+ "husky": "^9.1.7",
37
+ "lint-staged": "^17.2.0",
38
+ "prettier": "^3.9.6"
39
+ },
40
+ "lint-staged": {
41
+ "*.{js,jsx,ts,tsx}": [
42
+ "eslint --fix",
43
+ "prettier --write"
44
+ ],
45
+ "*.{json,md,css,scss,html}": [
46
+ "prettier --write"
47
+ ]
48
+ },
49
+ "commitGuard": {
50
+ "useModules": false,
51
+ "project": "PROJ",
52
+ "branchTypes": [
53
+ "feature",
54
+ "bugfix",
55
+ "hotfix",
56
+ "improvement",
57
+ "refactor",
58
+ "release",
59
+ "chore",
60
+ "docs",
61
+ "test",
62
+ "spike"
63
+ ],
64
+ "types": [
65
+ "feat",
66
+ "feature",
67
+ "fix",
68
+ "docs",
69
+ "style",
70
+ "refactor",
71
+ "test",
72
+ "chore",
73
+ "perf",
74
+ "ci",
75
+ "build",
76
+ "revert"
77
+ ]
78
+ }
79
+ }
@@ -0,0 +1,80 @@
1
+ commit_msg_file="$1"
2
+ commit_msg=$(cat "$commit_msg_file")
3
+
4
+ # Skip validation for commits git/tools generate themselves
5
+ case "$commit_msg" in
6
+ "Merge "*|"Revert "*|"fixup! "*|"squash! "*)
7
+ exit 0
8
+ ;;
9
+ esac
10
+
11
+ # ── Load config from package.json → "commitGuard" (falls back to sane defaults) ──
12
+ # commitGuard: { "useModules": true, "modules": [...], "types": [...] }
13
+ eval "$(node -e "
14
+ const fs = require('fs');
15
+ let cfg = {};
16
+ try { cfg = (JSON.parse(fs.readFileSync('package.json', 'utf8')).commitGuard) || {}; } catch (e) {}
17
+ const useModules = cfg.useModules !== undefined ? cfg.useModules : true;
18
+ const modules = (cfg.modules && cfg.modules.length ? cfg.modules.join('|') : '[A-Z]+');
19
+ const types = (cfg.types || ['feat','feature','fix','docs','style','refactor','test','chore','perf','ci','build','revert']).join('|');
20
+ console.log('USE_MODULES=' + useModules);
21
+ console.log('MODULES=\"' + modules + '\"');
22
+ console.log('TYPES=\"' + types + '\"');
23
+ " 2>/dev/null)"
24
+
25
+ if [ -z "$TYPES" ]; then
26
+ echo ""
27
+ echo " Could not load commitGuard config — is Node.js installed and is this running from the repo root?"
28
+ echo ""
29
+ exit 1
30
+ fi
31
+
32
+ # ── Commit message format ───────────────────────────────────────────────────
33
+ if [ "$USE_MODULES" = "true" ]; then
34
+ # Format : (TICKET) [MODULE] type(scope): message
35
+ # Example: (PROJ-1077) [SET] feature(organization structure): implement organization management
36
+ valid_commit_regex="^\([A-Z]+-[0-9]+\) \[($MODULES)\] ($TYPES)(\([^)]+\))?: .+"
37
+ else
38
+ # Format : (TICKET) type(scope): message
39
+ # Example: (PROJ-1077) feature(organization structure): implement organization management
40
+ valid_commit_regex="^\([A-Z]+-[0-9]+\) ($TYPES)(\([^)]+\))?: .+"
41
+ fi
42
+
43
+ if [[ ! $commit_msg =~ $valid_commit_regex ]]; then
44
+ echo ""
45
+ echo " Invalid commit message: '$commit_msg'"
46
+ echo ""
47
+ if [ "$USE_MODULES" = "true" ]; then
48
+ echo " Required format : (TICKET) [MODULE] type(scope): message"
49
+ echo " Example : (PROJ-1077) [SET] feature(organization structure): implement organization management"
50
+ echo " Example : (PROJ-1191) [SET] fix(register): reset registration session on menu reopen"
51
+ else
52
+ echo " Required format : (TICKET) type(scope): message"
53
+ echo " Example : (PROJ-1077) feature(organization structure): implement organization management"
54
+ echo " Example : (PROJ-1191) fix(register): reset registration session on menu reopen"
55
+ fi
56
+ echo ""
57
+ echo " Ticket : (PROJECT-NUMBER), e.g. (PROJ-1191)"
58
+ [ "$USE_MODULES" = "true" ] && echo " Modules : $(echo $MODULES | tr '|' ' ')"
59
+ echo " Types : $(echo $TYPES | tr '|' ' ')"
60
+ echo " Scope : optional — e.g. fix(register): ..."
61
+ echo ""
62
+ exit 1
63
+ fi
64
+
65
+ if [ "$USE_MODULES" = "true" ]; then
66
+ # ── Keep [MODULE] consistent with the branch ────────────────────────────
67
+ # e.g. bugfix/SET-PROJ-1127-... -> commit must be tagged [SET]
68
+ local_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
69
+ branch_module=$(printf '%s' "$local_branch" | sed -nE "s#^[a-z]+/($MODULES)-.*#\1#p")
70
+ commit_module=$(printf '%s' "$commit_msg" | sed -nE 's/^\([A-Z]+-[0-9]+\) \[([A-Z]+)\].*/\1/p')
71
+
72
+ if [ -n "$branch_module" ] && [ "$branch_module" != "$commit_module" ]; then
73
+ echo ""
74
+ echo " Commit module [$commit_module] does not match the branch module [$branch_module]."
75
+ echo " Branch : $local_branch"
76
+ echo " Use [$branch_module] to match the branch, e.g. (PROJ-1191) [$branch_module] fix(scope): ..."
77
+ echo ""
78
+ exit 1
79
+ fi
80
+ fi
@@ -0,0 +1,90 @@
1
+ local_branch="$(git rev-parse --abbrev-ref HEAD 2>/dev/null)" || true
2
+
3
+ # ── Load config from package.json → "commitGuard" (falls back to sane defaults) ──
4
+ # commitGuard: { "useModules": true, "project": "PROJ", "modules": [...], "branchTypes": [...] }
5
+ eval "$(node -e "
6
+ const fs = require('fs');
7
+ let cfg = {};
8
+ try { cfg = (JSON.parse(fs.readFileSync('package.json', 'utf8')).commitGuard) || {}; } catch (e) {}
9
+ const useModules = cfg.useModules !== undefined ? cfg.useModules : true;
10
+ const project = cfg.project || 'PROJ';
11
+ const modules = (cfg.modules && cfg.modules.length ? cfg.modules.join('|') : '[A-Z]+');
12
+ const branchTypes = (cfg.branchTypes || ['feature','bugfix','hotfix','improvement','refactor','release','chore','docs','test','spike']).join('|');
13
+ console.log('USE_MODULES=' + useModules);
14
+ console.log('PROJECT=' + project);
15
+ console.log('MODULES=\"' + modules + '\"');
16
+ console.log('BRANCH_TYPES=\"' + branchTypes + '\"');
17
+ " 2>/dev/null)"
18
+
19
+ # ── Branch format (skipped on detached HEAD, e.g. mid-rebase) ──────────────
20
+ if [ -z "$local_branch" ] || [ "$local_branch" = "HEAD" ]; then
21
+ echo " Detached HEAD — skipping branch name check."
22
+ elif [ -z "$PROJECT" ]; then
23
+ echo ""
24
+ echo " Could not load commitGuard config — is Node.js installed and is this running from the repo root?"
25
+ echo ""
26
+ exit 1
27
+ else
28
+ if [ "$USE_MODULES" = "true" ]; then
29
+ # Format : <type>/<MODULE>-<PROJECT>-<NUMBER>-<description>
30
+ # Example: improvement/PRO-PROJ-609-correct-husky-pre-commit-validation
31
+ valid_branch_regex="^($BRANCH_TYPES)\/($MODULES)-$PROJECT-[0-9]+-[a-z][a-z0-9-]*$"
32
+ else
33
+ # Format : <type>/<PROJECT>-<NUMBER>-<description>
34
+ # Example: bugfix/PROJ-1093-fix-validation
35
+ valid_branch_regex="^($BRANCH_TYPES)\/$PROJECT-[0-9]+-[a-z][a-z0-9-]*$"
36
+ fi
37
+
38
+ if [[ ! $local_branch =~ $valid_branch_regex ]]; then
39
+ echo ""
40
+ echo " Invalid branch name: '$local_branch'"
41
+ echo ""
42
+ if [ "$USE_MODULES" = "true" ]; then
43
+ echo " Required format : <type>/<MODULE>-<PROJECT>-<NUMBER>-<description>"
44
+ echo " Example : improvement/PRO-PROJ-609-correct-husky-pre-commit-validation"
45
+ else
46
+ echo " Required format : <type>/<PROJECT>-<NUMBER>-<description>"
47
+ echo " Example : bugfix/PROJ-1093-fix-meeting-validation"
48
+ fi
49
+ echo ""
50
+ echo " Types : $(echo $BRANCH_TYPES | tr '|' ' ')"
51
+ [ "$USE_MODULES" = "true" ] && echo " Modules : $(echo $MODULES | tr '|' ' ')"
52
+ echo " Project : $PROJECT"
53
+ echo ""
54
+ exit 1
55
+ fi
56
+ fi
57
+
58
+ # ── Lint + type-check (only runs if the project actually has them) ─────────
59
+ HAS_TYPE_CHECK=$(node -e "process.stdout.write(((require('./package.json').scripts)||{})['type-check'] ? '1' : '0')" 2>/dev/null)
60
+ if [ "$HAS_TYPE_CHECK" = "1" ]; then
61
+ npm run type-check || exit 1
62
+ fi
63
+
64
+ HAS_LINT_STAGED=$(node -e "
65
+ const p = require('./package.json');
66
+ const deps = Object.assign({}, p.dependencies, p.devDependencies);
67
+ process.stdout.write(deps['lint-staged'] ? '1' : '0');
68
+ " 2>/dev/null)
69
+ if [ "$HAS_LINT_STAGED" = "1" ]; then
70
+ npx lint-staged || exit 1
71
+ fi
72
+
73
+ # ── Related tests (only if vitest is installed) ─────────────────────────────
74
+ HAS_VITEST=$(node -e "
75
+ const p = require('./package.json');
76
+ const deps = Object.assign({}, p.dependencies, p.devDependencies);
77
+ process.stdout.write(deps['vitest'] ? '1' : '0');
78
+ " 2>/dev/null)
79
+ if [ "$HAS_VITEST" = "1" ]; then
80
+ STAGED_TS=$(git diff --cached --name-only --diff-filter=ACM | grep -E '\.(ts|tsx)$' || true)
81
+ if [ -n "$STAGED_TS" ]; then
82
+ if command -v timeout > /dev/null 2>&1; then
83
+ timeout 60 npx vitest related --run --passWithNoTests $STAGED_TS
84
+ elif command -v gtimeout > /dev/null 2>&1; then
85
+ gtimeout 60 npx vitest related --run --passWithNoTests $STAGED_TS
86
+ else
87
+ npx vitest related --run --passWithNoTests $STAGED_TS
88
+ fi
89
+ fi
90
+ fi