forgedock 1.19.0 → 1.20.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 +2 -0
- package/bin/engine.mjs +48 -3
- package/bin/runner.mjs +20 -4
- package/commands/orchestrate/phase-4-execution.md +1 -1
- package/commands/review-pr-agents/protocols.md +19 -0
- package/commands/review-pr.md +7 -6
- package/commands/work-on/build/architect.md +1 -1
- package/commands/work-on/build/implement.md +9 -1
- package/commands/work-on/build/validate.md +1 -1
- package/commands/work-on/build.md +118 -8
- package/commands/work-on/close.md +9 -4
- package/commands/work-on/remediate.md +11 -4
- package/commands/work-on/review.md +11 -3
- package/commands/work-on.md +4 -4
- package/docs/CONFIG.md +5 -1
- package/docs/RELEASE-NOTES.md +15 -0
- package/docs/TELEMETRY.md +2 -2
- package/docs/WORK-ON-RUNTIME.md +11 -3
- package/package.json +1 -1
- package/scripts/architect-resume-match.test.sh +12 -3
- package/scripts/build-knowledge-index.mjs +8 -5
- package/scripts/builder-resume-match.test.sh +12 -3
- package/scripts/calibration.mjs +5 -2
- package/scripts/check-spec-markers.sh +2 -0
- package/scripts/ci-local/lib.mjs +11 -3
- package/scripts/ci-local/run.mjs +155 -49
- package/scripts/ci-local/scoping.mjs +27 -4
- package/scripts/ci-local.sh +3 -2
- package/scripts/ci-local.test.sh +189 -0
- package/scripts/classify-finding.sh +1 -1
- package/scripts/file-deferred-followups.sh +91 -30
- package/scripts/file-deferred-followups.test.sh +101 -8
- package/scripts/lib/telemetry.sh +2 -0
- package/scripts/review-finalize.sh +101 -12
- package/scripts/review-finalize.test.sh +136 -5
- package/scripts/review-roster.sh +18 -5
- package/scripts/review-roster.test.sh +57 -1
- package/scripts/selfreview-plan.sh +86 -0
- package/scripts/selfreview-plan.test.sh +90 -0
- package/scripts/spawn-depth-check.sh +5 -4
- package/scripts/spawn-depth-check.test.sh +3 -0
- package/scripts/telemetry.test.sh +8 -0
- package/scripts/transition-label.sh +26 -0
- package/scripts/verify-phase-trail.test.sh +6 -0
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
5
5
|
#
|
|
6
6
|
# The builder decides fold-vs-defer (judgment, build.md B2.0); this script does the mechanics for the items
|
|
7
|
-
# that stay deferred: trusted lookup
|
|
8
|
-
#
|
|
7
|
+
# that stay deferred: trusted lookup (claim marker <!-- FORGE:DEFERRED_ISSUE: #M --> on the parent, then a state-all search) of a follow-up carrying
|
|
8
|
+
# <!-- FORGE:DEFERRED_FROM: #N -->, append the missing items (exact path match; a closed one is reopened), or create ONE issue
|
|
9
|
+
# with a checklist, claim it on the parent and re-check once for a concurrent lower-numbered duplicate.
|
|
9
10
|
#
|
|
10
11
|
# Usage: file-deferred-followups.sh --parent N --repo OWNER/REPO [--items FILE|-] [--folded N]
|
|
11
12
|
# items TSV, one per line: <path> TAB <why> TAB <known-flaw flag: known-flaw|1|yes|true, else empty>
|
|
@@ -71,27 +72,60 @@ finish() { # $1 = batched_into number or empty, $2 = filed count
|
|
|
71
72
|
}
|
|
72
73
|
|
|
73
74
|
# --- Trusted lookup (author predicate applied before path match), one retry ---------------------------------
|
|
74
|
-
|
|
75
|
+
# Sources, in order: (1) claim markers `<!-- FORGE:DEFERRED_ISSUE: #M -->` on the parent's REST comments (read-after-write
|
|
76
|
+
# consistent), (2) a search over ALL issue states (eventually consistent, so only a fallback). Every candidate is then
|
|
77
|
+
# verified by number over REST (trusted author, DEFERRED_FROM marker, not a PR); the lowest number is canonical.
|
|
78
|
+
JQ_PRE='def lst: split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length > 0));
|
|
79
|
+
def trusted: ((.author_association // "") as $x | ($assoc | lst) | index($x) != null)
|
|
80
|
+
or ((.user.type // "") == "Bot")
|
|
81
|
+
or ((.user.login // "") as $x | ($logins | lst) | index($x) != null);'
|
|
82
|
+
CLAIM_RE='^\s*<!-- FORGE:DEFERRED_ISSUE: #(?<n>[0-9]+) -->'
|
|
83
|
+
claim_nums() {
|
|
84
|
+
gh api --paginate "repos/${REPO}/issues/${PARENT}/comments" 2>/dev/null \
|
|
85
|
+
| jq -s -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg re "$CLAIM_RE" "$JQ_PRE"'
|
|
86
|
+
[.[][] | select(trusted) | (.body // "") | capture($re) | .n | tonumber] | unique | .[]' 2>/dev/null
|
|
87
|
+
}
|
|
88
|
+
search_nums() {
|
|
75
89
|
gh api -X GET search/issues -f per_page=50 \
|
|
76
|
-
-f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue
|
|
77
|
-
| jq -
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
|
90
|
+
-f q="\"FORGE:DEFERRED_FROM: #${PARENT}\" in:body repo:${REPO} is:issue" 2>/dev/null \
|
|
91
|
+
| jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
92
|
+
[.items[] | select(trusted) | select((.body // "") | contains($m)) | .number] | sort | .[]' 2>/dev/null
|
|
93
|
+
}
|
|
94
|
+
verify_num() { # number -> `number<TAB>state` when a trusted, non-PR issue carries the marker; empty when not (a 404 is "not it")
|
|
95
|
+
local out rc
|
|
96
|
+
out=$(gh api "repos/${REPO}/issues/$1" 2>"$TMP/verr"); rc=$?
|
|
97
|
+
if [ "$rc" -ne 0 ]; then grep -qE '404|Not Found|410' "$TMP/verr" 2>/dev/null && return 0; return 1; fi
|
|
98
|
+
printf '%s' "$out" | jq -r --arg assoc "$TRUSTED_ASSOC" --arg logins "$TRUSTED_LOGINS" --arg m "$MARKER" "$JQ_PRE"'
|
|
99
|
+
select(.pull_request == null) | select(trusted) | select((.body // "") | contains($m))
|
|
100
|
+
| "\(.number)\t\(.state)"' 2>/dev/null
|
|
101
|
+
}
|
|
102
|
+
CANON=""; CSTATE=""
|
|
103
|
+
pick_verified() { # numbers... ; sets CANON/CSTATE from the first verified one; non-zero only when a fetch failed
|
|
104
|
+
local n r
|
|
105
|
+
for n in "$@"; do
|
|
106
|
+
r=$(verify_num "$n") || return 1
|
|
107
|
+
if [ -n "$r" ]; then CANON="${r%%$'\t'*}"; CSTATE="${r#*$'\t'}"; return 0; fi
|
|
108
|
+
done
|
|
109
|
+
return 0
|
|
87
110
|
}
|
|
88
|
-
|
|
111
|
+
find_canonical() {
|
|
112
|
+
local cands
|
|
113
|
+
CANON=""; CSTATE=""
|
|
114
|
+
cands=$(claim_nums) || return 1
|
|
115
|
+
# shellcheck disable=SC2086
|
|
116
|
+
pick_verified $cands || return 1
|
|
117
|
+
[ -n "$CANON" ] && return 0
|
|
118
|
+
cands=$(search_nums) || return 1
|
|
119
|
+
# shellcheck disable=SC2086
|
|
120
|
+
pick_verified $cands || return 1
|
|
121
|
+
return 0
|
|
122
|
+
}
|
|
123
|
+
LOOKUP_OK=""
|
|
89
124
|
for attempt in 1 2; do
|
|
90
|
-
|
|
91
|
-
LOOKUP=""
|
|
125
|
+
if find_canonical; then LOOKUP_OK=1; break; fi
|
|
92
126
|
[ "$attempt" = 1 ] && sleep "${FORGE_DEFERRED_RETRY_SLEEP:-2}"
|
|
93
127
|
done
|
|
94
|
-
if [ -z "$
|
|
128
|
+
if [ -z "$LOOKUP_OK" ]; then
|
|
95
129
|
echo "DEFERRED_LOOKUP=FAILED"
|
|
96
130
|
exit 3
|
|
97
131
|
fi
|
|
@@ -108,27 +142,35 @@ has_entry() { # body-file path : exact checklist-line match, not substring
|
|
|
108
142
|
grep -F -- "\`$2\`" "$1" | grep -qE "^- \[[ xX]\] \`$(printf '%s' "$2" | sed 's/[][\.*^$+?(){}|]/\\&/g')\`( |\$)"
|
|
109
143
|
}
|
|
110
144
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
145
|
+
# Append the missing items to follow-up $1 (state $2), then reopen it when closed (review-finalize needs it open).
|
|
146
|
+
append_items() { # number state ; non-zero = failure
|
|
147
|
+
local num="$1" state="$2" note=""
|
|
114
148
|
# Re-read the body immediately before editing (parallel builds of the same parent), then append what is missing.
|
|
115
|
-
|
|
116
|
-
echo "DEFERRED_FILE=FAILED"; exit 4
|
|
117
|
-
fi
|
|
149
|
+
gh api "repos/${REPO}/issues/${num}" --jq '.body // ""' > "$TMP/body" 2>/dev/null || return 1
|
|
118
150
|
: > "$TMP/add"
|
|
119
151
|
while IFS="$SEP" read -r p w k; do
|
|
120
152
|
has_entry "$TMP/body" "$p" || entry "$p" "$w" "$k" >> "$TMP/add"
|
|
121
153
|
done < "$TMP/items"
|
|
154
|
+
[ "$state" = "closed" ] && note=" (follow-up was closed; reopened)"
|
|
122
155
|
if [ -s "$TMP/add" ]; then
|
|
123
|
-
{ cat "$TMP/body"; printf '\n### Added by a later build of #%s\n\n' "$PARENT"; cat "$TMP/add"; } > "$TMP/newbody"
|
|
156
|
+
{ cat "$TMP/body"; printf '\n### Added by a later build of #%s%s\n\n' "$PARENT" "$note"; cat "$TMP/add"; } > "$TMP/newbody"
|
|
124
157
|
if [ -n "${DRY_RUN:-}" ]; then
|
|
125
|
-
echo "DRY_RUN: gh issue edit $
|
|
126
|
-
|
|
127
|
-
|
|
158
|
+
echo "DRY_RUN: gh issue edit $num (append $(wc -l < "$TMP/add" | tr -d ' ') item(s))" >&2
|
|
159
|
+
else
|
|
160
|
+
gh issue edit "$num" -R "$REPO" --body-file "$TMP/newbody" >/dev/null 2>&1 || return 1
|
|
128
161
|
fi
|
|
129
162
|
fi
|
|
130
|
-
|
|
131
|
-
|
|
163
|
+
if [ "$state" = "closed" ]; then
|
|
164
|
+
if [ -n "${DRY_RUN:-}" ]; then echo "DRY_RUN: gh issue reopen $num" >&2
|
|
165
|
+
else gh issue reopen "$num" -R "$REPO" >/dev/null 2>&1 || return 1; fi
|
|
166
|
+
fi
|
|
167
|
+
return 0
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if [ -n "$CANON" ]; then
|
|
171
|
+
append_items "$CANON" "$CSTATE" || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
172
|
+
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$CANON"; done < "$TMP/items"
|
|
173
|
+
finish "$CANON" 0
|
|
132
174
|
exit 0
|
|
133
175
|
fi
|
|
134
176
|
|
|
@@ -154,6 +196,25 @@ else
|
|
|
154
196
|
NUM=$(printf '%s' "$URL" | grep -oE '[0-9]+$' | tail -n 1)
|
|
155
197
|
[ -n "$NUM" ] || { echo "DEFERRED_FILE=FAILED"; exit 4; }
|
|
156
198
|
fi
|
|
199
|
+
if [ -z "${DRY_RUN:-}" ]; then
|
|
200
|
+
# Claim the follow-up on the parent (REST comments are read-after-write consistent; best effort, the search still finds it).
|
|
201
|
+
gh issue comment "$PARENT" -R "$REPO" --body "<!-- FORGE:DEFERRED_ISSUE: #${NUM} -->" >/dev/null 2>&1 \
|
|
202
|
+
|| echo "file-deferred-followups: warning: could not post claim marker for #${NUM} on #${PARENT}" >&2
|
|
203
|
+
# Post-create re-check: a concurrent build may have filed first. The lowest trusted number is canonical; the newer one is closed.
|
|
204
|
+
if find_canonical; then
|
|
205
|
+
if [ -n "$CANON" ] && [ "$CANON" -lt "$NUM" ]; then
|
|
206
|
+
if append_items "$CANON" "$CSTATE"; then
|
|
207
|
+
gh issue close "$NUM" -R "$REPO" --reason "not planned" --comment "Duplicate of #${CANON}: deferred items of #${PARENT} are tracked there." >/dev/null 2>&1 \
|
|
208
|
+
|| echo "file-deferred-followups: warning: could not close duplicate #${NUM}" >&2
|
|
209
|
+
NUM="$CANON"
|
|
210
|
+
else
|
|
211
|
+
echo "file-deferred-followups: warning: could not move items to #${CANON}; keeping #${NUM}" >&2
|
|
212
|
+
fi
|
|
213
|
+
fi
|
|
214
|
+
else
|
|
215
|
+
echo "file-deferred-followups: warning: post-create re-check failed; keeping #${NUM}" >&2
|
|
216
|
+
fi
|
|
217
|
+
fi
|
|
157
218
|
while IFS="$SEP" read -r p w k; do printf '%s\t%s\n' "$p" "$NUM"; done < "$TMP/items"
|
|
158
219
|
finish "$NUM" 1
|
|
159
220
|
exit 0
|
|
@@ -8,19 +8,27 @@ S="$HERE/file-deferred-followups.sh"
|
|
|
8
8
|
T=$(mktemp -d); trap 'rm -rf "$T"' EXIT
|
|
9
9
|
mkdir "$T/bin"
|
|
10
10
|
export FORGE_DEFERRED_RETRY_SLEEP=0
|
|
11
|
-
export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body"
|
|
11
|
+
export GH_LOG="$T/gh.log" SEARCH_JSON="$T/search.json" ISSUE_BODY="$T/issue.body" COMMENTS_JSON="$T/comments.json" T_DIR="$T"
|
|
12
12
|
|
|
13
13
|
cat > "$T/bin/gh" <<'MOCK'
|
|
14
14
|
#!/usr/bin/env bash
|
|
15
15
|
echo "$*" >> "$GH_LOG"
|
|
16
16
|
case "$1 $2" in
|
|
17
17
|
"api -X") [ -n "${LOOKUP_FAIL:-}" ] && exit 1; cat "$SEARCH_JSON"; exit 0 ;;
|
|
18
|
+
"api --paginate") [ -n "${CLAIM_FAIL:-}" ] && exit 1; cat "$COMMENTS_JSON"; exit 0 ;;
|
|
18
19
|
"issue create") [ -n "${CREATE_FAIL:-}" ] && exit 1; echo "https://github.com/o/r/issues/901"; exit 0 ;;
|
|
20
|
+
"issue comment") [ -n "${COMMENT_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
21
|
+
"issue reopen") [ -n "${REOPEN_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
22
|
+
"issue close") [ -n "${CLOSE_FAIL:-}" ] && exit 1; exit 0 ;;
|
|
19
23
|
"issue edit")
|
|
20
24
|
[ -n "${EDIT_FAIL:-}" ] && exit 1
|
|
21
25
|
while [ $# -gt 0 ]; do [ "$1" = "--body-file" ] && cp "$2" "$ISSUE_BODY.edited"; shift; done; exit 0 ;;
|
|
22
26
|
esac
|
|
23
|
-
|
|
27
|
+
if [ "$1" = api ]; then
|
|
28
|
+
case "$*" in *--jq*) cat "$ISSUE_BODY"; exit 0 ;; esac
|
|
29
|
+
n="${2##*/}"; [ -f "$T_DIR/issue.$n.json" ] && { cat "$T_DIR/issue.$n.json"; exit 0; }
|
|
30
|
+
echo "HTTP 404: Not Found" >&2; exit 1
|
|
31
|
+
fi
|
|
24
32
|
exit 1
|
|
25
33
|
MOCK
|
|
26
34
|
chmod +x "$T/bin/gh"; export PATH="$T/bin:$PATH"
|
|
@@ -29,7 +37,12 @@ PASS=0; FAIL=0
|
|
|
29
37
|
ok() { PASS=$((PASS+1)); echo "PASS: $1"; }
|
|
30
38
|
bad() { FAIL=$((FAIL+1)); echo "FAIL: $1"; }
|
|
31
39
|
check() { if eval "$2"; then ok "$1"; else bad "$1"; fi; }
|
|
32
|
-
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited"; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL
|
|
40
|
+
reset() { : > "$GH_LOG"; rm -f "$ISSUE_BODY.edited" "$T"/issue.*.json; unset LOOKUP_FAIL CREATE_FAIL EDIT_FAIL CLAIM_FAIL COMMENT_FAIL REOPEN_FAIL CLOSE_FAIL DRY_RUN
|
|
41
|
+
echo '{"items":[]}' > "$SEARCH_JSON"; echo '[]' > "$COMMENTS_JSON"; }
|
|
42
|
+
# issue_json NUMBER STATE ASSOCIATION [PARENT]: REST view of a follow-up carrying the DEFERRED_FROM marker
|
|
43
|
+
issue_json() { printf '{"number":%s,"state":"%s","author_association":"%s","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #%s -->"}\n' "$1" "$2" "$3" "${4:-10}" > "$T/issue.$1.json"; }
|
|
44
|
+
# claim_json ASSOCIATION NUMBER...: parent comments carrying claim markers
|
|
45
|
+
claim_json() { local a="$1" out="" n; shift; for n in "$@"; do out="${out:+$out,}{\"author_association\":\"$a\",\"user\":{\"type\":\"User\"},\"body\":\"<!-- FORGE:DEFERRED_ISSUE: #$n -->\"}"; done; echo "[$out]" > "$COMMENTS_JSON"; }
|
|
33
46
|
printf 'a/x.sh\twhy a\t\nb/y.sh\twhy b\tknown-flaw\nc/z.sh\twhy c\t\n' > "$T/items.tsv"
|
|
34
47
|
|
|
35
48
|
# 1. N items -> exactly one create
|
|
@@ -43,7 +56,7 @@ check "create: telemetry" "printf '%s' \"\$OUT\" | grep -q 'DEFERRED_TELEMETRY d
|
|
|
43
56
|
# 2. append to existing trusted open issue; exact path match
|
|
44
57
|
reset
|
|
45
58
|
printf '## Affected Files\n- `a/x.sh`\n\n- [ ] `a/x.sh.bak` — other\n<!-- FORGE:DEFERRED_FROM: #10 -->\n' > "$ISSUE_BODY"
|
|
46
|
-
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
59
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
47
60
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
48
61
|
check "append: rc 0, no create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG'"
|
|
49
62
|
check "append: telemetry batched into 77 filed=0" "printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
|
|
@@ -55,7 +68,7 @@ reset
|
|
|
55
68
|
printf 'd/w.sh\t\tknown-flaw\n' > "$T/emptywhy.tsv"
|
|
56
69
|
bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv" >/dev/null; RC=$?
|
|
57
70
|
rm -f "$ISSUE_BODY.edited"; printf 'x\n' > "$ISSUE_BODY"
|
|
58
|
-
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
71
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
59
72
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/emptywhy.tsv"); RC=$?
|
|
60
73
|
check "empty-why: append keeps known-flaw flag" "[ $RC -eq 0 ] && grep -q 'd/w.sh. \*\*known-flaw\*\*' '$ISSUE_BODY.edited'"
|
|
61
74
|
|
|
@@ -66,13 +79,13 @@ check "missing option value: rc 2" "[ $RC -eq 2 ]"
|
|
|
66
79
|
# 3. already-listed path is not duplicated; nothing to add -> no edit
|
|
67
80
|
reset
|
|
68
81
|
printf -- '- [ ] `a/x.sh` — w\n- [x] `b/y.sh`\n- [ ] `c/z.sh`\n' > "$ISSUE_BODY"
|
|
69
|
-
echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
82
|
+
echo '{"items":[{"number":77,"author_association":"MEMBER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
70
83
|
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
71
84
|
check "idempotent: no edit, no create on resume" "[ $RC -eq 0 ] && ! grep -qE '^issue (edit|create)' '$GH_LOG'"
|
|
72
85
|
|
|
73
86
|
# 4. untrusted match ignored -> new issue
|
|
74
87
|
reset
|
|
75
|
-
echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
88
|
+
echo '{"items":[{"number":66,"author_association":"NONE","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 66 open NONE; issue_json 77 open OWNER
|
|
76
89
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv")
|
|
77
90
|
check "untrusted: ignored, create filed" "grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'filed=1'"
|
|
78
91
|
|
|
@@ -87,7 +100,7 @@ OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
|
87
100
|
check "create failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
88
101
|
reset; export EDIT_FAIL=1
|
|
89
102
|
printf 'x\n' > "$ISSUE_BODY"
|
|
90
|
-
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
103
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"; issue_json 77 open OWNER
|
|
91
104
|
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
92
105
|
check "edit failure: rc 4, no telemetry" "[ $RC -eq 4 ] && ! printf '%s' \"\$OUT\" | grep -q DEFERRED_TELEMETRY"
|
|
93
106
|
|
|
@@ -102,5 +115,85 @@ OUT=$(printf '' | bash "$S" --parent 10 --repo o/r)
|
|
|
102
115
|
check "empty: no create, deferred_items=0" "! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'deferred_items=0'"
|
|
103
116
|
bash "$S" --repo o/r >/dev/null 2>&1; check "usage: missing parent rc 2" "[ $? -eq 2 ]"
|
|
104
117
|
|
|
118
|
+
# 9. e1: search index lags (returns nothing) but the claim marker on the parent finds the follow-up
|
|
119
|
+
reset
|
|
120
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 open OWNER; claim_json OWNER 77
|
|
121
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
122
|
+
check "index-lag: claim found, no duplicate create" "[ $RC -eq 0 ] && ! grep -q '^issue create' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#77 filed=0'"
|
|
123
|
+
check "index-lag: search never needed" "! grep -q search/issues '$GH_LOG'"
|
|
124
|
+
|
|
125
|
+
# 10. e2: closed follow-up (found only by a state-all search) is reused and reopened
|
|
126
|
+
reset
|
|
127
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER
|
|
128
|
+
echo '{"items":[{"number":77,"author_association":"OWNER","user":{"type":"User"},"body":"<!-- FORGE:DEFERRED_FROM: #10 -->"}]}' > "$SEARCH_JSON"
|
|
129
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
130
|
+
check "closed: reused, reopened, no create" "[ $RC -eq 0 ] && grep -q '^issue reopen 77' '$GH_LOG' && ! grep -q '^issue create' '$GH_LOG'"
|
|
131
|
+
check "closed: search query covers all states" "grep -q 'is:issue' '$GH_LOG' && ! grep 'api -X' '$GH_LOG' | grep -q 'is:open'"
|
|
132
|
+
reset; export REOPEN_FAIL=1
|
|
133
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
134
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null; RC=$?
|
|
135
|
+
check "closed: reopen failure is rc 4" "[ $RC -eq 4 ]"
|
|
136
|
+
|
|
137
|
+
# 11. create posts the claim marker on the parent
|
|
138
|
+
reset
|
|
139
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
140
|
+
check "claim: posted on parent after create" "[ $RC -eq 0 ] && grep -q '^issue comment 10 .*FORGE:DEFERRED_ISSUE: #901' '$GH_LOG'"
|
|
141
|
+
reset; export COMMENT_FAIL=1
|
|
142
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
143
|
+
check "claim: post failure only warns" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#901 filed=1'"
|
|
144
|
+
|
|
145
|
+
# 12. e3: a concurrent build filed #800 first; the first claim read (before our create) misses it, the post-create re-check sees it
|
|
146
|
+
race_setup() { # first comments read returns [] (the other build has not claimed yet), later reads return the real listing
|
|
147
|
+
CALLS="$T/claim.calls"; : > "$CALLS"; export CALLS
|
|
148
|
+
mv "$T/bin/gh" "$T/bin/gh.real"
|
|
149
|
+
cat > "$T/bin/gh" <<'WRAP'
|
|
150
|
+
#!/usr/bin/env bash
|
|
151
|
+
if [ "$1 $2" = "api --paginate" ]; then
|
|
152
|
+
echo x >> "$CALLS"
|
|
153
|
+
if [ "$(wc -l < "$CALLS" | tr -d ' ')" -le 1 ]; then echo "$*" >> "$GH_LOG"; echo '[]'; exit 0; fi
|
|
154
|
+
fi
|
|
155
|
+
exec "$(dirname "$0")/gh.real" "$@"
|
|
156
|
+
WRAP
|
|
157
|
+
chmod +x "$T/bin/gh"
|
|
158
|
+
}
|
|
159
|
+
race_teardown() { mv "$T/bin/gh.real" "$T/bin/gh"; }
|
|
160
|
+
reset
|
|
161
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
|
|
162
|
+
race_setup
|
|
163
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
164
|
+
race_teardown
|
|
165
|
+
check "race: newer duplicate closed, items moved to lowest" "[ $RC -eq 0 ] && grep -q '^issue close 901' '$GH_LOG' && grep -q '^issue edit 800' '$GH_LOG' && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800 filed=1'"
|
|
166
|
+
check "race: rows point at canonical issue" "[ \$(printf '%s\n' \"\$OUT\" | grep -c '800\$') -ge 3 ]"
|
|
167
|
+
# e5: duplicate-close failure only warns
|
|
168
|
+
reset; export CLOSE_FAIL=1
|
|
169
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 800 open OWNER; claim_json OWNER 800
|
|
170
|
+
race_setup
|
|
171
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
172
|
+
race_teardown
|
|
173
|
+
check "race: duplicate-close failure still rc 0" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#800'"
|
|
174
|
+
|
|
175
|
+
# 13. e4: claim listing failure is a failed lookup, never "none found"
|
|
176
|
+
reset; export CLAIM_FAIL=1
|
|
177
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
178
|
+
check "claim-failure: rc 3, no create" "[ $RC -eq 3 ] && printf '%s' \"\$OUT\" | grep -q DEFERRED_LOOKUP=FAILED && ! grep -q '^issue create' '$GH_LOG'"
|
|
179
|
+
|
|
180
|
+
# 15. e6: untrusted claim comment is ignored (falls through to create)
|
|
181
|
+
reset
|
|
182
|
+
issue_json 77 open OWNER; claim_json NONE 77
|
|
183
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" 2>/dev/null); RC=$?
|
|
184
|
+
check "untrusted-claim: ignored, create filed" "[ $RC -eq 0 ] && grep -q '^issue create' '$GH_LOG'"
|
|
185
|
+
|
|
186
|
+
# 16. e7: several claims, lowest valid wins; a claim to a missing issue falls through
|
|
187
|
+
reset
|
|
188
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 80 open OWNER; issue_json 90 open OWNER; claim_json OWNER 70 90 80
|
|
189
|
+
OUT=$(bash "$S" --parent 10 --repo o/r --items "$T/items.tsv"); RC=$?
|
|
190
|
+
check "multi-claim: lowest valid (80) wins, missing 70 skipped" "[ $RC -eq 0 ] && printf '%s' \"\$OUT\" | grep -q 'batched_into=#80 filed=0'"
|
|
191
|
+
|
|
192
|
+
# 17. DRY_RUN: no claim, reopen or close
|
|
193
|
+
reset; export DRY_RUN=1
|
|
194
|
+
printf 'x\n' > "$ISSUE_BODY"; issue_json 77 closed OWNER; claim_json OWNER 77
|
|
195
|
+
bash "$S" --parent 10 --repo o/r --items "$T/items.tsv" >/dev/null 2>&1
|
|
196
|
+
check "dry-run: no comment/reopen/close/edit" "! grep -qE '^issue (comment|reopen|close|edit|create)' '$GH_LOG'"
|
|
197
|
+
|
|
105
198
|
echo "passed=$PASS failed=$FAIL"
|
|
106
199
|
[ "$FAIL" -eq 0 ]
|
package/scripts/lib/telemetry.sh
CHANGED
|
@@ -51,6 +51,8 @@ _forge_tel_record() { # event k=v... -> one JSON line on stdout
|
|
|
51
51
|
run: ($run | str), phase: ($phase | str), event: $ev, eid: $eid} + $p' --args "${args[@]}"
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
# build_selfreview (forge#3971: tier, agents, findings, fixed, declined, skipped=<reason>; ids and counts only) has no review-side
|
|
55
|
+
# routing: it defaults to the issue target below, like diff_size.
|
|
54
56
|
# finding_disposition payload is generic key=value (incl. scope=introduced|pre-existing|unknown); no per-field allowlist applies.
|
|
55
57
|
# Prints "<number> <repo>" of the comment target (issue or PR) for this event, or nothing when none is usable.
|
|
56
58
|
_forge_tel_target() {
|
|
@@ -50,7 +50,8 @@
|
|
|
50
50
|
# Phase 7-9 subcommands: --context FILE is optional; without it pass --review-sha SHA, --review-mode FULL|DELTA|BASE_SYNC_ONLY, --issue N, --base B.
|
|
51
51
|
# verdict: --merge-health S --merge-health-state S [--base B] [--head-branch H] [--is-milestone true|false] [--has-purpose-regression true|false]
|
|
52
52
|
# [--purpose-findings-file F | --purpose-findings TEXT] [--result stale|clean|blocking] [--agents-file F | --agents TEXT] [--findings-count N] [--blocking-count N]
|
|
53
|
-
# [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--dry-run]
|
|
53
|
+
# [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--other-blocking true|false] [--out-dir D] [--dry-run]
|
|
54
|
+
# (--other-blocking: the reviewer's criterion 1/2/3/5 result; resolved and persisted to OUT_DIR/other-blocking.txt, which merge-gate ORs in)
|
|
54
55
|
# calibrate: [--intensity-tier T] [--table-cell-key K] [--trust-shadow-mode B] [--trust-needs-human B] [--dry-run]; the issue is context contract.issue or --issue N
|
|
55
56
|
# merge-gate: --verdict "APPROVED|CHANGES REQUESTED" --has-purpose-regression B --calibration-needs-human B --trust-needs-human B
|
|
56
57
|
# --base-conflict B --other-blocking B [--calibration-note-file F | --calibration-note T] [--table-cell-key K] [--base B] [--issue N]
|
|
@@ -407,13 +408,15 @@ EOF
|
|
|
407
408
|
gh api "repos/${REPO}/pulls/${PR_NUMBER}/files" --paginate > "$FILES_JSON" 2>/dev/null || : > "$FILES_JSON"
|
|
408
409
|
# "path:line" for every added line; an empty file means the diff was unreadable (gates then keep every finding).
|
|
409
410
|
ADDED_LINES_FILE="$WORKDIR/added-lines"
|
|
410
|
-
|
|
411
|
+
# Second stream from the same pass: "path<TAB>text" per added line, for the Gate 2 symbol fallback.
|
|
412
|
+
ADDED_TEXT_FILE="$WORKDIR/added-text"; : > "$ADDED_TEXT_FILE"
|
|
413
|
+
jq -r '.[] | select(.patch != null) | "FILE\t\(.filename)", .patch' "$FILES_JSON" 2>/dev/null | awk -v tf="$ADDED_TEXT_FILE" '
|
|
411
414
|
/^FILE\t/{f=substr($(0),6);next}
|
|
412
415
|
/^@@/{s=$(0);sub(/^@@ -[0-9,]+ \+/,"",s);sub(/[ ,].*/,"",s);n=s+0;next}
|
|
413
416
|
/^\\/{next}
|
|
414
|
-
/^\+/{print f":"n;n++;next}
|
|
417
|
+
/^\+/{print f":"n; print f "\t" substr($(0),2) > tf; n++;next}
|
|
415
418
|
/^-/{next}
|
|
416
|
-
{n++}' > "$ADDED_LINES_FILE" || : > "$ADDED_LINES_FILE"
|
|
419
|
+
{n++}' > "$ADDED_LINES_FILE" || { : > "$ADDED_LINES_FILE"; : > "$ADDED_TEXT_FILE"; }
|
|
417
420
|
# Files the PR deleted or renamed (the old name too): findings on them always intersect.
|
|
418
421
|
GONE_FILES_FILE="$WORKDIR/gone-files"
|
|
419
422
|
jq -r '.[] | select(.status=="removed" or .status=="renamed") | .filename, (.previous_filename // empty)' "$FILES_JSON" > "$GONE_FILES_FILE" 2>/dev/null || : > "$GONE_FILES_FILE"
|
|
@@ -435,6 +438,52 @@ EOF
|
|
|
435
438
|
if gh api -H "Accept: application/vnd.github.raw" "repos/${REPO}/contents/${f}?ref=${REVIEW_SHA}" > "$t" 2>/dev/null; then awk 'END{print NR}' "$t"; else echo UNKNOWN; fi
|
|
436
439
|
}
|
|
437
440
|
|
|
441
|
+
# Gate 2 symbol fallback: 0 when an identifier the finding names occurs (whole word, fixed string) in the added lines of FILE
|
|
442
|
+
# and nowhere in the base version of FILE (a symbol the PR introduced).
|
|
443
|
+
# Candidates: backticked tokens, plus identifier-shaped tokens on "Root cause" lines; each must match an anchored identifier
|
|
444
|
+
# regex of 4+ chars and not be a common word. Reviewer text is never used in arithmetic, a glob or a regex. Any failure returns 1.
|
|
445
|
+
# A symbol counts only when it is NEW: present in the added lines AND absent from the base version of the file (#4068 review
|
|
446
|
+
# DSEC-1/DBH-1: `self`, `return`, `result` in an added line must not re-scope a pre-existing finding). The base text comes from
|
|
447
|
+
# `git show <base>:<file>` (local commit) or the raw contents API; a file the PR adds has an empty base; an unreadable base
|
|
448
|
+
# disables the fallback (the line-window result stands).
|
|
449
|
+
SYM_BASE_SHA=""
|
|
450
|
+
base_text_for() { # base_text_for <file> <out>; 0 = out holds the base text (possibly empty), 1 = unknown
|
|
451
|
+
local f="$1" o="$2"
|
|
452
|
+
if jq -e --arg f "$f" 'any(.[]; .filename == $f and .status == "added")' "$FILES_JSON" >/dev/null 2>&1; then : > "$o"; return 0; fi
|
|
453
|
+
[ -n "$SYM_BASE_SHA" ] || SYM_BASE_SHA=$(gh api "repos/${REPO}/pulls/${PR_NUMBER}" --jq .base.sha 2>/dev/null || true)
|
|
454
|
+
case "$SYM_BASE_SHA" in *[!0-9a-f]*|"") SYM_BASE_SHA="-"; return 1 ;; -) return 1 ;; esac
|
|
455
|
+
if git rev-parse -q --verify "${SYM_BASE_SHA}^{commit}" >/dev/null 2>&1; then
|
|
456
|
+
git show "${SYM_BASE_SHA}:${f}" > "$o" 2>/dev/null && return 0
|
|
457
|
+
git cat-file -e "${SYM_BASE_SHA}:${f}" 2>/dev/null && return 1
|
|
458
|
+
: > "$o"; return 0
|
|
459
|
+
fi
|
|
460
|
+
case "$f" in *[!A-Za-z0-9._/@+-]*|"") return 1 ;; esac
|
|
461
|
+
gh api -H "Accept: application/vnd.github.raw" "repos/${REPO}/contents/${f}?ref=${SYM_BASE_SHA}" > "$o" 2>/dev/null && return 0
|
|
462
|
+
return 1
|
|
463
|
+
}
|
|
464
|
+
symbol_in_added() {
|
|
465
|
+
local f="$1" text="$2" t="$WORKDIR/sym-added" b="$WORKDIR/sym-base" c n=0
|
|
466
|
+
[ -s "$ADDED_TEXT_FILE" ] || return 1
|
|
467
|
+
awk -v f="$f" 'index($0, f "\t") == 1 { print substr($0, length(f) + 2) }' "$ADDED_TEXT_FILE" > "$t" 2>/dev/null || return 1
|
|
468
|
+
[ -s "$t" ] || return 1
|
|
469
|
+
base_text_for "$f" "$b" || return 1
|
|
470
|
+
while IFS= read -r c; do
|
|
471
|
+
case "$c" in ""|[!A-Za-z_]*) continue ;; esac
|
|
472
|
+
[ "${#c}" -ge 4 ] || continue
|
|
473
|
+
case "$(printf '%s' "$c" | tr '[:upper:]' '[:lower:]')" in
|
|
474
|
+
this|that|with|from|true|false|null|none|file|line|code|test|case|value|root|cause|when|then|which|where|there|their|error|data|name|path|type|call|only|also|into|have|does|used|uses) continue ;;
|
|
475
|
+
esac
|
|
476
|
+
n=$((n + 1)); [ "$n" -le 8 ] || break
|
|
477
|
+
if grep -qwF -- "$c" "$t" 2>/dev/null && ! grep -qwF -- "$c" "$b" 2>/dev/null; then return 0; fi
|
|
478
|
+
done <<SYMEOF
|
|
479
|
+
$({
|
|
480
|
+
printf '%s\n' "$text" | grep -oE '`[^`]+`' 2>/dev/null | tr -c 'A-Za-z0-9_\n' '\n'
|
|
481
|
+
printf '%s\n' "$text" | grep -iE 'root cause' 2>/dev/null | grep -oE '[A-Za-z_][A-Za-z0-9_]*' | grep -E '_|[a-z][A-Z]|[0-9]'
|
|
482
|
+
} 2>/dev/null | grep -E '^[A-Za-z_][A-Za-z0-9_]{3,}$' | awk '!seen[$0]++')
|
|
483
|
+
SYMEOF
|
|
484
|
+
return 1
|
|
485
|
+
}
|
|
486
|
+
|
|
438
487
|
FINAL="$WORKDIR/final.jsonl"; : > "$FINAL"
|
|
439
488
|
N_STALE=0; N_PREEXISTING=0; N_NO_SCENARIO=0; N_INTRODUCED=0; N_KEPT=0
|
|
440
489
|
I=0; TOTAL=$(jq 'length' "$DEDUPED")
|
|
@@ -472,6 +521,8 @@ EOF
|
|
|
472
521
|
if grep -qxF -- "${FILE}:${L}" "$ADDED_LINES_FILE"; then INTRODUCED=1; break; fi
|
|
473
522
|
L=$((L + 1))
|
|
474
523
|
done
|
|
524
|
+
# The cited line is the reviewer's estimate (stale or at the call site): symbol fallback before routing pre-existing.
|
|
525
|
+
if [ "$INTRODUCED" -eq 0 ] && symbol_in_added "$FILE" "$(printf '%s' "$F" | jq -r '.text // ""')"; then INTRODUCED=1; fi
|
|
475
526
|
if [ "$INTRODUCED" -eq 1 ]; then SCOPE="introduced"; else
|
|
476
527
|
SCOPE="pre-existing"
|
|
477
528
|
case "$SEV" in MEDIUM|LOW|INFO) ROUTE="pre-existing" ;; esac
|
|
@@ -561,7 +612,7 @@ do_classify() {
|
|
|
561
612
|
&& [ -s "$CONTRACT_SCOPE_FILE" ] \
|
|
562
613
|
&& gh pr diff "$PR_NUMBER" -R "$REPO" --name-only > "$PR_FILES_FILE" 2>/dev/null; then
|
|
563
614
|
# A deferred row counts only if issue N is OPEN, is an issue (not a PR), is not MERGE_ISSUE, and its body
|
|
564
|
-
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus the row's path, and is authored by a trusted author
|
|
615
|
+
# carries `FORGE:DEFERRED_FROM: #<MERGE_ISSUE>` plus a checklist line for the row's exact path (the list script strips a trailing slash, so `p` and `p/` both match; `- [ ] \`path\``, [xX] accepted, the producer format of file-deferred-followups.sh; a body that only mentions the path does not count), and is authored by a trusted author
|
|
565
616
|
# (OWNER/MEMBER/COLLABORATOR or Bot; same predicate as trusted-comments.sh; missing fields fail closed).
|
|
566
617
|
# Unverified rows are dropped.
|
|
567
618
|
_verified="$_scratch/contract-verified"; : > "$_verified"
|
|
@@ -571,7 +622,7 @@ do_classify() {
|
|
|
571
622
|
[ "$_n" = "${MERGE_ISSUE#\#}" ] && continue
|
|
572
623
|
gh api "repos/${REPO}/issues/$_n" 2>/dev/null \
|
|
573
624
|
| jq -e --arg m "FORGE:DEFERRED_FROM: #${MERGE_ISSUE#\#} -->" --arg p "$_ipath" \
|
|
574
|
-
'.state == "open" and .pull_request == null and ((.body // "") | contains($m) and
|
|
625
|
+
'.state == "open" and .pull_request == null and ((.body // "") | (contains($m) and (split("\n") | any(rtrimstr("\r") as $l | ("- [ ] `","- [x] `","- [X] `") as $pre | ($p, $p + "/") as $q | ($pre + $q + "`") as $w | $l == $w or ($l | startswith($w + " ")))))) and (((.author_association // "") | IN("OWNER","MEMBER","COLLABORATOR")) or ((.user.type // "") == "Bot"))' >/dev/null 2>&1 \
|
|
575
626
|
|| continue
|
|
576
627
|
grep -Fxq -- "$_n" "$CONTRACT_OPEN_FILE" || echo "$_n" >> "$CONTRACT_OPEN_FILE"
|
|
577
628
|
fi
|
|
@@ -696,11 +747,32 @@ do_classify() {
|
|
|
696
747
|
fi
|
|
697
748
|
case "$DISPOSITION" in
|
|
698
749
|
"NOTE contract-deferred #"*)
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
750
|
+
# The demotion counts only after the comment on #N succeeded and #N is still OPEN; any failure files the finding (fails toward filing).
|
|
751
|
+
_dn="${DISPOSITION##*#}"; _dok=1
|
|
752
|
+
case "$_dn" in ''|*[!0-9]*) _dok=0 ;; *)
|
|
753
|
+
if [ "$DRY_RUN" != "true" ]; then
|
|
754
|
+
_dst=$(gh issue view "$_dn" -R "$REPO" --json state --jq .state 2>/dev/null) || _dst=""
|
|
755
|
+
if [ "$_dst" != "OPEN" ]; then _dok=0
|
|
756
|
+
elif ! gh issue comment "$_dn" -R "$REPO" --body "Review of #${PR_NUMBER} also found this on a path declared deferred here: ${F_LOC} ${F_TITLE}" >/dev/null 2>&1; then _dok=0 # allowlist:check-command-side-effects
|
|
757
|
+
fi
|
|
758
|
+
fi
|
|
702
759
|
;; esac
|
|
703
|
-
CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1))
|
|
760
|
+
if [ "$_dok" = "1" ]; then CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1)); else
|
|
761
|
+
# Fall back to what the finding would have been absent the contract (a LOW/POSSIBLE or lineage-damped finding stays a NOTE),
|
|
762
|
+
# not a blanket ISSUE: re-run the classifier without the contract flags. An INPR_FIX or unusable result files (fails toward filing).
|
|
763
|
+
DISPOSITION="ISSUE contract-comment-failed"
|
|
764
|
+
_n=$#; _skip=0
|
|
765
|
+
while [ "$_n" -gt 0 ]; do
|
|
766
|
+
_a="$1"; shift; _n=$((_n - 1))
|
|
767
|
+
if [ "$_skip" = "1" ]; then _skip=0; continue; fi
|
|
768
|
+
case "$_a" in --contract-scope|--contract-open|--pr-files) _skip=1 ;; *) set -- "$@" "$_a" ;; esac
|
|
769
|
+
done
|
|
770
|
+
_fb=$(bash "$CLASSIFY_SCRIPT" "$@" 2>/dev/null | head -n 1) || _fb=""
|
|
771
|
+
case "$_fb" in
|
|
772
|
+
"NOTE reflag"*) DISPOSITION="$_fb"; NOTES_REFLAG=$((NOTES_REFLAG + 1)) ;;
|
|
773
|
+
NOTE*|ISSUE*) DISPOSITION="$_fb" ;;
|
|
774
|
+
esac
|
|
775
|
+
fi ;;
|
|
704
776
|
"NOTE contract-accepted-risk"*) CONTRACT_DEMOTED=$((CONTRACT_DEMOTED + 1)) ;;
|
|
705
777
|
"NOTE reflag-"*) NOTES_REFLAG=$((NOTES_REFLAG + 1)) ;;
|
|
706
778
|
INPR_FIX*)
|
|
@@ -1021,6 +1093,20 @@ do_verdict() {
|
|
|
1021
1093
|
if [ "$RESULT" = "clean" ] && { [ "$BASE_CONFLICT" = "true" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ]; }; then
|
|
1022
1094
|
RESULT="blocking"; VERDICT_COERCED=true
|
|
1023
1095
|
fi
|
|
1096
|
+
# OTHER_BLOCKING (criteria 1, 2, 3 and 5) is reviewer-judged, so it is resolved here and persisted for merge-gate to OR in: a blocking
|
|
1097
|
+
# result that BASE_CONFLICT and HAS_PURPOSE_REGRESSION do not explain was caused by another criterion, whatever the reviewer passed.
|
|
1098
|
+
OTHER_BLOCKING_RESOLVED=""
|
|
1099
|
+
case "$OTHER_BLOCKING" in true|false) OTHER_BLOCKING_RESOLVED="$OTHER_BLOCKING" ;; esac
|
|
1100
|
+
if [ "$RESULT" = "blocking" ] && [ "$BASE_CONFLICT" != "true" ] && [ "$HAS_PURPOSE_REGRESSION" != "true" ]; then
|
|
1101
|
+
OTHER_BLOCKING_RESOLVED=true
|
|
1102
|
+
elif [ "$RESULT" = "clean" ]; then
|
|
1103
|
+
OTHER_BLOCKING_RESOLVED=false
|
|
1104
|
+
fi
|
|
1105
|
+
# Rewritten (or removed) on every run so a stale value from a prior head can never survive a re-review.
|
|
1106
|
+
rm -f "$OUT_DIR/other-blocking.txt" 2>/dev/null || true
|
|
1107
|
+
if { [ "$RESULT" = "clean" ] || [ "$RESULT" = "blocking" ]; } && [ -n "$OTHER_BLOCKING_RESOLVED" ] && [ "$DRY_RUN" != "true" ]; then
|
|
1108
|
+
printf '%s\n' "$OTHER_BLOCKING_RESOLVED" > "$OUT_DIR/other-blocking.txt" 2>/dev/null || true
|
|
1109
|
+
fi
|
|
1024
1110
|
VERDICT_OUT=""
|
|
1025
1111
|
case "$RESULT" in
|
|
1026
1112
|
stale) VERDICT_OUT="STALE" ;;
|
|
@@ -1055,6 +1141,7 @@ ${TRUST_ANNOTATION}${CONFLICT_LINE}${PROTECTION_LINE}${PURPOSE_LINE}${ATTRIBUTIO
|
|
|
1055
1141
|
echo "HAS_PURPOSE_REGRESSION=${HAS_PURPOSE_REGRESSION}"
|
|
1056
1142
|
echo "HAS_MERGE_CONFLICT=${HAS_MERGE_CONFLICT}"
|
|
1057
1143
|
echo "BASE_CONFLICT=${BASE_CONFLICT}"
|
|
1144
|
+
echo "OTHER_BLOCKING=${OTHER_BLOCKING_RESOLVED}"
|
|
1058
1145
|
echo "MERGE_CONFLICT_MSG=${MERGE_CONFLICT_MSG}"
|
|
1059
1146
|
echo "MERGE_PROTECTION_WARNING=${MERGE_PROTECTION_WARNING}"
|
|
1060
1147
|
exit 0
|
|
@@ -1347,6 +1434,8 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1347
1434
|
gh issue edit "$MERGE_ISSUE" -R "$REPO" --add-label "needs-human" 2>/dev/null || true # <!-- allowlist:check-command-side-effects -->
|
|
1348
1435
|
blocked "merge guard state unbound"
|
|
1349
1436
|
fi
|
|
1437
|
+
# OR in the value `verdict` persisted: a passed `false` never overrides a persisted `true`. Anything else in the file is ignored.
|
|
1438
|
+
if [ "$(head -n 1 "$OUT_DIR/other-blocking.txt" 2>/dev/null | tr -d '[:space:]')" = "true" ]; then OTHER_BLOCKING=true; fi
|
|
1350
1439
|
# ONLY_BASE_CONFLICT: the base conflict is the ONLY reason for CHANGES REQUESTED, evaluated after every human gate is set.
|
|
1351
1440
|
ONLY_BASE_CONFLICT=false
|
|
1352
1441
|
if [ "$BASE_CONFLICT" = "true" ] && [ "$OTHER_BLOCKING" != "true" ] && [ "$HAS_PURPOSE_REGRESSION" != "true" ] && [ "$CALIBRATION_NEEDS_HUMAN" != "true" ] && [ "$TRUST_NEEDS_HUMAN" != "true" ]; then
|
|
@@ -1354,7 +1443,7 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1354
1443
|
fi
|
|
1355
1444
|
if [ "$ONLY_BASE_CONFLICT" = "true" ] && [ "$VERDICT" = "CHANGES REQUESTED" ]; then
|
|
1356
1445
|
# Pure base conflict: route to the base-sync path, NOT needs-human. A standalone /review-pr caller just sees the blocker.
|
|
1357
|
-
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict with \`${BASE}\` (the only blocking reason).
|
|
1446
|
+
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict with \`${BASE}\` (the only blocking reason). Base sync needed: the work-on pipeline performs the base sync (merging \`origin/${BASE}\` into the PR branch, merge only, no rebase, no force-push); a standalone \`/review-pr --auto-merge\` caller with no work-on router must run \`/work-on ${MERGE_ISSUE} --remediate\` to do it. The sync re-runs the quality gate and then re-reviews the new head. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
1358
1447
|
echo "MERGE_OUTCOME=base-conflict"
|
|
1359
1448
|
blocked "base-conflict"
|
|
1360
1449
|
elif [ "$VERDICT" = "CHANGES REQUESTED" ] || [ "$HAS_PURPOSE_REGRESSION" = "true" ] || [ "$CALIBRATION_NEEDS_HUMAN" = "true" ] || [ "$TRUST_NEEDS_HUMAN" = "true" ]; then
|
|
@@ -1382,7 +1471,7 @@ The review must add a \`### Degraded Paths Reviewed\` section that names each de
|
|
|
1382
1471
|
fi
|
|
1383
1472
|
if [ "$PRE_MERGE_BASE_CONFLICT" = "true" ]; then
|
|
1384
1473
|
# Base moved during review: route to the base-sync path, NOT needs-human.
|
|
1385
|
-
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`).
|
|
1474
|
+
gh issue comment "$MERGE_ISSUE" -R "$REPO" --body "⛔ Auto-merge aborted for PR #${PR_NUMBER}: base conflict (\`mergeable=${PRE_MERGE_HEALTH}\`, \`mergeStateStatus=${PRE_MERGE_HEALTH_STATE}\`). Base sync needed: the work-on pipeline performs the base sync (merging \`origin/${BASE}\` into the PR branch, merge only, no rebase, no force-push); a standalone \`/review-pr --auto-merge\` caller with no work-on router must run \`/work-on ${MERGE_ISSUE} --remediate\` to do it. The sync is followed by a re-review. A human is pulled in only if the conflict cannot be resolved automatically." # <!-- allowlist:check-command-side-effects -->
|
|
1386
1475
|
echo "MERGE_OUTCOME=base-conflict"
|
|
1387
1476
|
blocked "base-conflict"
|
|
1388
1477
|
elif [ "$PRE_MERGE_HEALTH_STATE" = "BLOCKED" ]; then
|