@jonit-dev/night-watch-cli 1.8.12-beta.9 → 1.8.14-beta.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/dist/cli.js +376 -201
- package/dist/commands/audit.d.ts.map +1 -1
- package/dist/commands/audit.js +5 -1
- package/dist/commands/audit.js.map +1 -1
- package/dist/commands/board.d.ts.map +1 -1
- package/dist/commands/board.js +2 -0
- package/dist/commands/board.js.map +1 -1
- package/dist/commands/dashboard/tab-config.d.ts.map +1 -1
- package/dist/commands/dashboard/tab-config.js +1 -11
- package/dist/commands/dashboard/tab-config.js.map +1 -1
- package/dist/commands/doctor.d.ts +1 -6
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +2 -59
- package/dist/commands/doctor.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +1 -0
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/notify.d.ts.map +1 -1
- package/dist/commands/notify.js +3 -13
- package/dist/commands/notify.js.map +1 -1
- package/dist/commands/run.d.ts +14 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +97 -40
- package/dist/commands/run.js.map +1 -1
- package/dist/scripts/night-watch-audit-cron.sh +11 -1
- package/dist/scripts/night-watch-cron.sh +4 -2
- package/dist/scripts/night-watch-merger-cron.sh +177 -32
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +107 -2
- package/dist/templates/audit.md +64 -30
- package/dist/templates/night-watch-audit.md +71 -30
- package/dist/templates/night-watch-pr-reviewer.md +7 -6
- package/dist/templates/pr-reviewer.md +7 -6
- package/dist/web/assets/index-CL3Q-KB4.css +1 -0
- package/dist/web/assets/index-FDOCfjkP.js +442 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
|
@@ -14,6 +14,8 @@ set -euo pipefail
|
|
|
14
14
|
# NW_MERGER_BRANCH_PATTERNS= - Comma-separated branch prefixes (empty = all)
|
|
15
15
|
# NW_MERGER_REBASE_BEFORE_MERGE=1 - Set to 1 to rebase before merging
|
|
16
16
|
# NW_MERGER_MAX_PRS_PER_RUN=0 - Max PRs to merge per run (0 = unlimited)
|
|
17
|
+
# NW_MERGER_CI_MAX_WAIT=300 - Max seconds to wait for checks after rebase
|
|
18
|
+
# NW_MERGER_CI_POLL_INTERVAL=15 - Seconds between check polls after rebase
|
|
17
19
|
# NW_DRY_RUN=0 - Set to 1 for dry-run mode
|
|
18
20
|
|
|
19
21
|
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
@@ -26,6 +28,8 @@ MERGE_METHOD="${NW_MERGER_MERGE_METHOD:-squash}"
|
|
|
26
28
|
MIN_REVIEW_SCORE="${NW_MERGER_MIN_REVIEW_SCORE:-80}"
|
|
27
29
|
REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
|
|
28
30
|
MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
|
|
31
|
+
CI_MAX_WAIT="${NW_MERGER_CI_MAX_WAIT:-300}"
|
|
32
|
+
CI_POLL_INTERVAL="${NW_MERGER_CI_POLL_INTERVAL:-15}"
|
|
29
33
|
BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
|
|
30
34
|
READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
|
|
31
35
|
SCRIPT_START_TIME=$(date +%s)
|
|
@@ -40,6 +44,12 @@ fi
|
|
|
40
44
|
if ! [[ "${MIN_REVIEW_SCORE}" =~ ^[0-9]+$ ]]; then
|
|
41
45
|
MIN_REVIEW_SCORE="80"
|
|
42
46
|
fi
|
|
47
|
+
if ! [[ "${CI_MAX_WAIT}" =~ ^[0-9]+$ ]]; then
|
|
48
|
+
CI_MAX_WAIT="300"
|
|
49
|
+
fi
|
|
50
|
+
if ! [[ "${CI_POLL_INTERVAL}" =~ ^[0-9]+$ ]] || [ "${CI_POLL_INTERVAL}" = "0" ]; then
|
|
51
|
+
CI_POLL_INTERVAL="15"
|
|
52
|
+
fi
|
|
43
53
|
# Clamp merge method to valid values
|
|
44
54
|
case "${MERGE_METHOD}" in
|
|
45
55
|
squash|merge|rebase) ;;
|
|
@@ -110,24 +120,145 @@ get_review_score() {
|
|
|
110
120
|
echo "${score}"
|
|
111
121
|
}
|
|
112
122
|
|
|
113
|
-
#
|
|
114
|
-
|
|
123
|
+
# Count pending review feedback that should send the PR back through reviewer
|
|
124
|
+
# instead of allowing the merger to land it. We combine GitHub's review decision
|
|
125
|
+
# with unresolved review threads because some providers leave actionable feedback
|
|
126
|
+
# as thread comments without lowering the score.
|
|
127
|
+
get_pending_review_feedback_count() {
|
|
115
128
|
local pr_number="${1}"
|
|
116
|
-
local
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
local
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
129
|
+
local review_decision=""
|
|
130
|
+
local change_request_count="0"
|
|
131
|
+
local unresolved_thread_count="0"
|
|
132
|
+
local repo=""
|
|
133
|
+
local owner=""
|
|
134
|
+
local name=""
|
|
135
|
+
|
|
136
|
+
review_decision=$(gh pr view "${pr_number}" --json reviewDecision --jq '.reviewDecision // ""' 2>/dev/null || echo "")
|
|
137
|
+
|
|
138
|
+
change_request_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
|
|
139
|
+
--jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
|
|
140
|
+
if ! [[ "${change_request_count}" =~ ^[0-9]+$ ]]; then
|
|
141
|
+
change_request_count="0"
|
|
142
|
+
fi
|
|
143
|
+
|
|
144
|
+
repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
145
|
+
if [[ "${repo}" == */* ]]; then
|
|
146
|
+
owner="${repo%%/*}"
|
|
147
|
+
name="${repo#*/}"
|
|
148
|
+
unresolved_thread_count=$(gh api graphql \
|
|
149
|
+
-F owner="${owner}" \
|
|
150
|
+
-F name="${name}" \
|
|
151
|
+
-F number="${pr_number}" \
|
|
152
|
+
-f query='
|
|
153
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
154
|
+
repository(owner: $owner, name: $name) {
|
|
155
|
+
pullRequest(number: $number) {
|
|
156
|
+
reviewThreads(first: 100) {
|
|
157
|
+
nodes {
|
|
158
|
+
isResolved
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
' \
|
|
165
|
+
--jq '[.data.repository.pullRequest.reviewThreads.nodes[]? | select(.isResolved == false)] | length' \
|
|
166
|
+
2>/dev/null || echo "0")
|
|
167
|
+
if ! [[ "${unresolved_thread_count}" =~ ^[0-9]+$ ]]; then
|
|
168
|
+
unresolved_thread_count="0"
|
|
169
|
+
fi
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ "${unresolved_thread_count}" -gt 0 ]; then
|
|
173
|
+
echo "${unresolved_thread_count}"
|
|
174
|
+
return 0
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if [ "${change_request_count}" -gt 0 ]; then
|
|
178
|
+
echo "${change_request_count}"
|
|
179
|
+
return 0
|
|
123
180
|
fi
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return 1
|
|
181
|
+
|
|
182
|
+
if [ "${review_decision}" = "CHANGES_REQUESTED" ]; then
|
|
183
|
+
echo "1"
|
|
184
|
+
return 0
|
|
129
185
|
fi
|
|
130
|
-
|
|
186
|
+
|
|
187
|
+
echo "0"
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
# Get the current head OID for a PR.
|
|
191
|
+
get_pr_head_oid() {
|
|
192
|
+
local pr_number="${1}"
|
|
193
|
+
gh pr view "${pr_number}" --json headRefOid --jq '.headRefOid // ""' 2>/dev/null || echo ""
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
# Return the CI state for the PR's status rollup on the expected head OID.
|
|
197
|
+
ci_status_for_head() {
|
|
198
|
+
local pr_number="${1}"
|
|
199
|
+
local expected_head="${2:-}"
|
|
200
|
+
local status_json
|
|
201
|
+
|
|
202
|
+
status_json=$(gh pr view "${pr_number}" --json headRefOid,statusCheckRollup 2>/dev/null || echo "")
|
|
203
|
+
if [ -z "${status_json}" ]; then
|
|
204
|
+
echo "unknown"
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
echo "${status_json}" | jq -r --arg expected_head "${expected_head}" '
|
|
209
|
+
def in_list($values): . as $value | $values | index($value);
|
|
210
|
+
def check_run_failure:
|
|
211
|
+
.__typename == "CheckRun"
|
|
212
|
+
and ((.conclusion // "") | in_list(["FAILURE", "TIMED_OUT", "CANCELLED", "ACTION_REQUIRED", "STALE", "STARTUP_FAILURE"]));
|
|
213
|
+
def status_context_failure:
|
|
214
|
+
.__typename == "StatusContext"
|
|
215
|
+
and ((.state // "") | in_list(["FAILURE", "ERROR"]));
|
|
216
|
+
def check_run_pending:
|
|
217
|
+
.__typename == "CheckRun"
|
|
218
|
+
and ((.status // "") != "COMPLETED" or .conclusion == null);
|
|
219
|
+
def status_context_pending:
|
|
220
|
+
.__typename == "StatusContext"
|
|
221
|
+
and ((.state // "") | in_list(["PENDING", "EXPECTED"]));
|
|
222
|
+
def check_run_nonpassing:
|
|
223
|
+
.__typename == "CheckRun"
|
|
224
|
+
and ((.conclusion // "") | in_list(["SUCCESS", "NEUTRAL", "SKIPPED"]) | not);
|
|
225
|
+
def status_context_nonpassing:
|
|
226
|
+
.__typename == "StatusContext"
|
|
227
|
+
and (.state // "") != "SUCCESS";
|
|
228
|
+
if ($expected_head != "" and (.headRefOid // "") != $expected_head) then
|
|
229
|
+
"head_mismatch"
|
|
230
|
+
elif ((.statusCheckRollup // []) | length) == 0 then
|
|
231
|
+
"absent"
|
|
232
|
+
elif any((.statusCheckRollup // [])[]; check_run_failure or status_context_failure) then
|
|
233
|
+
"failed"
|
|
234
|
+
elif any((.statusCheckRollup // [])[]; check_run_pending or status_context_pending) then
|
|
235
|
+
"pending"
|
|
236
|
+
elif any((.statusCheckRollup // [])[]; check_run_nonpassing or status_context_nonpassing) then
|
|
237
|
+
"failed"
|
|
238
|
+
else
|
|
239
|
+
"passing"
|
|
240
|
+
end
|
|
241
|
+
' 2>/dev/null || echo "unknown"
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
wait_for_ci_passing_on_head() {
|
|
245
|
+
local pr_number="${1}"
|
|
246
|
+
local expected_head="${2}"
|
|
247
|
+
local ci_waited=0
|
|
248
|
+
LAST_CI_STATUS="unknown"
|
|
249
|
+
|
|
250
|
+
while [ "${ci_waited}" -lt "${CI_MAX_WAIT}" ]; do
|
|
251
|
+
LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
|
|
252
|
+
if [ "${LAST_CI_STATUS}" = "passing" ]; then
|
|
253
|
+
return 0
|
|
254
|
+
fi
|
|
255
|
+
log "INFO: PR #${pr_number}: Waiting for fresh CI on head ${expected_head} (${LAST_CI_STATUS}, ${ci_waited}s/${CI_MAX_WAIT}s)..."
|
|
256
|
+
sleep "${CI_POLL_INTERVAL}"
|
|
257
|
+
ci_waited=$((ci_waited + CI_POLL_INTERVAL))
|
|
258
|
+
done
|
|
259
|
+
|
|
260
|
+
LAST_CI_STATUS=$(ci_status_for_head "${pr_number}" "${expected_head}")
|
|
261
|
+
[ "${LAST_CI_STATUS}" = "passing" ]
|
|
131
262
|
}
|
|
132
263
|
|
|
133
264
|
# Rebase a PR against its base branch
|
|
@@ -254,9 +385,16 @@ while IFS= read -r pr_json; do
|
|
|
254
385
|
continue
|
|
255
386
|
fi
|
|
256
387
|
|
|
388
|
+
pr_head_oid=$(get_pr_head_oid "${pr_number}")
|
|
389
|
+
if [ -z "${pr_head_oid}" ]; then
|
|
390
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Unable to determine PR head, skipping"
|
|
391
|
+
continue
|
|
392
|
+
fi
|
|
393
|
+
|
|
257
394
|
# Check CI status
|
|
258
|
-
|
|
259
|
-
|
|
395
|
+
ci_status=$(ci_status_for_head "${pr_number}" "${pr_head_oid}")
|
|
396
|
+
if [ "${ci_status}" != "passing" ]; then
|
|
397
|
+
log "INFO: PR #${pr_number} (${pr_branch}): CI not passing on head ${pr_head_oid} (${ci_status}), skipping"
|
|
260
398
|
continue
|
|
261
399
|
fi
|
|
262
400
|
|
|
@@ -264,15 +402,22 @@ while IFS= read -r pr_json; do
|
|
|
264
402
|
if [ "${MIN_REVIEW_SCORE}" -gt "0" ]; then
|
|
265
403
|
score=$(get_review_score "${pr_number}")
|
|
266
404
|
if [ "${score}" -lt "0" ] || [ "${score}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
267
|
-
log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found),
|
|
405
|
+
log "INFO: PR #${pr_number} (${pr_branch}): Review score ${score} < ${MIN_REVIEW_SCORE} (or no score found), reviewer job required before merge"
|
|
268
406
|
continue
|
|
269
407
|
fi
|
|
270
408
|
fi
|
|
271
409
|
|
|
410
|
+
pending_review_feedback_count=$(get_pending_review_feedback_count "${pr_number}")
|
|
411
|
+
if [ "${pending_review_feedback_count}" -gt "0" ]; then
|
|
412
|
+
log "INFO: PR #${pr_number} (${pr_branch}): ${pending_review_feedback_count} pending review feedback item(s), reviewer job required before merge"
|
|
413
|
+
continue
|
|
414
|
+
fi
|
|
415
|
+
|
|
272
416
|
log "INFO: PR #${pr_number} (${pr_branch}): Eligible for merge"
|
|
273
417
|
|
|
274
418
|
# Rebase before merge if configured
|
|
275
419
|
if [ "${REBASE_BEFORE_MERGE}" = "1" ]; then
|
|
420
|
+
pr_head_before_rebase="${pr_head_oid}"
|
|
276
421
|
if ! rebase_pr "${pr_number}"; then
|
|
277
422
|
log "WARN: PR #${pr_number}: Rebase failed, skipping"
|
|
278
423
|
FAILED_PRS=$((FAILED_PRS + 1))
|
|
@@ -280,20 +425,20 @@ while IFS= read -r pr_json; do
|
|
|
280
425
|
fi
|
|
281
426
|
log "INFO: PR #${pr_number}: Rebase successful"
|
|
282
427
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
if !
|
|
296
|
-
log "INFO: PR #${pr_number}: CI not passing after rebase (waited ${
|
|
428
|
+
pr_head_after_rebase=$(get_pr_head_oid "${pr_number}")
|
|
429
|
+
if [ -z "${pr_head_after_rebase}" ]; then
|
|
430
|
+
log "INFO: PR #${pr_number}: Unable to determine PR head after rebase, skipping"
|
|
431
|
+
continue
|
|
432
|
+
fi
|
|
433
|
+
if [ "${pr_head_after_rebase}" != "${pr_head_before_rebase}" ]; then
|
|
434
|
+
log "INFO: PR #${pr_number}: Head changed after rebase ${pr_head_before_rebase} -> ${pr_head_after_rebase}; waiting for fresh CI"
|
|
435
|
+
else
|
|
436
|
+
log "INFO: PR #${pr_number}: Head unchanged after rebase (${pr_head_after_rebase}); confirming CI"
|
|
437
|
+
fi
|
|
438
|
+
|
|
439
|
+
# Poll CI until all checks attached to the post-rebase head are complete and passing.
|
|
440
|
+
if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
|
|
441
|
+
log "INFO: PR #${pr_number}: Fresh CI not passing on head ${pr_head_after_rebase} after rebase (${LAST_CI_STATUS}, waited ${CI_MAX_WAIT}s), skipping"
|
|
297
442
|
continue
|
|
298
443
|
fi
|
|
299
444
|
fi
|
|
@@ -89,6 +89,26 @@ emit_result() {
|
|
|
89
89
|
fi
|
|
90
90
|
}
|
|
91
91
|
|
|
92
|
+
is_pr_open() {
|
|
93
|
+
local pr_number="${1:?PR number required}"
|
|
94
|
+
local pr_state=""
|
|
95
|
+
|
|
96
|
+
pr_state=$(gh pr view "${pr_number}" --json state --jq '.state // ""' 2>/dev/null || echo "")
|
|
97
|
+
case "${pr_state}" in
|
|
98
|
+
OPEN)
|
|
99
|
+
return 0
|
|
100
|
+
;;
|
|
101
|
+
MERGED|CLOSED)
|
|
102
|
+
return 1
|
|
103
|
+
;;
|
|
104
|
+
esac
|
|
105
|
+
|
|
106
|
+
# Backward-compatible fallback for older gh versions or tests that do not
|
|
107
|
+
# support the state field. Treat an unknown/empty state as open only when the
|
|
108
|
+
# PR is still viewable by number.
|
|
109
|
+
gh pr view "${pr_number}" --json number >/dev/null 2>&1
|
|
110
|
+
}
|
|
111
|
+
|
|
92
112
|
require_provider_on_path() {
|
|
93
113
|
if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
|
|
94
114
|
echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
|
|
@@ -132,6 +152,72 @@ get_pr_comments() {
|
|
|
132
152
|
} | awk '!seen[$0]++'
|
|
133
153
|
}
|
|
134
154
|
|
|
155
|
+
get_pr_pending_review_feedback_count() {
|
|
156
|
+
local pr_number="${1:?PR number required}"
|
|
157
|
+
local review_decision=""
|
|
158
|
+
local change_request_count="0"
|
|
159
|
+
local unresolved_thread_count="0"
|
|
160
|
+
local repo="${REPO:-}"
|
|
161
|
+
local owner=""
|
|
162
|
+
local name=""
|
|
163
|
+
|
|
164
|
+
review_decision=$(gh pr view "${pr_number}" --json reviewDecision --jq '.reviewDecision // ""' 2>/dev/null || echo "")
|
|
165
|
+
|
|
166
|
+
change_request_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
|
|
167
|
+
--jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
|
|
168
|
+
if ! [[ "${change_request_count}" =~ ^[0-9]+$ ]]; then
|
|
169
|
+
change_request_count="0"
|
|
170
|
+
fi
|
|
171
|
+
|
|
172
|
+
if [ -z "${repo}" ]; then
|
|
173
|
+
repo=$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null || echo "")
|
|
174
|
+
fi
|
|
175
|
+
|
|
176
|
+
if [[ "${repo}" == */* ]]; then
|
|
177
|
+
owner="${repo%%/*}"
|
|
178
|
+
name="${repo#*/}"
|
|
179
|
+
unresolved_thread_count=$(gh api graphql \
|
|
180
|
+
-F owner="${owner}" \
|
|
181
|
+
-F name="${name}" \
|
|
182
|
+
-F number="${pr_number}" \
|
|
183
|
+
-f query='
|
|
184
|
+
query($owner: String!, $name: String!, $number: Int!) {
|
|
185
|
+
repository(owner: $owner, name: $name) {
|
|
186
|
+
pullRequest(number: $number) {
|
|
187
|
+
reviewThreads(first: 100) {
|
|
188
|
+
nodes {
|
|
189
|
+
isResolved
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
' \
|
|
196
|
+
--jq '[.data.repository.pullRequest.reviewThreads.nodes[]? | select(.isResolved == false)] | length' \
|
|
197
|
+
2>/dev/null || echo "0")
|
|
198
|
+
if ! [[ "${unresolved_thread_count}" =~ ^[0-9]+$ ]]; then
|
|
199
|
+
unresolved_thread_count="0"
|
|
200
|
+
fi
|
|
201
|
+
fi
|
|
202
|
+
|
|
203
|
+
if [ "${unresolved_thread_count}" -gt 0 ]; then
|
|
204
|
+
echo "${unresolved_thread_count}"
|
|
205
|
+
return 0
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
if [ "${change_request_count}" -gt 0 ]; then
|
|
209
|
+
echo "${change_request_count}"
|
|
210
|
+
return 0
|
|
211
|
+
fi
|
|
212
|
+
|
|
213
|
+
if [ "${review_decision}" = "CHANGES_REQUESTED" ]; then
|
|
214
|
+
echo "1"
|
|
215
|
+
return 0
|
|
216
|
+
fi
|
|
217
|
+
|
|
218
|
+
echo "0"
|
|
219
|
+
}
|
|
220
|
+
|
|
135
221
|
get_pr_head_ref_oid() {
|
|
136
222
|
local pr_number="${1:?PR number required}"
|
|
137
223
|
gh pr view "${pr_number}" --json headRefOid --jq '.headRefOid' 2>/dev/null || echo ""
|
|
@@ -654,7 +740,7 @@ fi
|
|
|
654
740
|
|
|
655
741
|
if [ -n "${TARGET_PR}" ]; then
|
|
656
742
|
OPEN_PRS=$(
|
|
657
|
-
if
|
|
743
|
+
if is_pr_open "${TARGET_PR}"; then
|
|
658
744
|
echo "1"
|
|
659
745
|
else
|
|
660
746
|
echo "0"
|
|
@@ -760,6 +846,18 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
|
|
|
760
846
|
continue
|
|
761
847
|
fi
|
|
762
848
|
|
|
849
|
+
PENDING_REVIEW_FEEDBACK_COUNT=$(get_pr_pending_review_feedback_count "${pr_number}")
|
|
850
|
+
if [ "${PENDING_REVIEW_FEEDBACK_COUNT}" -gt 0 ]; then
|
|
851
|
+
if [ "${local_ready_for_review_label_present}" -eq 1 ]; then
|
|
852
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has pending review feedback; removing stale ${READY_FOR_REVIEW_LABEL} label"
|
|
853
|
+
clear_ready_for_human_review_label "${pr_number}"
|
|
854
|
+
fi
|
|
855
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has ${PENDING_REVIEW_FEEDBACK_COUNT} pending review feedback item(s)"
|
|
856
|
+
NEEDS_WORK=1
|
|
857
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
858
|
+
continue
|
|
859
|
+
fi
|
|
860
|
+
|
|
763
861
|
if has_ready_for_human_review_marker "${all_comments}" "${current_head_sha}"; then
|
|
764
862
|
SKIPPED_ALREADY_REVIEWED_CURRENT_HEAD=1
|
|
765
863
|
log "INFO: PR #${pr_number} (${pr_branch}) is already marked ready for human review at head ${current_head_sha:0:12}; skipping repeat automated review"
|
|
@@ -893,6 +991,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
893
991
|
EXIT_CODE=0
|
|
894
992
|
AUTO_MERGED_PRS=""
|
|
895
993
|
AUTO_MERGE_FAILED_PRS=""
|
|
994
|
+
ACTUAL_REVIEWED_PRS=""
|
|
896
995
|
NO_CHANGES_PRS=""
|
|
897
996
|
MAX_WORKER_ATTEMPTS=1
|
|
898
997
|
MAX_WORKER_FINAL_SCORE=""
|
|
@@ -937,6 +1036,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
937
1036
|
worker_status=$(printf '%s' "${worker_result}" | sed -n 's/^NIGHT_WATCH_RESULT:\([^|]*\).*$/\1/p')
|
|
938
1037
|
worker_auto_merged=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merged=)[^|]+' || true)
|
|
939
1038
|
worker_auto_merge_failed=$(printf '%s' "${worker_result}" | grep -oP '(?<=auto_merge_failed=)[^|]+' || true)
|
|
1039
|
+
worker_reviewed_prs=$(printf '%s' "${worker_result}" | grep -oP '(?<=prs=)[^|]+' || true)
|
|
940
1040
|
worker_attempts=$(printf '%s' "${worker_result}" | grep -oP '(?<=attempts=)[^|]+' || true)
|
|
941
1041
|
worker_final_score=$(printf '%s' "${worker_result}" | grep -oP '(?<=final_score=)[^|]+' || true)
|
|
942
1042
|
worker_no_changes=$(printf '%s' "${worker_result}" | grep -oP '(?<=no_changes_needed=)[^|]+' || true)
|
|
@@ -944,6 +1044,9 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
944
1044
|
|
|
945
1045
|
AUTO_MERGED_PRS=$(append_csv "${AUTO_MERGED_PRS}" "${worker_auto_merged}")
|
|
946
1046
|
AUTO_MERGE_FAILED_PRS=$(append_csv "${AUTO_MERGE_FAILED_PRS}" "${worker_auto_merge_failed}")
|
|
1047
|
+
if [ "${worker_status}" = "success_reviewed" ]; then
|
|
1048
|
+
ACTUAL_REVIEWED_PRS=$(append_csv "${ACTUAL_REVIEWED_PRS}" "${worker_reviewed_prs}")
|
|
1049
|
+
fi
|
|
947
1050
|
NO_CHANGES_PRS=$(append_csv "${NO_CHANGES_PRS}" "${worker_no_changes_prs}")
|
|
948
1051
|
if [ -z "${worker_no_changes_prs}" ] && [ "${worker_no_changes}" = "1" ]; then
|
|
949
1052
|
NO_CHANGES_PRS=$(append_csv "${NO_CHANGES_PRS}" "#${worker_pr}")
|
|
@@ -988,7 +1091,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
988
1091
|
# worker runs may have left behind.
|
|
989
1092
|
cleanup_reviewer_worktrees
|
|
990
1093
|
|
|
991
|
-
emit_final_status "${EXIT_CODE}" "${
|
|
1094
|
+
emit_final_status "${EXIT_CODE}" "${ACTUAL_REVIEWED_PRS}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
|
|
992
1095
|
exit "${EXIT_CODE}"
|
|
993
1096
|
fi
|
|
994
1097
|
|
|
@@ -1077,6 +1180,7 @@ if [ -n "${TARGET_PR}" ]; then
|
|
|
1077
1180
|
TARGET_MERGE_STATE=$(gh pr view "${TARGET_PR}" --json mergeStateStatus --jq '.mergeStateStatus' 2>/dev/null || echo "UNKNOWN")
|
|
1078
1181
|
TARGET_FAILED_CHECKS=$(get_pr_failed_ci_summary "${TARGET_PR}")
|
|
1079
1182
|
TARGET_SCORE=$(get_pr_score "${TARGET_PR}")
|
|
1183
|
+
TARGET_PENDING_REVIEW_FEEDBACK=$(get_pr_pending_review_feedback_count "${TARGET_PR}")
|
|
1080
1184
|
|
|
1081
1185
|
TARGET_SCOPE_PROMPT+=$'\n## Preflight Data (from CLI)\n- mergeStateStatus: '"${TARGET_MERGE_STATE}"$'\n'
|
|
1082
1186
|
if [ -n "${TARGET_FAILED_CHECKS}" ]; then
|
|
@@ -1084,6 +1188,7 @@ if [ -n "${TARGET_PR}" ]; then
|
|
|
1084
1188
|
else
|
|
1085
1189
|
TARGET_SCOPE_PROMPT+=$'- failing checks: none detected\n'
|
|
1086
1190
|
fi
|
|
1191
|
+
TARGET_SCOPE_PROMPT+=$'- pending review feedback: '"${TARGET_PENDING_REVIEW_FEEDBACK}"$' item(s)\n'
|
|
1087
1192
|
if [ -n "${TARGET_SCORE}" ]; then
|
|
1088
1193
|
TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
|
|
1089
1194
|
TARGET_SCOPE_PROMPT+=$'- action: fix\n'
|
package/dist/templates/audit.md
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
You are the Night Watch Code Auditor. Your job is to scan the codebase for
|
|
1
|
+
You are the Night Watch Code Auditor. Your job is to scan the codebase for systemic engineering risks and write a consolidated architecture/code-quality audit report for human prioritization.
|
|
2
|
+
|
|
3
|
+
The default output is a broad report, not executor fodder. Do not create or recommend one board issue per finding unless an explicit "Board Issue Mode" section is appended to this prompt.
|
|
2
4
|
|
|
3
5
|
## What to look for
|
|
4
6
|
|
|
@@ -30,65 +32,97 @@ You are the Night Watch Code Auditor. Your job is to scan the codebase for real
|
|
|
30
32
|
|
|
31
33
|
- `node_modules/`, `dist/`, `.git/`, `coverage/`, generated files.
|
|
32
34
|
- Test files (`*.test.ts`, `*.spec.ts`, `__tests__/`) unless they expose production design flaws.
|
|
33
|
-
- Intentional no-op catches in file walkers/read-only probing paths (
|
|
35
|
+
- Intentional no-op catches in file walkers/read-only probing paths (for example, `catch { continue }`, `catch { return null }` when clearly harmless).
|
|
34
36
|
- Cosmetic style-only nits (formatting, naming preference, import order).
|
|
35
|
-
- Hypothetical principle violations without concrete impact.
|
|
37
|
+
- Hypothetical principle violations without concrete code evidence and impact.
|
|
36
38
|
|
|
37
39
|
## How to scan
|
|
38
40
|
|
|
39
41
|
Use file-reading/search tools and scan systematically, prioritizing:
|
|
40
42
|
|
|
41
|
-
- `src/`
|
|
42
|
-
- `scripts/`
|
|
43
|
+
- `src/` and package implementation directories.
|
|
44
|
+
- `scripts/` and automation/runtime shell paths.
|
|
45
|
+
- Shared configuration, scheduler, queue, provider, board, and command flows.
|
|
43
46
|
|
|
44
47
|
For each potential issue, verify:
|
|
45
48
|
|
|
46
49
|
1. It is real and actionable.
|
|
47
|
-
2. It has concrete impact
|
|
48
|
-
3. The
|
|
50
|
+
2. It has concrete impact on correctness, security, scalability, operability, or maintainability.
|
|
51
|
+
3. The affected locations show a pattern or systemic design problem, not just a tiny isolated nit.
|
|
52
|
+
4. The fix direction is useful for human planning.
|
|
53
|
+
|
|
54
|
+
## Priority model
|
|
55
|
+
|
|
56
|
+
Use an Effort x Impact priority model:
|
|
49
57
|
|
|
50
|
-
|
|
58
|
+
- **Impact**: critical, high, medium, low.
|
|
59
|
+
- **Effort**: small, medium, large.
|
|
60
|
+
- **Priority**: P0, P1, P2, P3.
|
|
51
61
|
|
|
52
|
-
|
|
53
|
-
- **high**: significant bug/risk with near-term impact.
|
|
54
|
-
- **medium**: clear risk/smell that should be addressed soon.
|
|
55
|
-
- **low**: valid but lower urgency.
|
|
62
|
+
Assign P0/P1 only when the issue is urgent or unlocks significant risk reduction. Be selective.
|
|
56
63
|
|
|
57
64
|
## Report format
|
|
58
65
|
|
|
59
|
-
Write
|
|
66
|
+
Write `logs/audit-report.md` using this format:
|
|
60
67
|
|
|
61
68
|
```markdown
|
|
62
|
-
# Code Audit
|
|
69
|
+
# Architecture and Code Quality Audit
|
|
63
70
|
|
|
64
71
|
Generated: <ISO timestamp>
|
|
65
72
|
|
|
66
|
-
##
|
|
73
|
+
## Executive Summary
|
|
67
74
|
|
|
68
|
-
|
|
75
|
+
One to three concise paragraphs covering the highest-risk themes, the likely cost of leaving them alone, and the recommended order of attack.
|
|
69
76
|
|
|
70
|
-
|
|
71
|
-
- **Severity**: critical | high | medium | low
|
|
72
|
-
- **Category**: empty_catch | critical_todo | hardcoded_secret | unhandled_promise | unsafe_assertion | scalability_hotspot | architecture_violation | srp_violation | dry_violation | kiss_violation | solid_violation | yagni_violation
|
|
73
|
-
- **Description**: What the issue is, why it matters, and concrete impact
|
|
74
|
-
- **Snippet**: `the offending code`
|
|
75
|
-
- **Suggested Fix**: Specific fix direction (minimal, pragmatic)
|
|
77
|
+
## Priority Matrix
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
| Priority | Theme | Impact | Effort | Why now |
|
|
80
|
+
| -------- | ------------ | ------ | ------ | -------------- |
|
|
81
|
+
| P1 | <theme name> | high | medium | <short reason> |
|
|
78
82
|
|
|
79
|
-
|
|
80
|
-
```
|
|
83
|
+
## Findings by Theme
|
|
81
84
|
|
|
82
|
-
|
|
85
|
+
### <Theme Name>
|
|
86
|
+
|
|
87
|
+
Impact: critical | high | medium | low
|
|
88
|
+
Effort: small | medium | large
|
|
89
|
+
Priority: P0 | P1 | P2 | P3
|
|
90
|
+
|
|
91
|
+
#### Evidence
|
|
92
|
+
|
|
93
|
+
- `<path>:<line>` - what is happening and why it matters.
|
|
94
|
+
- `<path>:<line>` - related evidence showing this is systemic.
|
|
95
|
+
|
|
96
|
+
#### Architecture or Quality Rule Violated
|
|
97
|
+
|
|
98
|
+
Name the concrete rule or boundary being violated.
|
|
99
|
+
|
|
100
|
+
#### Recommended Direction
|
|
101
|
+
|
|
102
|
+
Describe the pragmatic remediation path. Prefer grouped fixes and sequencing over tiny task breakdowns.
|
|
103
|
+
|
|
104
|
+
#### Full Violation List
|
|
105
|
+
|
|
106
|
+
- `<path>:<line>` - concise violation.
|
|
107
|
+
- `<path>:<line>` - concise violation.
|
|
108
|
+
|
|
109
|
+
## Cross-Cutting Recommendations
|
|
110
|
+
|
|
111
|
+
- <Recommendation that helps multiple themes>
|
|
112
|
+
|
|
113
|
+
## No-Issue Result
|
|
114
|
+
|
|
115
|
+
If there are no actionable systemic issues, write exactly:
|
|
83
116
|
|
|
84
|
-
```
|
|
85
117
|
NO_ISSUES_FOUND
|
|
86
118
|
```
|
|
87
119
|
|
|
88
120
|
## Rules
|
|
89
121
|
|
|
90
|
-
-
|
|
91
|
-
-
|
|
122
|
+
- Favor grouped systemic findings over granular one-off findings.
|
|
123
|
+
- Include a full violation list under each theme so humans can size and prioritize the work.
|
|
124
|
+
- Do not use `### Finding N` headings in default report mode.
|
|
125
|
+
- Do not create one issue per finding, and do not optimize the report for automatic execution.
|
|
126
|
+
- Report principle violations only when they create concrete risk.
|
|
92
127
|
- Avoid theoretical architecture criticism without code evidence.
|
|
93
|
-
- Be decisive: skip noisy false positives.
|
|
94
128
|
- After writing the report, stop. Do NOT open PRs, push code, or make changes.
|