yadflow 3.12.1 → 3.12.2
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/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
## [3.12.2](https://github.com/abdelrahmannasr/yadflow/compare/v3.12.1...v3.12.2) (2026-07-14)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* **checks:** waive verified-commits signature for content-free merge commits ([1e73837](https://github.com/abdelrahmannasr/yadflow/commit/1e738372f821e93020069b565d40315cd7be2591)), closes [#138](https://github.com/abdelrahmannasr/yadflow/issues/138)
|
|
7
|
+
|
|
1
8
|
## [3.12.1](https://github.com/abdelrahmannasr/yadflow/compare/v3.12.0...v3.12.1) (2026-07-14)
|
|
2
9
|
|
|
3
10
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "yadflow",
|
|
3
|
-
"version": "3.12.
|
|
3
|
+
"version": "3.12.2",
|
|
4
4
|
"description": "Yadflow — the gated, team, multi-repo SDLC: author → review → build with a PR-driven review gate and a zero-dependency `yad` CLI (setup, gate, commit, open-pr, ship, repo, thread, reconcile). A BMAD module + 38 yad-* skills.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"author": "AbdelRahman Nasr",
|
|
@@ -93,6 +93,13 @@ repo. For each commit in `<base>..HEAD`, two independent checks:
|
|
|
93
93
|
(2+ parents) — a merge's author is whoever pressed merge (often a platform noreply), not a roster
|
|
94
94
|
human, and its content already passed the PR gate suite. This waiver matters for the push-on-default
|
|
95
95
|
`yad-update-guard` (§9), which — unlike this PR-triggered gate — sees merge commits.
|
|
96
|
+
A merge commit is **additionally signature-waived when it introduces no content of its own** (its
|
|
97
|
+
combined diff — `git diff-tree --cc` — is empty, i.e. no conflict-resolution or evil-merge hunks):
|
|
98
|
+
every change it carries already lives in an individually author+signature-checked parent, so there
|
|
99
|
+
is nothing to protect. This unblocks **self-hosted GitLab**, which does not sign UI-created merge
|
|
100
|
+
commits (the signature API returns 404) — without it every routine merge would red the branch. A
|
|
101
|
+
merge that *does* introduce content of its own still requires a verified signature (fail-closed),
|
|
102
|
+
so an evil merge pushed direct-to-default cannot smuggle in unverified changes.
|
|
96
103
|
|
|
97
104
|
Degradation is explicit, never silent: a missing allowlist SKIPs the author check with a warning
|
|
98
105
|
(configure roster emails, re-wire); no GitHub/GitLab remote SKIPs the signature check (the badge is a
|
|
@@ -11,6 +11,12 @@
|
|
|
11
11
|
# commits set the platform itself as the committer (e.g. noreply@github.com), and their integrity is
|
|
12
12
|
# covered by the signature check (the platform signs them).
|
|
13
13
|
#
|
|
14
|
+
# Merge-commit signature exemption: a merge that introduces NO content of its own (its combined diff
|
|
15
|
+
# is empty — no conflict-resolution / evil-merge hunks) is signature-waived, because every change it
|
|
16
|
+
# carries already lives in its individually author+signature-checked parents. This unblocks self-hosted
|
|
17
|
+
# GitLab, which does not sign UI-created merge commits (the signature API returns 404). A merge that
|
|
18
|
+
# DOES introduce content of its own still requires a verified signature (fail-closed).
|
|
19
|
+
#
|
|
14
20
|
# Degradation is explicit, never silent:
|
|
15
21
|
# - no allowlist file -> author check SKIPPED with a warning (configure emails, re-wire)
|
|
16
22
|
# - no GitHub/GitLab remote -> signature check SKIPPED with a warning (no platform, no badge)
|
|
@@ -88,6 +94,19 @@ signature_verified() {
|
|
|
88
94
|
esac
|
|
89
95
|
}
|
|
90
96
|
|
|
97
|
+
# 0 when a merge commit introduced NO content of its own. The combined diff (--cc) lists only hunks
|
|
98
|
+
# that differ from ALL parents, so empty output means every change lives in an individually-checked
|
|
99
|
+
# parent — there is no conflict-resolution or evil-merge content unique to the merge commit, hence
|
|
100
|
+
# nothing an unverified author could smuggle in past the per-parent author+signature checks.
|
|
101
|
+
merge_introduces_no_content() {
|
|
102
|
+
# `local` on its own line: `local out=$(...)` would mask the substitution's exit status (local
|
|
103
|
+
# always returns 0). A git error (e.g. a parent tree missing in a shallow clone) must fail closed —
|
|
104
|
+
# an empty stdout from a *failed* command is not evidence the merge is content-free.
|
|
105
|
+
local out
|
|
106
|
+
out="$(git diff-tree --cc --no-commit-id -r "$1")" || return 1
|
|
107
|
+
[ -z "$out" ]
|
|
108
|
+
}
|
|
109
|
+
|
|
91
110
|
rc=0
|
|
92
111
|
while IFS= read -r sha; do
|
|
93
112
|
[ -z "$sha" ] && continue
|
|
@@ -126,6 +145,11 @@ while IFS= read -r sha; do
|
|
|
126
145
|
if [ -n "$platform" ]; then
|
|
127
146
|
if signature_verified "$sha"; then
|
|
128
147
|
echo "PASS [verified-commits]: ${short} signature verified by ${platform}"
|
|
148
|
+
elif [ "$is_merge" = 1 ] && merge_introduces_no_content "$sha"; then
|
|
149
|
+
# Content-free merge: nothing to protect. Self-hosted GitLab does not sign UI merge commits, so
|
|
150
|
+
# requiring a signature here would block every routine merge. An evil merge (content of its own)
|
|
151
|
+
# falls through to FAIL below.
|
|
152
|
+
echo "WARN [verified-commits]: ${short} unsigned merge commit — introduces no content of its own (covered by verified parents); signature waived. (Self-hosted GitLab does not sign UI merge commits; on a platform that signs its merges an unsigned content-free merge is unusual but still harmless.)"
|
|
129
153
|
else
|
|
130
154
|
echo "FAIL [verified-commits]: ${short} signature missing/unverified — sign commits (GPG/SSH key registered on ${platform}), or the signature API was unreachable (GitLab: set GITLAB_TOKEN/SDLC_API_TOKEN with read_api)."
|
|
131
155
|
rc=1
|