@biffo/cli 0.228.5 → 0.228.7
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.
|
@@ -4,13 +4,114 @@
|
|
|
4
4
|
root=$(git rev-parse --show-toplevel) || exit 0
|
|
5
5
|
cd "$root" || exit 0
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# The authoritative type list is commitlint.config.js's `type-enum` (AGENTS.md
|
|
8
|
+
# §3). Mirrored here for the fallback below, and `cli/src/lib/commit-msg-fallback.test.ts`
|
|
9
|
+
# asserts the two never drift apart -- a second copy of a list is a second copy
|
|
10
|
+
# of a decision, and this one is load-bearing: ADR-0006 derives the release bump
|
|
11
|
+
# from the subject.
|
|
12
|
+
BIFFO_COMMIT_TYPES='feat fix chore docs test infra security refactor perf ci'
|
|
13
|
+
|
|
14
|
+
# Subjects commitlint ignores by default (`defaultIgnores`): git's own generated
|
|
15
|
+
# merge/revert subjects and the markers rebase writes. The fallback must ignore
|
|
16
|
+
# them too, or it blocks ordinary git operations that commitlint would allow.
|
|
17
|
+
_is_ignored_subject() {
|
|
18
|
+
case "$1" in
|
|
19
|
+
"Merge "* | "Revert "* | "Reverts "* | "fixup! "* | "squash! "* | "amend! "*) return 0 ;;
|
|
20
|
+
*) return 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_fallback_explain() {
|
|
25
|
+
echo "commit-msg: $2" >&2
|
|
26
|
+
echo " subject: $1" >&2
|
|
27
|
+
echo " expected: type(scope): summary e.g. feat(api): add run history endpoint" >&2
|
|
28
|
+
echo " types: $BIFFO_COMMIT_TYPES" >&2
|
|
29
|
+
echo " (checked without commitlint -- this repo has no runnable config; see local-gates.md)" >&2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# A dependency-free Conventional Commits check, for repos where commitlint
|
|
33
|
+
# cannot run.
|
|
34
|
+
#
|
|
35
|
+
# ## Why this exists
|
|
36
|
+
#
|
|
37
|
+
# This hook used to print
|
|
38
|
+
#
|
|
39
|
+
# commit-msg: no commitlint config -- subject not checked
|
|
40
|
+
#
|
|
41
|
+
# and exit 0. That reads like information; it is a gate abstaining. It abstained
|
|
42
|
+
# in 13 of 17 estate repos (#1193): it looked for a root commitlint config, and
|
|
43
|
+
# ran it with `pnpm exec`, which needs a root package.json. Most satellites have
|
|
44
|
+
# neither -- tabsii-crm's frontend is its own pnpm workspace under
|
|
45
|
+
# apps/frontend/, so the repo root holds no package.json at all. AGENTS.md §3
|
|
46
|
+
# was binding everywhere and enforced in three places.
|
|
47
|
+
#
|
|
48
|
+
# Shipping a config would not have fixed it: a config with no resolvable
|
|
49
|
+
# commitlint binary still cannot run. Resolving the toolchain from whichever
|
|
50
|
+
# workspace happens to hold it is fragile across six different layouts. A check
|
|
51
|
+
# needing nothing but `sh` works in all of them and cannot abstain.
|
|
52
|
+
#
|
|
53
|
+
# ## What it deliberately does NOT check
|
|
54
|
+
#
|
|
55
|
+
# Only the load-bearing rules: the `type(scope): subject` shape, the type list,
|
|
56
|
+
# and the 100-character header limit. `subject-case` and the
|
|
57
|
+
# `body-no-ci-skip-token` plugin stay commitlint's job where commitlint runs. A
|
|
58
|
+
# fallback STRICTER than the tool it stands in for would reject commits CI
|
|
59
|
+
# accepts, and a local gate that is wrong gets bypassed on reflex.
|
|
60
|
+
_fallback_check_subject() {
|
|
61
|
+
_subject=$(sed -n '1p' "$1")
|
|
62
|
+
|
|
63
|
+
# Empty or comment-only: git aborts the commit itself, so there is nothing to
|
|
64
|
+
# judge and refusing here would just produce a confusing second error.
|
|
65
|
+
case "$_subject" in '' | '#'*) return 0 ;; esac
|
|
66
|
+
|
|
67
|
+
_is_ignored_subject "$_subject" && return 0
|
|
68
|
+
|
|
69
|
+
# `${#var}` counts characters in POSIX sh. Matches header-max-length.
|
|
70
|
+
if [ "${#_subject}" -gt 100 ]; then
|
|
71
|
+
_fallback_explain "$_subject" "subject is ${#_subject} characters, limit is 100"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
case "$_subject" in
|
|
76
|
+
*': '*) ;;
|
|
77
|
+
*)
|
|
78
|
+
_fallback_explain "$_subject" 'no ": " separator'
|
|
79
|
+
return 1
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
_head=${_subject%%': '*} # everything before the first ": "
|
|
84
|
+
_summary=${_subject#*': '}
|
|
85
|
+
|
|
86
|
+
if [ -z "$_summary" ]; then
|
|
87
|
+
_fallback_explain "$_subject" 'the summary after ": " is empty'
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Strip an optional "(scope)" and an optional breaking-change "!", in either
|
|
92
|
+
# order: both `feat!: x` and `feat(api)!: x` are valid Conventional Commits.
|
|
93
|
+
_type=${_head%%'('*} # drop "(scope)" when present
|
|
94
|
+
_type=${_type%'!'} # drop a trailing "!"
|
|
95
|
+
|
|
96
|
+
for _known in $BIFFO_COMMIT_TYPES; do
|
|
97
|
+
if [ "$_type" = "$_known" ]; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
_fallback_explain "$_subject" "unknown type \"$_type\""
|
|
103
|
+
return 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# commitlint where it is configured AND runnable; the built-in check everywhere
|
|
107
|
+
# else. Both conditions matter: a config with no root package.json cannot be run
|
|
108
|
+
# by `pnpm exec` from here, and taking that branch anyway is what produced the
|
|
109
|
+
# silent abstention this replaces.
|
|
110
|
+
if [ -f package.json ] &&
|
|
111
|
+
{ [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; }; then
|
|
11
112
|
pnpm exec commitlint --edit "$1" || exit 1
|
|
12
113
|
else
|
|
13
|
-
|
|
114
|
+
_fallback_check_subject "$1" || exit 1
|
|
14
115
|
fi
|
|
15
116
|
|
|
16
117
|
# Refuse commits that edit template-owned paths (#370). Inert in the template
|
|
@@ -9,13 +9,31 @@
|
|
|
9
9
|
root=$(git rev-parse --show-toplevel) || exit 0
|
|
10
10
|
cd "$root" || exit 0
|
|
11
11
|
|
|
12
|
+
# The two abstention messages below are deliberately phrased as abstentions
|
|
13
|
+
# rather than as status (#1193). The previous wording -- "nothing staged-file-
|
|
14
|
+
# level to run", "skipping" -- read like the hook working, which is why 13 of 17
|
|
15
|
+
# repos ran a formatting gate that formats nothing without anyone noticing.
|
|
16
|
+
#
|
|
17
|
+
# Unlike commit-msg, which now has a built-in fallback because Conventional
|
|
18
|
+
# Commits is a RULE that can be checked with nothing but `sh`, this hook runs a
|
|
19
|
+
# TOOLCHAIN. Checked 2026-08-03: no satellite carries `lint-staged` anywhere --
|
|
20
|
+
# not at the root, not in any workspace -- so there is genuinely nothing to
|
|
21
|
+
# resolve and nothing to fall back to. Making it fatal would block all work in
|
|
22
|
+
# those repos, which is how a safety measure gets ripped out.
|
|
23
|
+
#
|
|
24
|
+
# So it still exits 0. It just no longer implies it checked anything, and it
|
|
25
|
+
# says what would make it real.
|
|
12
26
|
if [ ! -f package.json ]; then
|
|
13
|
-
echo "pre-commit: no package.json
|
|
27
|
+
echo "pre-commit: ABSTAINED — no root package.json, so lint-staged cannot run here." >&2
|
|
28
|
+
echo " Staged files were NOT linted or formatted. CI still checks them." >&2
|
|
29
|
+
echo " To make this real: add lint-staged to a root package.json (see local-gates.md)." >&2
|
|
14
30
|
exit 0
|
|
15
31
|
fi
|
|
16
32
|
if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
|
|
17
|
-
|
|
18
|
-
echo "pre-commit: no lint-staged config
|
|
33
|
+
&& [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
|
|
34
|
+
echo "pre-commit: ABSTAINED — package.json exists but declares no lint-staged config." >&2
|
|
35
|
+
echo " Staged files were NOT linted or formatted. CI still checks them." >&2
|
|
36
|
+
echo " To make this real: add a lint-staged config (see local-gates.md)." >&2
|
|
19
37
|
exit 0
|
|
20
38
|
fi
|
|
21
39
|
exec pnpm exec lint-staged
|
|
@@ -4,13 +4,114 @@
|
|
|
4
4
|
root=$(git rev-parse --show-toplevel) || exit 0
|
|
5
5
|
cd "$root" || exit 0
|
|
6
6
|
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
|
|
7
|
+
# The authoritative type list is commitlint.config.js's `type-enum` (AGENTS.md
|
|
8
|
+
# §3). Mirrored here for the fallback below, and `cli/src/lib/commit-msg-fallback.test.ts`
|
|
9
|
+
# asserts the two never drift apart -- a second copy of a list is a second copy
|
|
10
|
+
# of a decision, and this one is load-bearing: ADR-0006 derives the release bump
|
|
11
|
+
# from the subject.
|
|
12
|
+
BIFFO_COMMIT_TYPES='feat fix chore docs test infra security refactor perf ci'
|
|
13
|
+
|
|
14
|
+
# Subjects commitlint ignores by default (`defaultIgnores`): git's own generated
|
|
15
|
+
# merge/revert subjects and the markers rebase writes. The fallback must ignore
|
|
16
|
+
# them too, or it blocks ordinary git operations that commitlint would allow.
|
|
17
|
+
_is_ignored_subject() {
|
|
18
|
+
case "$1" in
|
|
19
|
+
"Merge "* | "Revert "* | "Reverts "* | "fixup! "* | "squash! "* | "amend! "*) return 0 ;;
|
|
20
|
+
*) return 1 ;;
|
|
21
|
+
esac
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
_fallback_explain() {
|
|
25
|
+
echo "commit-msg: $2" >&2
|
|
26
|
+
echo " subject: $1" >&2
|
|
27
|
+
echo " expected: type(scope): summary e.g. feat(api): add run history endpoint" >&2
|
|
28
|
+
echo " types: $BIFFO_COMMIT_TYPES" >&2
|
|
29
|
+
echo " (checked without commitlint -- this repo has no runnable config; see local-gates.md)" >&2
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
# A dependency-free Conventional Commits check, for repos where commitlint
|
|
33
|
+
# cannot run.
|
|
34
|
+
#
|
|
35
|
+
# ## Why this exists
|
|
36
|
+
#
|
|
37
|
+
# This hook used to print
|
|
38
|
+
#
|
|
39
|
+
# commit-msg: no commitlint config -- subject not checked
|
|
40
|
+
#
|
|
41
|
+
# and exit 0. That reads like information; it is a gate abstaining. It abstained
|
|
42
|
+
# in 13 of 17 estate repos (#1193): it looked for a root commitlint config, and
|
|
43
|
+
# ran it with `pnpm exec`, which needs a root package.json. Most satellites have
|
|
44
|
+
# neither -- tabsii-crm's frontend is its own pnpm workspace under
|
|
45
|
+
# apps/frontend/, so the repo root holds no package.json at all. AGENTS.md §3
|
|
46
|
+
# was binding everywhere and enforced in three places.
|
|
47
|
+
#
|
|
48
|
+
# Shipping a config would not have fixed it: a config with no resolvable
|
|
49
|
+
# commitlint binary still cannot run. Resolving the toolchain from whichever
|
|
50
|
+
# workspace happens to hold it is fragile across six different layouts. A check
|
|
51
|
+
# needing nothing but `sh` works in all of them and cannot abstain.
|
|
52
|
+
#
|
|
53
|
+
# ## What it deliberately does NOT check
|
|
54
|
+
#
|
|
55
|
+
# Only the load-bearing rules: the `type(scope): subject` shape, the type list,
|
|
56
|
+
# and the 100-character header limit. `subject-case` and the
|
|
57
|
+
# `body-no-ci-skip-token` plugin stay commitlint's job where commitlint runs. A
|
|
58
|
+
# fallback STRICTER than the tool it stands in for would reject commits CI
|
|
59
|
+
# accepts, and a local gate that is wrong gets bypassed on reflex.
|
|
60
|
+
_fallback_check_subject() {
|
|
61
|
+
_subject=$(sed -n '1p' "$1")
|
|
62
|
+
|
|
63
|
+
# Empty or comment-only: git aborts the commit itself, so there is nothing to
|
|
64
|
+
# judge and refusing here would just produce a confusing second error.
|
|
65
|
+
case "$_subject" in '' | '#'*) return 0 ;; esac
|
|
66
|
+
|
|
67
|
+
_is_ignored_subject "$_subject" && return 0
|
|
68
|
+
|
|
69
|
+
# `${#var}` counts characters in POSIX sh. Matches header-max-length.
|
|
70
|
+
if [ "${#_subject}" -gt 100 ]; then
|
|
71
|
+
_fallback_explain "$_subject" "subject is ${#_subject} characters, limit is 100"
|
|
72
|
+
return 1
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
case "$_subject" in
|
|
76
|
+
*': '*) ;;
|
|
77
|
+
*)
|
|
78
|
+
_fallback_explain "$_subject" 'no ": " separator'
|
|
79
|
+
return 1
|
|
80
|
+
;;
|
|
81
|
+
esac
|
|
82
|
+
|
|
83
|
+
_head=${_subject%%': '*} # everything before the first ": "
|
|
84
|
+
_summary=${_subject#*': '}
|
|
85
|
+
|
|
86
|
+
if [ -z "$_summary" ]; then
|
|
87
|
+
_fallback_explain "$_subject" 'the summary after ": " is empty'
|
|
88
|
+
return 1
|
|
89
|
+
fi
|
|
90
|
+
|
|
91
|
+
# Strip an optional "(scope)" and an optional breaking-change "!", in either
|
|
92
|
+
# order: both `feat!: x` and `feat(api)!: x` are valid Conventional Commits.
|
|
93
|
+
_type=${_head%%'('*} # drop "(scope)" when present
|
|
94
|
+
_type=${_type%'!'} # drop a trailing "!"
|
|
95
|
+
|
|
96
|
+
for _known in $BIFFO_COMMIT_TYPES; do
|
|
97
|
+
if [ "$_type" = "$_known" ]; then
|
|
98
|
+
return 0
|
|
99
|
+
fi
|
|
100
|
+
done
|
|
101
|
+
|
|
102
|
+
_fallback_explain "$_subject" "unknown type \"$_type\""
|
|
103
|
+
return 1
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
# commitlint where it is configured AND runnable; the built-in check everywhere
|
|
107
|
+
# else. Both conditions matter: a config with no root package.json cannot be run
|
|
108
|
+
# by `pnpm exec` from here, and taking that branch anyway is what produced the
|
|
109
|
+
# silent abstention this replaces.
|
|
110
|
+
if [ -f package.json ] &&
|
|
111
|
+
{ [ -f commitlint.config.js ] || [ -f commitlint.config.mjs ] || [ -f .commitlintrc.json ]; }; then
|
|
11
112
|
pnpm exec commitlint --edit "$1" || exit 1
|
|
12
113
|
else
|
|
13
|
-
|
|
114
|
+
_fallback_check_subject "$1" || exit 1
|
|
14
115
|
fi
|
|
15
116
|
|
|
16
117
|
# Refuse commits that edit template-owned paths (#370). Inert in the template
|
|
@@ -9,13 +9,31 @@
|
|
|
9
9
|
root=$(git rev-parse --show-toplevel) || exit 0
|
|
10
10
|
cd "$root" || exit 0
|
|
11
11
|
|
|
12
|
+
# The two abstention messages below are deliberately phrased as abstentions
|
|
13
|
+
# rather than as status (#1193). The previous wording -- "nothing staged-file-
|
|
14
|
+
# level to run", "skipping" -- read like the hook working, which is why 13 of 17
|
|
15
|
+
# repos ran a formatting gate that formats nothing without anyone noticing.
|
|
16
|
+
#
|
|
17
|
+
# Unlike commit-msg, which now has a built-in fallback because Conventional
|
|
18
|
+
# Commits is a RULE that can be checked with nothing but `sh`, this hook runs a
|
|
19
|
+
# TOOLCHAIN. Checked 2026-08-03: no satellite carries `lint-staged` anywhere --
|
|
20
|
+
# not at the root, not in any workspace -- so there is genuinely nothing to
|
|
21
|
+
# resolve and nothing to fall back to. Making it fatal would block all work in
|
|
22
|
+
# those repos, which is how a safety measure gets ripped out.
|
|
23
|
+
#
|
|
24
|
+
# So it still exits 0. It just no longer implies it checked anything, and it
|
|
25
|
+
# says what would make it real.
|
|
12
26
|
if [ ! -f package.json ]; then
|
|
13
|
-
echo "pre-commit: no package.json
|
|
27
|
+
echo "pre-commit: ABSTAINED — no root package.json, so lint-staged cannot run here." >&2
|
|
28
|
+
echo " Staged files were NOT linted or formatted. CI still checks them." >&2
|
|
29
|
+
echo " To make this real: add lint-staged to a root package.json (see local-gates.md)." >&2
|
|
14
30
|
exit 0
|
|
15
31
|
fi
|
|
16
32
|
if ! node -e "const p=require('./package.json');process.exit(p['lint-staged']||p.config?.['lint-staged']?0:1)" 2>/dev/null \
|
|
17
|
-
|
|
18
|
-
echo "pre-commit: no lint-staged config
|
|
33
|
+
&& [ ! -f .lintstagedrc ] && [ ! -f .lintstagedrc.json ] && [ ! -f lint-staged.config.js ]; then
|
|
34
|
+
echo "pre-commit: ABSTAINED — package.json exists but declares no lint-staged config." >&2
|
|
35
|
+
echo " Staged files were NOT linted or formatted. CI still checks them." >&2
|
|
36
|
+
echo " To make this real: add a lint-staged config (see local-gates.md)." >&2
|
|
19
37
|
exit 0
|
|
20
38
|
fi
|
|
21
39
|
exec pnpm exec lint-staged
|