@regardio/dev 2.6.1 → 2.7.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.
@@ -31,11 +31,15 @@ function runShipHotfix(subcommand, subArgs, cwd = process.cwd()) {
31
31
  console.log("\nFetching latest state from origin...");
32
32
  git("fetch", "origin");
33
33
  if (!branchExists("production")) {
34
- console.error("Branch \"production\" does not exist. Create it first:\n git checkout -b production && git push -u origin production");
35
- process.exit(1);
34
+ console.log("Creating production branch...");
35
+ git("checkout", "-b", "production");
36
+ git("push", "-u", "origin", "production");
37
+ git("checkout", "main");
36
38
  }
37
39
  git("checkout", "production");
38
- git("pull", "--ff-only", "origin", "production");
40
+ try {
41
+ git("pull", "--ff-only", "origin", "production");
42
+ } catch {}
39
43
  git("checkout", "-b", hotfixBranch);
40
44
  console.log(`\n✅ Hotfix branch "${hotfixBranch}" created from production.`);
41
45
  console.log("Apply your fix using conventional commits, then run:");
@@ -80,12 +84,22 @@ function runShipHotfix(subcommand, subArgs, cwd = process.cwd()) {
80
84
  } else console.log("No packages configured for versioning — skipping.");
81
85
  console.log("\nMerging hotfix into production...");
82
86
  git("checkout", "production");
83
- git("pull", "--ff-only", "origin", "production");
87
+ try {
88
+ git("pull", "--ff-only", "origin", "production");
89
+ } catch {}
84
90
  git("merge", "--no-ff", currentBranch, "-m", `chore(hotfix): merge ${currentBranch} into production`);
85
91
  git("push", "origin", "production");
86
92
  console.log("\nPropagating hotfix to staging...");
93
+ if (!branchExists("staging")) {
94
+ console.log("Creating staging branch...");
95
+ git("checkout", "-b", "staging");
96
+ git("push", "-u", "origin", "staging");
97
+ git("checkout", "production");
98
+ }
87
99
  git("checkout", "staging");
88
- git("pull", "--ff-only", "origin", "staging");
100
+ try {
101
+ git("pull", "--ff-only", "origin", "staging");
102
+ } catch {}
89
103
  git("merge", "--no-ff", "production", "-m", "chore(hotfix): merge production into staging");
90
104
  git("push", "origin", "staging");
91
105
  console.log("\nPropagating hotfix to main...");
@@ -27,15 +27,24 @@ function runShipProduction(cwd = process.cwd()) {
27
27
  console.log("\nFetching latest state from origin...");
28
28
  git("fetch", "origin");
29
29
  if (!branchExists("staging")) {
30
- console.error("Branch \"staging\" does not exist. Create it first:\n git checkout -b staging && git push -u origin staging");
31
- process.exit(1);
30
+ console.log("Creating staging branch...");
31
+ git("checkout", "-b", "staging");
32
+ git("push", "-u", "origin", "staging");
33
+ git("checkout", "main");
32
34
  }
33
35
  if (!branchExists("production")) {
34
- console.error("Branch \"production\" does not exist. Create it first:\n git checkout -b production && git push -u origin production");
35
- process.exit(1);
36
+ console.log("Creating production branch...");
37
+ git("checkout", "-b", "production");
38
+ git("push", "-u", "origin", "production");
39
+ git("checkout", "main");
36
40
  }
37
41
  git("pull", "--ff-only", "origin", "main");
38
- const ahead = gitRead("log", "origin/production..HEAD", "--oneline");
42
+ let ahead;
43
+ try {
44
+ ahead = gitRead("log", "origin/production..HEAD", "--oneline");
45
+ } catch {
46
+ ahead = gitRead("log", "--oneline");
47
+ }
39
48
  if (!ahead) {
40
49
  console.error("main is already in sync with production. Nothing to ship.");
41
50
  process.exit(1);
@@ -72,7 +81,9 @@ function runShipProduction(cwd = process.cwd()) {
72
81
  } else if (!confirm("\nNo packages configured for versioning — ship commits to production?\n")) process.exit(0);
73
82
  console.log("\nMerging main into production...");
74
83
  git("checkout", "production");
75
- git("pull", "--ff-only", "origin", "production");
84
+ try {
85
+ git("pull", "--ff-only", "origin", "production");
86
+ } catch {}
76
87
  git("merge", "--ff-only", "main");
77
88
  git("push", "origin", "production");
78
89
  console.log("\nSyncing staging with production...");
@@ -42,8 +42,10 @@ function runShipStaging(cwd = process.cwd()) {
42
42
  process.exit(1);
43
43
  }
44
44
  if (!branchExists("staging")) {
45
- console.error("Branch \"staging\" does not exist locally or on origin. Create it first:\n git checkout -b staging && git push -u origin staging");
46
- process.exit(1);
45
+ console.log("Creating staging branch...");
46
+ git("checkout", "-b", "staging");
47
+ git("push", "-u", "origin", "staging");
48
+ git("checkout", "main");
47
49
  }
48
50
  console.log("\nRunning quality checks...");
49
51
  try {
@@ -97,27 +97,45 @@ Schema tests run against a real Postgres so policies and triggers behave as they
97
97
 
98
98
  ## Coverage
99
99
 
100
- Library packages hold a floor around 80% for statements, branches, functions, and lines. The floor is a minimum, not a target — the goal is meaningful tests, not arithmetic.
100
+ Library packages hold a floor of 80% for statements, branches, functions, and lines. The floor is a minimum, not a target — the goal is meaningful tests, not arithmetic.
101
101
 
102
- Enforced at:
102
+ ## Branch quality gates
103
103
 
104
- - Local development `pnpm test`
105
- - Release preparation — `ship-staging`
106
- - CI GitHub Actions
104
+ Different branches carry different promises, and the gates match those promises.
105
+
106
+ | Branch | Gate | What runs |
107
+ |--------|------|-----------|
108
+ | `main` | commit | lint, typecheck |
109
+ | `staging` | CI | lint, typecheck, test, coverage |
110
+ | `production` | CI + ship | lint, typecheck, test, coverage |
111
+ | `feature/*` | none | — |
112
+ | `hotfix/*` | none | — |
113
+
114
+ **`main` is the development branch.** Commits to `main` must be syntactically sound — lint and typecheck pass — but tests and coverage are not required to succeed. Work in progress lives here.
115
+
116
+ **`staging` and `production` promise correctness.** The full QA chain runs before anything lands there. The ship commands enforce this before merging, and CI enforces it again on push.
117
+
118
+ **Feature and hotfix branches are free.** Commit at will. The gate is applied when the branch merges into its target — feature → main applies main's gate, hotfix → production applies production's gate.
119
+
120
+ Coverage is enforced at:
121
+
122
+ - `pnpm report` — fails locally if coverage drops below the floor (run before shipping)
123
+ - `ship:staging` and `ship:production` — run the full quality suite before merging
124
+ - CI on `staging` and `production` — re-runs the suite on push
107
125
 
108
126
  ## Quality gates
109
127
 
110
- Before a package publishes:
128
+ Before a package ships to `staging` or `production`:
111
129
 
112
130
  1. Build succeeds
113
131
  2. Type check passes
114
- 3. Coverage meets the floor
115
- 4. Tests pass no skipped or failing
132
+ 3. Tests pass no skipped or failing
133
+ 4. Coverage meets the floor
116
134
 
117
135
  ## Continuous integration
118
136
 
119
- - Pre-commit hooks run linting (Biome)
120
- - CI runs build, typecheck, and the test suite with coverage
137
+ - Pre-commit hooks run lint (Biome, markdownlint) and typecheck on staged files
138
+ - CI on `staging` and `production` runs the full suite including coverage
121
139
  - Release workflow blocks publishing on any failure
122
140
 
123
141
  ## Test maintenance
@@ -27,6 +27,24 @@ Husky configures itself through the `prepare` script:
27
27
 
28
28
  This runs after `pnpm install` and sets up the `.husky` directory.
29
29
 
30
+ Copy the hook templates from `@regardio/dev`:
31
+
32
+ ```bash
33
+ mkdir -p .husky
34
+ cp node_modules/@regardio/dev/templates/husky/pre-commit .husky/pre-commit
35
+ cp node_modules/@regardio/dev/templates/husky/commit-msg .husky/commit-msg
36
+ cp node_modules/@regardio/dev/templates/husky/pre-push .husky/pre-push
37
+ chmod +x .husky/pre-commit .husky/commit-msg .husky/pre-push
38
+ ```
39
+
40
+ ## What the hooks enforce
41
+
42
+ The pre-commit hook enforces **syntax correctness**, not behavioural correctness. The distinction is intentional and matches the branch model:
43
+
44
+ - **`main`** is the development branch. Commits to `main` must pass lint and typecheck, but tests and coverage are not required. Work in progress belongs here.
45
+ - **`staging` and `production`** promise correctness. The full QA chain (typecheck, test, coverage) runs on every push to those branches via the `pre-push` hook, and again inside the `ship:*` commands as a second gate.
46
+ - **`feature/*` and `hotfix/*`** branches carry no gate. Commit freely — the gate applies when merging into the target branch.
47
+
30
48
  ## Hooks
31
49
 
32
50
  ### `commit-msg`
@@ -35,25 +53,83 @@ Validates commit messages against the conventional-commit format:
35
53
 
36
54
  ```bash
37
55
  #!/bin/sh
38
- . "$(dirname "$0")/_/husky.sh"
39
- commitlint --edit $1
56
+ pnpm exec commitlint --edit "$1"
40
57
  ```
41
58
 
42
- ### `pre-commit` (optional)
59
+ ### `pre-push`
43
60
 
44
- Runs linting before a commit lands:
61
+ Branch-aware: runs typecheck on every branch except `feature/*` and `hotfix/*`; runs the full QA chain before any push to `staging` or `production`.
45
62
 
46
63
  ```bash
47
64
  #!/bin/sh
48
- pnpm lint
65
+ set -eu
66
+
67
+ BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || echo 'HEAD')"
68
+
69
+ # Feature and hotfix branches carry no push gate.
70
+ case "$BRANCH" in
71
+ feature/*|hotfix/*) exit 0 ;;
72
+ esac
73
+
74
+ pnpm run --if-present typecheck
75
+
76
+ # Staging and production must pass the full QA chain.
77
+ case "$BRANCH" in
78
+ staging|production)
79
+ pnpm run --if-present test
80
+ pnpm run --if-present report
81
+ ;;
82
+ esac
49
83
  ```
50
84
 
85
+ ### `pre-commit`
86
+
87
+ Branch-aware: lints staged files and runs typecheck; full QA on `staging`/`production` commits.
88
+
89
+ ```bash
90
+ #!/bin/sh
91
+ set -eu
92
+
93
+ BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || echo 'HEAD')"
94
+
95
+ # Feature and hotfix branches carry no commit gate.
96
+ case "$BRANCH" in
97
+ feature/*|hotfix/*) exit 0 ;;
98
+ esac
99
+
100
+ STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACMR)"
101
+ [ -z "$STAGED_FILES" ] && exit 0
102
+
103
+ STAGED_BIOME_FILES="$(echo "$STAGED_FILES" | grep -E '\.(cjs|cts|js|jsx|mjs|mts|ts|tsx|json|jsonc)$' || true)"
104
+ STAGED_MD_FILES="$(echo "$STAGED_FILES" | grep -E '\.(md|mdoc|mdx)$' | grep -v 'CHANGELOG\.md' || true)"
105
+
106
+ if [ -n "$STAGED_BIOME_FILES" ]; then
107
+ echo "$STAGED_BIOME_FILES" | xargs pnpm exec biome check --no-errors-on-unmatched
108
+ fi
109
+ if [ -n "$STAGED_MD_FILES" ]; then
110
+ echo "$STAGED_MD_FILES" | xargs pnpm exec markdownlint-cli2 --config .markdownlint-cli2.jsonc
111
+ fi
112
+
113
+ pnpm run --if-present typecheck
114
+
115
+ # Staging and production must pass the full QA chain.
116
+ case "$BRANCH" in
117
+ staging|production)
118
+ pnpm run --if-present test
119
+ pnpm run --if-present report
120
+ ;;
121
+ esac
122
+ ```
123
+
124
+ `--if-present` means repos that don't define a given script skip that step silently.
125
+
51
126
  ## Bypassing hooks
52
127
 
53
- In the rare case where a hook truly has to be skipped:
128
+ In the rare case where a hook truly has to be skipped, `--no-verify` works for both commit and push:
54
129
 
55
130
  ```bash
56
131
  git commit --no-verify -m "emergency fix"
132
+ git push --no-verify origin main
57
133
  ```
58
134
 
59
135
  Use sparingly. A bypass that becomes habit stops being a bypass.
@@ -82,4 +158,6 @@ chmod +x .husky/*
82
158
 
83
159
  - [Commitlint](./commitlint.md) — commit-message validation
84
160
  - [Biome](./biome.md) — linting and formatting
161
+ - [Release Workflow](./releases.md) — how branches, ship commands, and CI enforce quality gates
162
+ - [Testing](../standards/testing.md) — branch gate model explained
85
163
  - [Husky Documentation](https://typicode.github.io/husky/)
@@ -143,7 +143,28 @@ pnpm ship:hotfix finish
143
143
 
144
144
  ## Adoption
145
145
 
146
- ### 1. Add ship scripts to `package.json`
146
+ ### 1. Set up Husky hooks
147
+
148
+ ```bash
149
+ mkdir -p .husky
150
+ cp node_modules/@regardio/dev/templates/husky/pre-commit .husky/pre-commit
151
+ cp node_modules/@regardio/dev/templates/husky/commit-msg .husky/commit-msg
152
+ chmod +x .husky/pre-commit .husky/commit-msg
153
+ ```
154
+
155
+ Add the `prepare` script so Husky installs itself after `pnpm install`:
156
+
157
+ ```json
158
+ {
159
+ "scripts": {
160
+ "prepare": "husky"
161
+ }
162
+ }
163
+ ```
164
+
165
+ The `pre-commit` template is branch-aware: feature and hotfix branches commit freely, `main` enforces lint and typecheck, and `staging`/`production` enforce the full QA chain. See [Husky](./husky.md) for details.
166
+
167
+ ### 2. Add ship scripts to `package.json`
147
168
 
148
169
  ```json
149
170
  {
@@ -176,7 +197,9 @@ Every `.versionrc.json` must include `"gitRawCommitsOpts": { "path": "." }` to f
176
197
 
177
198
  For a single-package repo, place `.versionrc.json` at the root instead.
178
199
 
179
- ### 3. Create the branches
200
+ ### 3. Create the branches (optional)
201
+
202
+ The ship scripts auto-create `staging` and `production` branches if they don't exist. You only need to create them manually if you prefer to set them up ahead of time:
180
203
 
181
204
  ```bash
182
205
  git checkout -b staging && git push -u origin staging
@@ -208,12 +231,22 @@ CI publishes subsequent versions; the very first one requires a local publish be
208
231
 
209
232
  ## Quality Gates
210
233
 
211
- Every ship command enforces the same gates before any commit or merge:
234
+ Gates differ by branch. The ship commands enforce the appropriate gate before any merge.
235
+
236
+ | Context | What runs |
237
+ |---------|-----------|
238
+ | Commit to `main` (pre-commit hook) | lint, typecheck |
239
+ | Commit to `feature/*` or `hotfix/*` | nothing |
240
+ | `ship:staging` / `ship:production` / `ship:hotfix finish` | build, typecheck, test, coverage |
241
+ | CI on `staging` / `production` | build, typecheck, test, coverage |
242
+
243
+ Ship commands run:
212
244
 
213
245
  ```bash
214
246
  pnpm build # Must succeed
215
247
  pnpm typecheck # Must succeed
216
248
  pnpm test # Must succeed
249
+ pnpm report # Coverage must meet the floor
217
250
  ```
218
251
 
219
252
  ## Publishing vs Non-Publishing Repos
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://www.schemastore.org/package.json",
3
3
  "name": "@regardio/dev",
4
- "version": "2.6.1",
4
+ "version": "2.7.0",
5
5
  "private": false,
6
6
  "description": "Regardio development presets: biome, typescript, commitlint, markdownlint, vitest, playwright, sqlfluff, husky, and GitLab-flow ship tooling",
7
7
  "keywords": [
@@ -74,10 +74,10 @@
74
74
  ],
75
75
  "devDependencies": {
76
76
  "@total-typescript/ts-reset": "0.6.1",
77
- "@types/node": "25.6.2",
78
- "@vitest/coverage-v8": "4.1.5",
77
+ "@types/node": "25.7.0",
78
+ "@vitest/coverage-v8": "4.1.6",
79
79
  "tsdown": "0.22.0",
80
- "vitest": "4.1.5"
80
+ "vitest": "4.1.6"
81
81
  },
82
82
  "peerDependencies": {
83
83
  "@biomejs/biome": ">=2",
@@ -0,0 +1,2 @@
1
+ #!/bin/sh
2
+ pnpm exec commitlint --edit "$1"
@@ -0,0 +1,32 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || echo 'HEAD')"
5
+
6
+ # Feature and hotfix branches carry no commit gate.
7
+ case "$BRANCH" in
8
+ feature/*|hotfix/*) exit 0 ;;
9
+ esac
10
+
11
+ STAGED_FILES="$(git diff --cached --name-only --diff-filter=ACMR)"
12
+ [ -z "$STAGED_FILES" ] && exit 0
13
+
14
+ STAGED_BIOME_FILES="$(echo "$STAGED_FILES" | grep -E '\.(cjs|cts|js|jsx|mjs|mts|ts|tsx|json|jsonc)$' || true)"
15
+ STAGED_MD_FILES="$(echo "$STAGED_FILES" | grep -E '\.(md|mdoc|mdx)$' | grep -v 'CHANGELOG\.md' || true)"
16
+
17
+ if [ -n "$STAGED_BIOME_FILES" ]; then
18
+ echo "$STAGED_BIOME_FILES" | xargs pnpm exec biome check --no-errors-on-unmatched
19
+ fi
20
+ if [ -n "$STAGED_MD_FILES" ]; then
21
+ echo "$STAGED_MD_FILES" | xargs pnpm exec markdownlint-cli2 --config .markdownlint-cli2.jsonc
22
+ fi
23
+
24
+ pnpm run --if-present typecheck
25
+
26
+ # Staging and production must pass the full QA chain.
27
+ case "$BRANCH" in
28
+ staging|production)
29
+ pnpm run --if-present test
30
+ pnpm run --if-present report
31
+ ;;
32
+ esac
@@ -0,0 +1,19 @@
1
+ #!/bin/sh
2
+ set -eu
3
+
4
+ BRANCH="$(git symbolic-ref --short HEAD 2>/dev/null || echo 'HEAD')"
5
+
6
+ # Feature and hotfix branches carry no push gate.
7
+ case "$BRANCH" in
8
+ feature/*|hotfix/*) exit 0 ;;
9
+ esac
10
+
11
+ pnpm run --if-present typecheck
12
+
13
+ # Staging and production must pass the full QA chain.
14
+ case "$BRANCH" in
15
+ staging|production)
16
+ pnpm run --if-present test
17
+ pnpm run --if-present report
18
+ ;;
19
+ esac