no-yolo-commits 1.2.0 → 1.3.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 +7 -2
- package/bin/cli.js +28 -6
- package/package.json +3 -3
- package/templates/pre-commit.sh +2 -2
- package/templates/pre-push.sh +35 -0
package/README.md
CHANGED
|
@@ -17,6 +17,10 @@ One command. On every commit from now on:
|
|
|
17
17
|
2. **An AI actually reads your diff** (via the [`claude`](https://claude.com/claude-code) CLI) and blocks the commit — but only for real, high-confidence problems. Not vibes, not "you could refactor this." If the reviewer is missing, slow, or having a bad day, it fails **open** — a flaky reviewer should never be the reason your commit is stuck. Skipped automatically when everything staged is a lockfile or generated asset, so bumping `package-lock.json` doesn't cost you a 2-minute wait.
|
|
18
18
|
3. **`main`/`master` become look-but-don't-touch.** Commit straight there and instead of yelling at you, it just makes you a branch — `ACME-1788716508-fix-the-thing-you-were-actually-fixing`, ready to go, commit already on it. It even wrote the branch name from your diff.
|
|
19
19
|
|
|
20
|
+
And on every `git push`:
|
|
21
|
+
|
|
22
|
+
4. **Pushing to `main`/`master` is refused, full stop** — independent of #3 above. The auto-branch step only stops a *direct commit* from landing on a protected branch; nothing stops you from merging that branch back locally and pushing anyway a minute later, which lands you in exactly the state #3 was trying to prevent. This hook checks the actual destination of the push itself — merged, rebased, cherry-picked, doesn't matter, only where it's headed — so protection doesn't quietly evaporate the moment you (or an agent working on your behalf) does the merge-and-push in one motion without a real pause in between.
|
|
23
|
+
|
|
20
24
|
No dashboard. No config file to argue with. No runtime dependencies — it's shell scripts wearing a [husky](https://typicode.github.io/husky/) trenchcoat.
|
|
21
25
|
|
|
22
26
|
## Install
|
|
@@ -29,7 +33,7 @@ That's the whole install, per project. It will, in order:
|
|
|
29
33
|
|
|
30
34
|
- add `husky` as a devDependency (if you don't have it)
|
|
31
35
|
- set `"scripts.prepare": "husky"` in `package.json`
|
|
32
|
-
- drop `.husky/pre-commit` into your repo
|
|
36
|
+
- drop `.husky/pre-commit` and `.husky/pre-push` into your repo
|
|
33
37
|
|
|
34
38
|
### Or install it once, everywhere
|
|
35
39
|
|
|
@@ -89,7 +93,8 @@ Same secret scan, same AI review, run against the PR's diff against its base bra
|
|
|
89
93
|
This is a guardrail, not a cage. Bad day, emergency hotfix, you know exactly what you're doing:
|
|
90
94
|
|
|
91
95
|
```bash
|
|
92
|
-
git commit --no-verify
|
|
96
|
+
git commit --no-verify # skip the secret scan / AI review / auto-branch for one commit
|
|
97
|
+
git push --no-verify # skip the protected-branch push check for one push
|
|
93
98
|
```
|
|
94
99
|
|
|
95
100
|
No questions asked. No shame either — that's what it's there for.
|
package/bin/cli.js
CHANGED
|
@@ -39,7 +39,7 @@ function parseArgs(argv) {
|
|
|
39
39
|
}
|
|
40
40
|
|
|
41
41
|
function printHelp() {
|
|
42
|
-
console.log(`no-yolo-commits — an AI pre-commit reviewer + "no direct commits to main" guard
|
|
42
|
+
console.log(`no-yolo-commits — an AI pre-commit reviewer + "no direct commits or pushes to main" guard
|
|
43
43
|
|
|
44
44
|
Usage:
|
|
45
45
|
npx no-yolo-commits init [options] per-project install (via husky)
|
|
@@ -57,7 +57,7 @@ Options:
|
|
|
57
57
|
reviewer, e.g. --stack="Next.js + TypeScript + Postgres"
|
|
58
58
|
(default: "TypeScript")
|
|
59
59
|
--protect=a,b,c Comma-separated branch names that block direct commits
|
|
60
|
-
(default: "main,master")
|
|
60
|
+
*and* pushes (default: "main,master")
|
|
61
61
|
--model=NAME Pass --model NAME through to every \`claude\` invocation
|
|
62
62
|
(cost/speed control — e.g. --model=haiku)
|
|
63
63
|
--global, -g Install into git's global hook template
|
|
@@ -108,6 +108,11 @@ function buildHookScript(args, defaultPrefixSource) {
|
|
|
108
108
|
return { hookScript, prefix, protectedBranches };
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
+
function buildPrePushScript(protectedBranches) {
|
|
112
|
+
const templatePath = path.join(__dirname, '..', 'templates', 'pre-push.sh');
|
|
113
|
+
return fs.readFileSync(templatePath, 'utf8').replace(/__PROTECTED__/g, protectedBranches);
|
|
114
|
+
}
|
|
115
|
+
|
|
111
116
|
function run(cmd, cmdArgs) {
|
|
112
117
|
console.log(` $ ${cmd} ${cmdArgs.join(' ')}`);
|
|
113
118
|
const result = spawnSync(cmd, cmdArgs, { cwd: CWD, stdio: 'inherit' });
|
|
@@ -128,9 +133,11 @@ function initProject(args) {
|
|
|
128
133
|
|
|
129
134
|
const huskyDir = path.join(CWD, '.husky');
|
|
130
135
|
const hookPath = path.join(huskyDir, 'pre-commit');
|
|
136
|
+
const pushHookPath = path.join(huskyDir, 'pre-push');
|
|
131
137
|
|
|
132
|
-
if (
|
|
133
|
-
fail(`${hookPath} already exists — pass --force to overwrite it.`);
|
|
138
|
+
if (!args.force) {
|
|
139
|
+
if (fs.existsSync(hookPath)) fail(`${hookPath} already exists — pass --force to overwrite it.`);
|
|
140
|
+
if (fs.existsSync(pushHookPath)) fail(`${pushHookPath} already exists — pass --force to overwrite it.`);
|
|
134
141
|
}
|
|
135
142
|
|
|
136
143
|
const hasHusky = (pkg.devDependencies && pkg.devDependencies.husky) || (pkg.dependencies && pkg.dependencies.husky);
|
|
@@ -153,12 +160,16 @@ function initProject(args) {
|
|
|
153
160
|
run('npx', ['husky']);
|
|
154
161
|
|
|
155
162
|
const { hookScript, prefix, protectedBranches } = buildHookScript(args, freshPkg.name);
|
|
163
|
+
const pushHookScript = buildPrePushScript(protectedBranches);
|
|
156
164
|
|
|
157
165
|
fs.mkdirSync(huskyDir, { recursive: true });
|
|
158
166
|
fs.writeFileSync(hookPath, hookScript);
|
|
159
167
|
fs.chmodSync(hookPath, 0o755);
|
|
168
|
+
fs.writeFileSync(pushHookPath, pushHookScript);
|
|
169
|
+
fs.chmodSync(pushHookPath, 0o755);
|
|
160
170
|
|
|
161
171
|
console.log(`✓ wrote ${path.relative(CWD, hookPath)}`);
|
|
172
|
+
console.log(`✓ wrote ${path.relative(CWD, pushHookPath)}`);
|
|
162
173
|
printSummary(prefix, protectedBranches);
|
|
163
174
|
}
|
|
164
175
|
|
|
@@ -171,17 +182,23 @@ function initGlobal(args) {
|
|
|
171
182
|
|
|
172
183
|
const hooksDir = path.join(templateDir, 'hooks');
|
|
173
184
|
const hookPath = path.join(hooksDir, 'pre-commit');
|
|
185
|
+
const pushHookPath = path.join(hooksDir, 'pre-push');
|
|
174
186
|
|
|
175
|
-
if (
|
|
176
|
-
fail(`${hookPath} already exists — pass --force to overwrite it.`);
|
|
187
|
+
if (!args.force) {
|
|
188
|
+
if (fs.existsSync(hookPath)) fail(`${hookPath} already exists — pass --force to overwrite it.`);
|
|
189
|
+
if (fs.existsSync(pushHookPath)) fail(`${pushHookPath} already exists — pass --force to overwrite it.`);
|
|
177
190
|
}
|
|
178
191
|
|
|
179
192
|
const { hookScript, prefix, protectedBranches } = buildHookScript(args, null);
|
|
193
|
+
const pushHookScript = buildPrePushScript(protectedBranches);
|
|
180
194
|
|
|
181
195
|
fs.mkdirSync(hooksDir, { recursive: true });
|
|
182
196
|
fs.writeFileSync(hookPath, hookScript);
|
|
183
197
|
fs.chmodSync(hookPath, 0o755);
|
|
198
|
+
fs.writeFileSync(pushHookPath, pushHookScript);
|
|
199
|
+
fs.chmodSync(pushHookPath, 0o755);
|
|
184
200
|
console.log(`✓ wrote ${hookPath}`);
|
|
201
|
+
console.log(`✓ wrote ${pushHookPath}`);
|
|
185
202
|
|
|
186
203
|
run('git', ['config', '--global', 'init.templateDir', templateDir]);
|
|
187
204
|
|
|
@@ -201,7 +218,12 @@ function printSummary(prefix, protectedBranches) {
|
|
|
201
218
|
console.log(' - AI review of staged changes (needs `claude` on PATH) — blocks only on high-confidence issues');
|
|
202
219
|
console.log(` - direct commits to [${protectedBranches}] auto-branch instead, as ${prefix}-<timestamp>-<slug>`);
|
|
203
220
|
console.log('');
|
|
221
|
+
console.log('And on `git push`:');
|
|
222
|
+
console.log(` - pushing to [${protectedBranches}] is refused outright — merged, rebased, whatever got`);
|
|
223
|
+
console.log(' a commit there locally doesn\'t matter, only where the push is headed');
|
|
224
|
+
console.log('');
|
|
204
225
|
console.log('Bypass for one commit: git commit --no-verify');
|
|
226
|
+
console.log('Bypass for one push: git push --no-verify');
|
|
205
227
|
}
|
|
206
228
|
|
|
207
229
|
function init(args) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "no-yolo-commits",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "AI pre-commit review (via the claude CLI) + a guard that auto-branches you off main/master instead of letting a direct commit land there.",
|
|
3
|
+
"version": "1.3.0",
|
|
4
|
+
"description": "AI pre-commit review (via the claude CLI) + a guard that auto-branches you off main/master instead of letting a direct commit land there, and a pre-push hook that refuses any push to main/master outright regardless of how it got there locally.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"husky",
|
|
7
7
|
"git-hooks",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
"url": "https://github.com/ataztech910/no-yolo-commits/issues"
|
|
22
22
|
},
|
|
23
23
|
"bin": {
|
|
24
|
-
"no-yolo-commits": "
|
|
24
|
+
"no-yolo-commits": "bin/cli.js"
|
|
25
25
|
},
|
|
26
26
|
"files": [
|
|
27
27
|
"bin",
|
package/templates/pre-commit.sh
CHANGED
|
@@ -11,10 +11,10 @@ if ! git diff --cached --quiet 2>/dev/null; then
|
|
|
11
11
|
-e '-----BEGIN (RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----' \
|
|
12
12
|
-e 'AKIA[0-9A-Z]{16}' \
|
|
13
13
|
-e 'xox[baprs]-[0-9A-Za-z-]{10,}' \
|
|
14
|
-
-e '(api[_-]?key|secret|
|
|
14
|
+
-e '(api[_-]?key|secret|token|password|passwd)["'"'"']?[[:space:]]*[:=][[:space:]]*["'"'"'][A-Za-z0-9_/+=:-]{16,}["'"'"']' \
|
|
15
15
|
2>/dev/null || true)"
|
|
16
16
|
|
|
17
|
-
env_files="$(git diff --cached --name-only -- . 2>/dev/null | grep -E '(^|/)\.env(\.[^.]+)?$' | grep -v -E '\.(example|sample|template|dist)$' || true)"
|
|
17
|
+
env_files="$(git diff --cached --name-only -- . 2>/dev/null | grep -E '(^|/)\.env(\.[^.]+)?$|(^|/)[A-Za-z0-9_-]+\.env(\.[^.]+)?$' | grep -v -E '\.(example|sample|template|dist)$' || true)"
|
|
18
18
|
|
|
19
19
|
if [ -n "$secret_hits" ] || [ -n "$env_files" ]; then
|
|
20
20
|
echo ""
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
#!/usr/bin/env sh
|
|
2
|
+
|
|
3
|
+
# --- Refuse pushes that land on a protected branch ---
|
|
4
|
+
# git feeds this hook one line per ref being pushed on stdin:
|
|
5
|
+
# <local ref> <local sha1> <remote ref> <remote sha1>
|
|
6
|
+
# The auto-branch step in pre-commit only pauses a direct commit to main/master — it says nothing
|
|
7
|
+
# about what happens next. Fast-forward merge that branch locally and push, and the commit lands
|
|
8
|
+
# on the protected branch exactly as if pre-commit had never stepped in. This hook closes that gap
|
|
9
|
+
# by checking the actual destination of the push itself, independent of how the commit got there
|
|
10
|
+
# locally (direct commit that somehow slipped through, merge, rebase, whatever) — merged or not
|
|
11
|
+
# doesn't matter, only where it's headed.
|
|
12
|
+
#
|
|
13
|
+
# Deliberately blocks unconditionally rather than trying to special-case "but this one was already
|
|
14
|
+
# reviewed" — that judgment call is exactly what a human decides by reaching for --no-verify, not
|
|
15
|
+
# something a hook can infer from the ref list.
|
|
16
|
+
while read -r local_ref local_sha remote_ref remote_sha; do
|
|
17
|
+
[ -z "$remote_ref" ] && continue
|
|
18
|
+
remote_branch="${remote_ref#refs/heads/}"
|
|
19
|
+
case " __PROTECTED__ " in
|
|
20
|
+
*" $remote_branch "*)
|
|
21
|
+
echo ""
|
|
22
|
+
echo "⛔ Push to '$remote_branch' blocked."
|
|
23
|
+
echo " This repo doesn't push straight to a protected branch as a side effect of"
|
|
24
|
+
echo " merging or committing locally — that has to be a deliberate, visible step."
|
|
25
|
+
echo ""
|
|
26
|
+
echo " Push your branch instead (git push origin \$(git branch --show-current)) and"
|
|
27
|
+
echo " open a PR, or if you really do mean to push here directly:"
|
|
28
|
+
echo " git push --no-verify"
|
|
29
|
+
echo ""
|
|
30
|
+
exit 1
|
|
31
|
+
;;
|
|
32
|
+
esac
|
|
33
|
+
done
|
|
34
|
+
|
|
35
|
+
exit 0
|