@jonit-dev/night-watch-cli 1.8.8-beta.1 → 1.8.8-beta.10
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 +638 -24
- package/dist/cli.js.map +1 -1
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +38 -6
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/install.d.ts +4 -0
- package/dist/commands/install.d.ts.map +1 -1
- package/dist/commands/install.js +25 -0
- package/dist/commands/install.js.map +1 -1
- package/dist/commands/qa.d.ts.map +1 -1
- package/dist/commands/qa.js +2 -0
- package/dist/commands/qa.js.map +1 -1
- package/dist/commands/queue.d.ts.map +1 -1
- package/dist/commands/queue.js +27 -4
- package/dist/commands/queue.js.map +1 -1
- package/dist/commands/resolve.d.ts +26 -0
- package/dist/commands/resolve.d.ts.map +1 -0
- package/dist/commands/resolve.js +186 -0
- package/dist/commands/resolve.js.map +1 -0
- package/dist/commands/review.d.ts +5 -0
- package/dist/commands/review.d.ts.map +1 -1
- package/dist/commands/review.js +18 -5
- package/dist/commands/review.js.map +1 -1
- package/dist/commands/summary.d.ts +14 -0
- package/dist/commands/summary.d.ts.map +1 -0
- package/dist/commands/summary.js +193 -0
- package/dist/commands/summary.js.map +1 -0
- package/dist/commands/uninstall.d.ts.map +1 -1
- package/dist/commands/uninstall.js +14 -2
- package/dist/commands/uninstall.js.map +1 -1
- package/dist/scripts/night-watch-helpers.sh +10 -1
- package/dist/scripts/night-watch-pr-resolver-cron.sh +402 -0
- package/dist/scripts/night-watch-pr-reviewer-cron.sh +22 -5
- package/dist/scripts/night-watch-qa-cron.sh +30 -4
- package/dist/scripts/test-helpers.bats +45 -0
- package/dist/templates/night-watch-pr-reviewer.md +2 -1
- package/dist/templates/pr-reviewer.md +2 -1
- package/package.json +1 -1
|
@@ -589,6 +589,11 @@ cleanup_worktrees() {
|
|
|
589
589
|
local project_name
|
|
590
590
|
project_name=$(basename "${project_dir}")
|
|
591
591
|
|
|
592
|
+
# Clear stale worktree registrations first. This fixes cases where a
|
|
593
|
+
# worktree directory was deleted out-of-band (for example by an agent
|
|
594
|
+
# runtime crash), leaving Git thinking the branch is still in use.
|
|
595
|
+
git -C "${project_dir}" worktree prune >/dev/null 2>&1 || true
|
|
596
|
+
|
|
592
597
|
local match_token="${project_name}-nw"
|
|
593
598
|
if [ -n "${scope}" ]; then
|
|
594
599
|
match_token="${scope}"
|
|
@@ -602,6 +607,10 @@ cleanup_worktrees() {
|
|
|
602
607
|
log "CLEANUP: Removing leftover worktree ${wt}"
|
|
603
608
|
git -C "${project_dir}" worktree remove --force "${wt}" 2>/dev/null || true
|
|
604
609
|
done || true
|
|
610
|
+
|
|
611
|
+
# Prune again after removals so Git drops any admin entries left behind by
|
|
612
|
+
# force-removal or previously broken worktrees outside Night Watch naming.
|
|
613
|
+
git -C "${project_dir}" worktree prune >/dev/null 2>&1 || true
|
|
605
614
|
}
|
|
606
615
|
|
|
607
616
|
# Pick the best available ref for creating a new detached worktree.
|
|
@@ -1149,7 +1158,7 @@ dispatch_next_queued_job() {
|
|
|
1149
1158
|
log "QUEUE: Checking for pending jobs to dispatch"
|
|
1150
1159
|
|
|
1151
1160
|
# Call CLI to dispatch next job (this handles priority, expiration, and spawning)
|
|
1152
|
-
"${cli_bin}" queue dispatch --log "${LOG_FILE:-/dev/null}" 2>/dev/null || true
|
|
1161
|
+
"${cli_bin}" queue dispatch --project-dir "${PROJECT_DIR:-$(pwd)}" --log "${LOG_FILE:-/dev/null}" 2>/dev/null || true
|
|
1153
1162
|
}
|
|
1154
1163
|
|
|
1155
1164
|
complete_queued_job() {
|
|
@@ -0,0 +1,402 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Night Watch PR Resolver Cron Runner (project-agnostic)
|
|
5
|
+
# Usage: night-watch-pr-resolver-cron.sh /path/to/project
|
|
6
|
+
#
|
|
7
|
+
# NOTE: This script expects environment variables to be set by the caller.
|
|
8
|
+
# The Node.js CLI will inject config values via environment variables.
|
|
9
|
+
# Required env vars (with defaults shown):
|
|
10
|
+
# NW_PR_RESOLVER_MAX_RUNTIME=3600 - Maximum runtime in seconds (1 hour)
|
|
11
|
+
# NW_PROVIDER_CMD=claude - AI provider CLI to use (claude, codex, etc.)
|
|
12
|
+
# NW_DRY_RUN=0 - Set to 1 for dry-run mode (prints diagnostics only)
|
|
13
|
+
# NW_PR_RESOLVER_MAX_PRS_PER_RUN=0 - Max PRs to process per run (0 = unlimited)
|
|
14
|
+
# NW_PR_RESOLVER_PER_PR_TIMEOUT=600 - Per-PR AI timeout in seconds
|
|
15
|
+
# NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION=1 - Set to 1 to use AI for conflict resolution
|
|
16
|
+
# NW_PR_RESOLVER_AI_REVIEW_RESOLUTION=0 - Set to 1 to also address review comments
|
|
17
|
+
# NW_PR_RESOLVER_READY_LABEL=ready-to-merge - Label to add when PR is conflict-free
|
|
18
|
+
# NW_PR_RESOLVER_BRANCH_PATTERNS= - Comma-separated branch prefixes to filter (empty = all)
|
|
19
|
+
|
|
20
|
+
PROJECT_DIR="${1:?Usage: $0 /path/to/project}"
|
|
21
|
+
PROJECT_NAME=$(basename "${PROJECT_DIR}")
|
|
22
|
+
LOG_DIR="${PROJECT_DIR}/logs"
|
|
23
|
+
LOG_FILE="${LOG_DIR}/pr-resolver.log"
|
|
24
|
+
MAX_RUNTIME="${NW_PR_RESOLVER_MAX_RUNTIME:-3600}" # 1 hour
|
|
25
|
+
MAX_LOG_SIZE="524288" # 512 KB
|
|
26
|
+
PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
27
|
+
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
28
|
+
MAX_PRS_PER_RUN="${NW_PR_RESOLVER_MAX_PRS_PER_RUN:-0}"
|
|
29
|
+
PER_PR_TIMEOUT="${NW_PR_RESOLVER_PER_PR_TIMEOUT:-600}"
|
|
30
|
+
AI_CONFLICT_RESOLUTION="${NW_PR_RESOLVER_AI_CONFLICT_RESOLUTION:-1}"
|
|
31
|
+
AI_REVIEW_RESOLUTION="${NW_PR_RESOLVER_AI_REVIEW_RESOLUTION:-0}"
|
|
32
|
+
READY_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
|
|
33
|
+
BRANCH_PATTERNS_RAW="${NW_PR_RESOLVER_BRANCH_PATTERNS:-}"
|
|
34
|
+
SCRIPT_START_TIME=$(date +%s)
|
|
35
|
+
|
|
36
|
+
# Normalize numeric settings to safe ranges
|
|
37
|
+
if ! [[ "${MAX_PRS_PER_RUN}" =~ ^[0-9]+$ ]]; then
|
|
38
|
+
MAX_PRS_PER_RUN="0"
|
|
39
|
+
fi
|
|
40
|
+
if ! [[ "${PER_PR_TIMEOUT}" =~ ^[0-9]+$ ]]; then
|
|
41
|
+
PER_PR_TIMEOUT="600"
|
|
42
|
+
fi
|
|
43
|
+
if [ "${MAX_PRS_PER_RUN}" -gt 100 ]; then
|
|
44
|
+
MAX_PRS_PER_RUN="100"
|
|
45
|
+
fi
|
|
46
|
+
if [ "${PER_PR_TIMEOUT}" -gt 3600 ]; then
|
|
47
|
+
PER_PR_TIMEOUT="3600"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
mkdir -p "${LOG_DIR}"
|
|
51
|
+
|
|
52
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
53
|
+
# shellcheck source=night-watch-helpers.sh
|
|
54
|
+
source "${SCRIPT_DIR}/night-watch-helpers.sh"
|
|
55
|
+
|
|
56
|
+
# Ensure provider CLI is on PATH (nvm, fnm, volta, common bin dirs)
|
|
57
|
+
if ! ensure_provider_on_path "${PROVIDER_CMD}"; then
|
|
58
|
+
echo "ERROR: Provider '${PROVIDER_CMD}' not found in PATH or common installation locations" >&2
|
|
59
|
+
exit 127
|
|
60
|
+
fi
|
|
61
|
+
PROJECT_RUNTIME_KEY=$(project_runtime_key "${PROJECT_DIR}")
|
|
62
|
+
PROVIDER_MODEL_DISPLAY=$(resolve_provider_model_display "${PROVIDER_CMD}" "${PROVIDER_LABEL}")
|
|
63
|
+
# NOTE: Lock file path must match resolverLockPath() in src/utils/status-data.ts
|
|
64
|
+
LOCK_FILE="/tmp/night-watch-pr-resolver-${PROJECT_RUNTIME_KEY}.lock"
|
|
65
|
+
SCRIPT_TYPE="pr-resolver"
|
|
66
|
+
|
|
67
|
+
emit_result() {
|
|
68
|
+
local status="${1:?status required}"
|
|
69
|
+
local details="${2:-}"
|
|
70
|
+
if [ -n "${details}" ]; then
|
|
71
|
+
echo "NIGHT_WATCH_RESULT:${status}|${details}"
|
|
72
|
+
else
|
|
73
|
+
echo "NIGHT_WATCH_RESULT:${status}"
|
|
74
|
+
fi
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
78
|
+
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
79
|
+
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
80
|
+
if [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
|
|
81
|
+
arm_global_queue_cleanup
|
|
82
|
+
else
|
|
83
|
+
claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
|
|
84
|
+
fi
|
|
85
|
+
fi
|
|
86
|
+
# ──────────────────────────────────────────────────────────────────────────────
|
|
87
|
+
|
|
88
|
+
# PR discovery: returns JSON array of open PRs with required fields
|
|
89
|
+
discover_open_prs() {
|
|
90
|
+
gh pr list --state open \
|
|
91
|
+
--json number,title,headRefName,mergeable,isDraft,labels \
|
|
92
|
+
2>/dev/null || echo "[]"
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
# Check if a branch matches any configured branch prefix patterns.
|
|
96
|
+
# Returns 0 (match/pass) or 1 (no match, skip PR).
|
|
97
|
+
matches_branch_patterns() {
|
|
98
|
+
local branch="${1}"
|
|
99
|
+
if [ -z "${BRANCH_PATTERNS_RAW}" ]; then
|
|
100
|
+
return 0 # No filter configured = match all
|
|
101
|
+
fi
|
|
102
|
+
IFS=',' read -ra patterns <<< "${BRANCH_PATTERNS_RAW}"
|
|
103
|
+
for pattern in "${patterns[@]}"; do
|
|
104
|
+
pattern="${pattern# }" # trim leading space
|
|
105
|
+
if [[ "${branch}" == ${pattern}* ]]; then
|
|
106
|
+
return 0
|
|
107
|
+
fi
|
|
108
|
+
done
|
|
109
|
+
return 1
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
# Process a single PR: resolve conflicts and/or review comments, then label.
|
|
113
|
+
# Echoes "ready" if the PR ends up conflict-free, "conflicted" otherwise.
|
|
114
|
+
# Returns 0 on success, 1 on unrecoverable failure.
|
|
115
|
+
process_pr() {
|
|
116
|
+
local pr_number="${1:?pr_number required}"
|
|
117
|
+
local pr_branch="${2:?pr_branch required}"
|
|
118
|
+
local pr_title="${3:-}"
|
|
119
|
+
local worktree_dir="/tmp/nw-resolver-pr${pr_number}-$$"
|
|
120
|
+
|
|
121
|
+
log "INFO: Processing PR #${pr_number}: ${pr_title}" "branch=${pr_branch}"
|
|
122
|
+
|
|
123
|
+
# Inner cleanup for worktree created during this PR's processing
|
|
124
|
+
cleanup_pr_worktree() {
|
|
125
|
+
if git -C "${PROJECT_DIR}" worktree list --porcelain 2>/dev/null \
|
|
126
|
+
| grep -qF "worktree ${worktree_dir}"; then
|
|
127
|
+
git -C "${PROJECT_DIR}" worktree remove --force "${worktree_dir}" 2>/dev/null || true
|
|
128
|
+
fi
|
|
129
|
+
rm -rf "${worktree_dir}" 2>/dev/null || true
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
# ── Determine default branch ─────────────────────────────────────────────
|
|
133
|
+
local default_branch
|
|
134
|
+
default_branch="${NW_DEFAULT_BRANCH:-}"
|
|
135
|
+
if [ -z "${default_branch}" ]; then
|
|
136
|
+
default_branch=$(detect_default_branch "${PROJECT_DIR}")
|
|
137
|
+
fi
|
|
138
|
+
|
|
139
|
+
# ── Check current mergeable status ──────────────────────────────────────
|
|
140
|
+
local mergeable
|
|
141
|
+
mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
|
|
142
|
+
|
|
143
|
+
if [ "${mergeable}" = "CONFLICTING" ]; then
|
|
144
|
+
log "INFO: PR #${pr_number} has conflicts, attempting resolution" "branch=${pr_branch}"
|
|
145
|
+
|
|
146
|
+
# Fetch the PR branch so we have an up-to-date ref
|
|
147
|
+
git -C "${PROJECT_DIR}" fetch --quiet origin "${pr_branch}" 2>/dev/null || true
|
|
148
|
+
|
|
149
|
+
# Create an isolated worktree on the PR branch
|
|
150
|
+
if ! prepare_branch_worktree "${PROJECT_DIR}" "${worktree_dir}" "${pr_branch}" "${default_branch}" "${LOG_FILE}"; then
|
|
151
|
+
log "WARN: Failed to create worktree for PR #${pr_number}" "branch=${pr_branch}"
|
|
152
|
+
cleanup_pr_worktree
|
|
153
|
+
return 1
|
|
154
|
+
fi
|
|
155
|
+
|
|
156
|
+
local rebase_success=0
|
|
157
|
+
|
|
158
|
+
# Attempt a clean rebase first (no AI needed if it auto-resolves)
|
|
159
|
+
if git -C "${worktree_dir}" rebase "origin/${default_branch}" --quiet 2>/dev/null; then
|
|
160
|
+
rebase_success=1
|
|
161
|
+
log "INFO: PR #${pr_number} rebased cleanly (no conflicts)" "branch=${pr_branch}"
|
|
162
|
+
else
|
|
163
|
+
# Clean up the failed rebase state
|
|
164
|
+
git -C "${worktree_dir}" rebase --abort 2>/dev/null || true
|
|
165
|
+
|
|
166
|
+
if [ "${AI_CONFLICT_RESOLUTION}" = "1" ]; then
|
|
167
|
+
log "INFO: Invoking AI to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
168
|
+
|
|
169
|
+
local ai_prompt
|
|
170
|
+
ai_prompt="You are working in a git repository at ${worktree_dir}. \
|
|
171
|
+
Branch '${pr_branch}' has merge conflicts with '${default_branch}'. \
|
|
172
|
+
Please resolve the merge conflicts by: \
|
|
173
|
+
1) Running: git rebase origin/${default_branch} \
|
|
174
|
+
2) Resolving any conflict markers in the affected files \
|
|
175
|
+
3) Staging resolved files with: git add <files> \
|
|
176
|
+
4) Continuing the rebase with: git rebase --continue \
|
|
177
|
+
5) Finally pushing with: git push --force-with-lease origin ${pr_branch} \
|
|
178
|
+
Work exclusively in the directory: ${worktree_dir}"
|
|
179
|
+
|
|
180
|
+
local -a cmd_parts
|
|
181
|
+
mapfile -d '' -t cmd_parts < <(build_provider_cmd "${worktree_dir}" "${ai_prompt}")
|
|
182
|
+
|
|
183
|
+
if timeout "${PER_PR_TIMEOUT}" "${cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
|
|
184
|
+
rebase_success=1
|
|
185
|
+
log "INFO: AI resolved conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
186
|
+
else
|
|
187
|
+
log "WARN: AI failed to resolve conflicts for PR #${pr_number}" "branch=${pr_branch}"
|
|
188
|
+
cleanup_pr_worktree
|
|
189
|
+
return 1
|
|
190
|
+
fi
|
|
191
|
+
else
|
|
192
|
+
log "WARN: Skipping PR #${pr_number} — conflicts exist and AI resolution is disabled" "branch=${pr_branch}"
|
|
193
|
+
cleanup_pr_worktree
|
|
194
|
+
return 1
|
|
195
|
+
fi
|
|
196
|
+
fi
|
|
197
|
+
|
|
198
|
+
if [ "${rebase_success}" = "1" ]; then
|
|
199
|
+
# Safety: never force-push to the default branch
|
|
200
|
+
if [ "${pr_branch}" = "${default_branch}" ]; then
|
|
201
|
+
log "WARN: Refusing to force-push to default branch ${default_branch} for PR #${pr_number}"
|
|
202
|
+
cleanup_pr_worktree
|
|
203
|
+
return 1
|
|
204
|
+
fi
|
|
205
|
+
# Push the rebased branch (AI may have already pushed; --force-with-lease is idempotent)
|
|
206
|
+
git -C "${worktree_dir}" push --force-with-lease origin "${pr_branch}" >> "${LOG_FILE}" 2>&1 || {
|
|
207
|
+
log "WARN: Push after rebase failed for PR #${pr_number}" "branch=${pr_branch}"
|
|
208
|
+
}
|
|
209
|
+
fi
|
|
210
|
+
fi
|
|
211
|
+
|
|
212
|
+
# ── Secondary: AI review comment resolution (opt-in) ────────────────────
|
|
213
|
+
if [ "${AI_REVIEW_RESOLUTION}" = "1" ]; then
|
|
214
|
+
local unresolved_count
|
|
215
|
+
unresolved_count=$(gh api "repos/{owner}/{repo}/pulls/${pr_number}/reviews" \
|
|
216
|
+
--jq '[.[] | select(.state == "CHANGES_REQUESTED")] | length' 2>/dev/null || echo "0")
|
|
217
|
+
|
|
218
|
+
if [ "${unresolved_count}" -gt "0" ]; then
|
|
219
|
+
log "INFO: PR #${pr_number} has ${unresolved_count} change request(s), invoking AI" "branch=${pr_branch}"
|
|
220
|
+
|
|
221
|
+
local review_workdir="${worktree_dir}"
|
|
222
|
+
if [ ! -d "${review_workdir}" ]; then
|
|
223
|
+
review_workdir="${PROJECT_DIR}"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
local review_prompt
|
|
227
|
+
review_prompt="You are working in the git repository at ${review_workdir}. \
|
|
228
|
+
PR #${pr_number} on branch '${pr_branch}' has unresolved review comments requesting changes. \
|
|
229
|
+
Please: \
|
|
230
|
+
1) Run 'gh pr view ${pr_number} --comments' to read the review comments \
|
|
231
|
+
2) Implement the requested changes \
|
|
232
|
+
3) Commit the changes with a descriptive message \
|
|
233
|
+
4) Push with: git push origin ${pr_branch} \
|
|
234
|
+
Work in the directory: ${review_workdir}"
|
|
235
|
+
|
|
236
|
+
local -a review_cmd_parts
|
|
237
|
+
mapfile -d '' -t review_cmd_parts < <(build_provider_cmd "${review_workdir}" "${review_prompt}")
|
|
238
|
+
|
|
239
|
+
if timeout "${PER_PR_TIMEOUT}" "${review_cmd_parts[@]}" >> "${LOG_FILE}" 2>&1; then
|
|
240
|
+
log "INFO: AI addressed review comments for PR #${pr_number}" "branch=${pr_branch}"
|
|
241
|
+
else
|
|
242
|
+
log "WARN: AI failed to address review comments for PR #${pr_number}" "branch=${pr_branch}"
|
|
243
|
+
fi
|
|
244
|
+
fi
|
|
245
|
+
fi
|
|
246
|
+
|
|
247
|
+
# ── Re-check mergeable status after processing ──────────────────────────
|
|
248
|
+
# Brief wait for GitHub to propagate the push and recompute mergeability
|
|
249
|
+
sleep 3
|
|
250
|
+
local final_mergeable
|
|
251
|
+
final_mergeable=$(gh pr view "${pr_number}" --json mergeable --jq '.mergeable' 2>/dev/null || echo "UNKNOWN")
|
|
252
|
+
|
|
253
|
+
# ── Labeling ─────────────────────────────────────────────────────────────
|
|
254
|
+
local result
|
|
255
|
+
if [ "${final_mergeable}" != "CONFLICTING" ]; then
|
|
256
|
+
# Ensure the ready label exists in the repo (idempotent)
|
|
257
|
+
gh label create "${READY_LABEL}" \
|
|
258
|
+
--color "0075ca" \
|
|
259
|
+
--description "PR is conflict-free and ready to merge" \
|
|
260
|
+
2>/dev/null || true
|
|
261
|
+
gh pr edit "${pr_number}" --add-label "${READY_LABEL}" 2>/dev/null || true
|
|
262
|
+
log "INFO: PR #${pr_number} marked as '${READY_LABEL}'" "branch=${pr_branch}"
|
|
263
|
+
result="ready"
|
|
264
|
+
else
|
|
265
|
+
gh pr edit "${pr_number}" --remove-label "${READY_LABEL}" 2>/dev/null || true
|
|
266
|
+
log "WARN: PR #${pr_number} still has conflicts after processing" "branch=${pr_branch}"
|
|
267
|
+
result="conflicted"
|
|
268
|
+
fi
|
|
269
|
+
|
|
270
|
+
cleanup_pr_worktree
|
|
271
|
+
echo "${result}"
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
# ── Validate provider ────────────────────────────────────────────────────────
|
|
275
|
+
if ! validate_provider "${PROVIDER_CMD}"; then
|
|
276
|
+
echo "ERROR: Unknown provider: ${PROVIDER_CMD}" >&2
|
|
277
|
+
exit 1
|
|
278
|
+
fi
|
|
279
|
+
|
|
280
|
+
rotate_log
|
|
281
|
+
log_separator
|
|
282
|
+
log "RUN-START: pr-resolver invoked project=${PROJECT_DIR} provider=${PROVIDER_CMD} dry_run=${NW_DRY_RUN:-0}"
|
|
283
|
+
log "CONFIG: max_runtime=${MAX_RUNTIME}s max_prs=${MAX_PRS_PER_RUN} per_pr_timeout=${PER_PR_TIMEOUT}s ai_conflict=${AI_CONFLICT_RESOLUTION} ai_review=${AI_REVIEW_RESOLUTION} ready_label=${READY_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
|
|
284
|
+
|
|
285
|
+
if ! acquire_lock "${LOCK_FILE}"; then
|
|
286
|
+
emit_result "skip_locked"
|
|
287
|
+
exit 0
|
|
288
|
+
fi
|
|
289
|
+
|
|
290
|
+
cd "${PROJECT_DIR}"
|
|
291
|
+
|
|
292
|
+
# ── Dry-run mode ────────────────────────────────────────────────────────────
|
|
293
|
+
if [ "${NW_DRY_RUN:-0}" = "1" ]; then
|
|
294
|
+
echo "=== Dry Run: PR Resolver ==="
|
|
295
|
+
echo "Provider (model): ${PROVIDER_MODEL_DISPLAY}"
|
|
296
|
+
echo "Branch Patterns: ${BRANCH_PATTERNS_RAW:-<all>}"
|
|
297
|
+
echo "Max PRs Per Run: ${MAX_PRS_PER_RUN}"
|
|
298
|
+
echo "Per-PR Timeout: ${PER_PR_TIMEOUT}s"
|
|
299
|
+
echo "AI Conflict Resolution: ${AI_CONFLICT_RESOLUTION}"
|
|
300
|
+
echo "AI Review Resolution: ${AI_REVIEW_RESOLUTION}"
|
|
301
|
+
echo "Ready Label: ${READY_LABEL}"
|
|
302
|
+
echo "Max Runtime: ${MAX_RUNTIME}s"
|
|
303
|
+
log "INFO: Dry run mode — exiting without processing"
|
|
304
|
+
emit_result "skip_dry_run"
|
|
305
|
+
exit 0
|
|
306
|
+
fi
|
|
307
|
+
|
|
308
|
+
send_telegram_status_message "Night Watch PR Resolver: started" "Project: ${PROJECT_NAME}
|
|
309
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
310
|
+
Branch patterns: ${BRANCH_PATTERNS_RAW:-all}
|
|
311
|
+
Action: scanning open PRs for merge conflicts."
|
|
312
|
+
|
|
313
|
+
# ── Discover open PRs ────────────────────────────────────────────────────────
|
|
314
|
+
pr_json=$(discover_open_prs)
|
|
315
|
+
|
|
316
|
+
if [ -z "${pr_json}" ] || [ "${pr_json}" = "[]" ]; then
|
|
317
|
+
log "SKIP: No open PRs found"
|
|
318
|
+
send_telegram_status_message "Night Watch PR Resolver: nothing to do" "Project: ${PROJECT_NAME}
|
|
319
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
320
|
+
Result: no open PRs found."
|
|
321
|
+
emit_result "skip_no_open_prs"
|
|
322
|
+
exit 0
|
|
323
|
+
fi
|
|
324
|
+
|
|
325
|
+
pr_count=$(printf '%s' "${pr_json}" | jq 'length' 2>/dev/null || echo "0")
|
|
326
|
+
log "INFO: Found ${pr_count} open PR(s) to evaluate"
|
|
327
|
+
|
|
328
|
+
# ── Main processing loop ─────────────────────────────────────────────────────
|
|
329
|
+
processed=0
|
|
330
|
+
conflicts_resolved=0
|
|
331
|
+
reviews_addressed=0
|
|
332
|
+
prs_ready=0
|
|
333
|
+
prs_failed=0
|
|
334
|
+
|
|
335
|
+
while IFS= read -r pr_line; do
|
|
336
|
+
[ -z "${pr_line}" ] && continue
|
|
337
|
+
|
|
338
|
+
pr_number=$(printf '%s' "${pr_line}" | jq -r '.number')
|
|
339
|
+
pr_branch=$(printf '%s' "${pr_line}" | jq -r '.headRefName')
|
|
340
|
+
pr_title=$(printf '%s' "${pr_line}" | jq -r '.title')
|
|
341
|
+
is_draft=$(printf '%s' "${pr_line}" | jq -r '.isDraft')
|
|
342
|
+
labels=$(printf '%s' "${pr_line}" | jq -r '[.labels[].name] | join(",")')
|
|
343
|
+
|
|
344
|
+
[ -z "${pr_number}" ] || [ -z "${pr_branch}" ] && continue
|
|
345
|
+
|
|
346
|
+
# Skip draft PRs
|
|
347
|
+
if [ "${is_draft}" = "true" ]; then
|
|
348
|
+
log "INFO: Skipping draft PR #${pr_number}" "branch=${pr_branch}"
|
|
349
|
+
continue
|
|
350
|
+
fi
|
|
351
|
+
|
|
352
|
+
# Skip PRs labelled skip-resolver
|
|
353
|
+
if [[ "${labels}" == *"skip-resolver"* ]]; then
|
|
354
|
+
log "INFO: Skipping PR #${pr_number} (skip-resolver label)" "branch=${pr_branch}"
|
|
355
|
+
continue
|
|
356
|
+
fi
|
|
357
|
+
|
|
358
|
+
# Apply branch pattern filter
|
|
359
|
+
if ! matches_branch_patterns "${pr_branch}"; then
|
|
360
|
+
log "DEBUG: Skipping PR #${pr_number} — branch '${pr_branch}' does not match patterns" "patterns=${BRANCH_PATTERNS_RAW}"
|
|
361
|
+
continue
|
|
362
|
+
fi
|
|
363
|
+
|
|
364
|
+
# Enforce max PRs per run
|
|
365
|
+
if [ "${MAX_PRS_PER_RUN}" -gt "0" ] && [ "${processed}" -ge "${MAX_PRS_PER_RUN}" ]; then
|
|
366
|
+
log "INFO: Reached max PRs per run (${MAX_PRS_PER_RUN}), stopping"
|
|
367
|
+
break
|
|
368
|
+
fi
|
|
369
|
+
|
|
370
|
+
# Enforce global timeout
|
|
371
|
+
elapsed=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
372
|
+
if [ "${elapsed}" -ge "${MAX_RUNTIME}" ]; then
|
|
373
|
+
log "WARN: Global timeout reached (${MAX_RUNTIME}s), stopping early"
|
|
374
|
+
break
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
processed=$(( processed + 1 ))
|
|
378
|
+
|
|
379
|
+
result=""
|
|
380
|
+
if result=$(process_pr "${pr_number}" "${pr_branch}" "${pr_title}" 2>&1); then
|
|
381
|
+
# process_pr echoes "ready" or "conflicted" on the last line; extract it
|
|
382
|
+
last_line=$(printf '%s' "${result}" | tail -1)
|
|
383
|
+
if [ "${last_line}" = "ready" ]; then
|
|
384
|
+
prs_ready=$(( prs_ready + 1 ))
|
|
385
|
+
conflicts_resolved=$(( conflicts_resolved + 1 ))
|
|
386
|
+
fi
|
|
387
|
+
else
|
|
388
|
+
prs_failed=$(( prs_failed + 1 ))
|
|
389
|
+
fi
|
|
390
|
+
|
|
391
|
+
done < <(printf '%s' "${pr_json}" | jq -c '.[]')
|
|
392
|
+
|
|
393
|
+
log "RUN-END: pr-resolver complete processed=${processed} conflicts_resolved=${conflicts_resolved} prs_ready=${prs_ready} prs_failed=${prs_failed}"
|
|
394
|
+
|
|
395
|
+
send_telegram_status_message "Night Watch PR Resolver: completed" "Project: ${PROJECT_NAME}
|
|
396
|
+
Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
397
|
+
PRs processed: ${processed}
|
|
398
|
+
Conflicts resolved: ${conflicts_resolved}
|
|
399
|
+
PRs marked '${READY_LABEL}': ${prs_ready}
|
|
400
|
+
PRs failed: ${prs_failed}"
|
|
401
|
+
|
|
402
|
+
emit_result "success" "prs_processed=${processed}|conflicts_resolved=${conflicts_resolved}|reviews_addressed=${reviews_addressed}|prs_ready=${prs_ready}|prs_failed=${prs_failed}"
|
|
@@ -105,7 +105,9 @@ extract_review_score_from_text() {
|
|
|
105
105
|
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
106
106
|
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
107
107
|
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
108
|
-
if [ "${
|
|
108
|
+
if [ "${NW_QUEUE_INHERITED_SLOT:-0}" = "1" ]; then
|
|
109
|
+
:
|
|
110
|
+
elif [ "${NW_QUEUE_DISPATCHED:-0}" = "1" ]; then
|
|
109
111
|
arm_global_queue_cleanup
|
|
110
112
|
else
|
|
111
113
|
claim_or_enqueue "${SCRIPT_TYPE}" "${PROJECT_DIR}"
|
|
@@ -664,7 +666,11 @@ while IFS=$'\t' read -r pr_number pr_branch pr_labels; do
|
|
|
664
666
|
} | awk '!seen[$0]++'
|
|
665
667
|
)
|
|
666
668
|
LATEST_SCORE=$(extract_review_score_from_text "${ALL_COMMENTS}")
|
|
667
|
-
if [ -
|
|
669
|
+
if [ -z "${LATEST_SCORE}" ]; then
|
|
670
|
+
log "INFO: PR #${pr_number} (${pr_branch}) has no review score yet — needs initial review"
|
|
671
|
+
NEEDS_WORK=1
|
|
672
|
+
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
673
|
+
elif [ "${LATEST_SCORE}" -lt "${MIN_REVIEW_SCORE}" ]; then
|
|
668
674
|
log "INFO: PR #${pr_number} (${pr_branch}) has review score ${LATEST_SCORE}/100 (threshold: ${MIN_REVIEW_SCORE})"
|
|
669
675
|
NEEDS_WORK=1
|
|
670
676
|
PRS_NEEDING_WORK="${PRS_NEEDING_WORK} #${pr_number}"
|
|
@@ -675,7 +681,7 @@ done < <(
|
|
|
675
681
|
)
|
|
676
682
|
|
|
677
683
|
if [ "${NEEDS_WORK}" -eq 0 ]; then
|
|
678
|
-
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}
|
|
684
|
+
log "SKIP: All ${OPEN_PRS} open PR(s) have passing CI and review score >= ${MIN_REVIEW_SCORE}"
|
|
679
685
|
|
|
680
686
|
# ── Auto-merge eligible PRs ───────────────────────────────
|
|
681
687
|
if [ "${NW_AUTO_MERGE:-0}" = "1" ]; then
|
|
@@ -814,6 +820,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
814
820
|
NW_TARGET_PR="${pr_number}" \
|
|
815
821
|
NW_REVIEWER_WORKER_MODE="1" \
|
|
816
822
|
NW_REVIEWER_PARALLEL="0" \
|
|
823
|
+
NW_QUEUE_INHERITED_SLOT="1" \
|
|
817
824
|
bash "${SCRIPT_DIR}/night-watch-pr-reviewer-cron.sh" "${PROJECT_DIR}" > "${worker_output}" 2>&1
|
|
818
825
|
) &
|
|
819
826
|
|
|
@@ -922,7 +929,7 @@ if [ -z "${TARGET_PR}" ] && [ "${WORKER_MODE}" != "1" ] && [ "${PARALLEL_ENABLED
|
|
|
922
929
|
cleanup_reviewer_worktrees
|
|
923
930
|
|
|
924
931
|
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${MAX_WORKER_ATTEMPTS}" "${MAX_WORKER_FINAL_SCORE}" "0" "${NO_CHANGES_PRS}"
|
|
925
|
-
exit
|
|
932
|
+
exit "${EXIT_CODE}"
|
|
926
933
|
fi
|
|
927
934
|
|
|
928
935
|
REVIEW_RUN_TOKEN="${PROJECT_RUNTIME_KEY}-$$"
|
|
@@ -1015,8 +1022,16 @@ if [ -n "${TARGET_PR}" ]; then
|
|
|
1015
1022
|
fi
|
|
1016
1023
|
if [ -n "${TARGET_SCORE}" ]; then
|
|
1017
1024
|
TARGET_SCOPE_PROMPT+=$'- latest review score: '"${TARGET_SCORE}"$'/100\n'
|
|
1025
|
+
TARGET_SCOPE_PROMPT+=$'- action: fix\n'
|
|
1026
|
+
# Inject the latest review comment body for the fix prompt
|
|
1027
|
+
REVIEW_BODY=$(gh api "repos/$(gh repo view --json nameWithOwner --jq '.nameWithOwner' 2>/dev/null)/issues/${TARGET_PR}/comments" --jq '[.[] | select(.body | test("Overall Score|Score:.*[0-9]+/100"))] | last | .body // ""' 2>/dev/null || echo "")
|
|
1028
|
+
if [ -n "${REVIEW_BODY}" ]; then
|
|
1029
|
+
TRUNCATED_REVIEW=$(printf '%s' "${REVIEW_BODY}" | head -c 6000)
|
|
1030
|
+
TARGET_SCOPE_PROMPT+=$'\n## Latest Review Feedback\n'"${TRUNCATED_REVIEW}"$'\n'
|
|
1031
|
+
fi
|
|
1018
1032
|
else
|
|
1019
1033
|
TARGET_SCOPE_PROMPT+=$'- latest review score: not found\n'
|
|
1034
|
+
TARGET_SCOPE_PROMPT+=$'- action: review\n'
|
|
1020
1035
|
fi
|
|
1021
1036
|
fi
|
|
1022
1037
|
|
|
@@ -1201,7 +1216,8 @@ for ATTEMPT in $(seq 1 "${TOTAL_ATTEMPTS}"); do
|
|
|
1201
1216
|
fi
|
|
1202
1217
|
continue
|
|
1203
1218
|
fi
|
|
1204
|
-
log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; failing run."
|
|
1219
|
+
log "RETRY: No review score found for PR #${TARGET_PR} after ${TOTAL_ATTEMPTS} attempts; labeling needs-human-review and failing run."
|
|
1220
|
+
gh pr edit "${TARGET_PR}" --add-label "needs-human-review" 2>/dev/null || true
|
|
1205
1221
|
EXIT_CODE=1
|
|
1206
1222
|
break
|
|
1207
1223
|
fi
|
|
@@ -1316,3 +1332,4 @@ fi
|
|
|
1316
1332
|
REVIEWER_TOTAL_ELAPSED=$(( $(date +%s) - SCRIPT_START_TIME ))
|
|
1317
1333
|
log "OUTCOME: exit_code=${EXIT_CODE} total_elapsed=${REVIEWER_TOTAL_ELAPSED}s prs=${PRS_NEEDING_WORK_CSV:-none} attempts=${ATTEMPTS_MADE}"
|
|
1318
1334
|
emit_final_status "${EXIT_CODE}" "${PRS_NEEDING_WORK_CSV}" "${AUTO_MERGED_PRS}" "${AUTO_MERGE_FAILED_PRS}" "${ATTEMPTS_MADE}" "${FINAL_SCORE}" "${NO_CHANGES_NEEDED}" "${NO_CHANGES_PRS}"
|
|
1335
|
+
exit "${EXIT_CODE}"
|
|
@@ -25,6 +25,7 @@ PROVIDER_CMD="${NW_PROVIDER_CMD:-claude}"
|
|
|
25
25
|
PROVIDER_LABEL="${NW_PROVIDER_LABEL:-}"
|
|
26
26
|
BRANCH_PATTERNS_RAW="${NW_BRANCH_PATTERNS:-feat/,night-watch/}"
|
|
27
27
|
SKIP_LABEL="${NW_QA_SKIP_LABEL:-skip-qa}"
|
|
28
|
+
VALIDATED_LABEL="${NW_QA_VALIDATED_LABEL:-e2e-validated}"
|
|
28
29
|
QA_ARTIFACTS="${NW_QA_ARTIFACTS:-both}"
|
|
29
30
|
QA_AUTO_INSTALL_PLAYWRIGHT="${NW_QA_AUTO_INSTALL_PLAYWRIGHT:-1}"
|
|
30
31
|
SCRIPT_START_TIME=$(date +%s)
|
|
@@ -55,6 +56,16 @@ emit_result() {
|
|
|
55
56
|
fi
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
LABEL_ENSURED=0
|
|
60
|
+
ensure_validated_label() {
|
|
61
|
+
if [ "${LABEL_ENSURED}" -eq 1 ]; then return 0; fi
|
|
62
|
+
gh label create "${VALIDATED_LABEL}" \
|
|
63
|
+
--description "PR acceptance requirements validated by e2e/integration tests" \
|
|
64
|
+
--color "0e8a16" \
|
|
65
|
+
--force 2>/dev/null || true
|
|
66
|
+
LABEL_ENSURED=1
|
|
67
|
+
}
|
|
68
|
+
|
|
58
69
|
# ── Global Job Queue Gate ────────────────────────────────────────────────────
|
|
59
70
|
# Atomically claim a DB slot or enqueue for later dispatch — no flock needed.
|
|
60
71
|
if [ "${NW_QUEUE_ENABLED:-0}" = "1" ]; then
|
|
@@ -487,6 +498,7 @@ fi
|
|
|
487
498
|
EXIT_CODE=0
|
|
488
499
|
PROCESSED_PRS_CSV=""
|
|
489
500
|
PASSING_PRS_CSV=""
|
|
501
|
+
VALIDATED_PRS_CSV=""
|
|
490
502
|
ISSUES_FOUND_PRS_CSV=""
|
|
491
503
|
NO_TESTS_PRS_CSV=""
|
|
492
504
|
UNCLASSIFIED_PRS_CSV=""
|
|
@@ -617,12 +629,23 @@ for pr_ref in ${PRS_NEEDING_QA}; do
|
|
|
617
629
|
case "${QA_OUTCOME}" in
|
|
618
630
|
passing)
|
|
619
631
|
PASSING_PRS_CSV=$(append_csv "${PASSING_PRS_CSV}" "#${pr_num}")
|
|
632
|
+
# Apply e2e-validated label
|
|
633
|
+
ensure_validated_label
|
|
634
|
+
gh pr edit "${pr_num}" --add-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
635
|
+
VALIDATED_PRS_CSV=$(append_csv "${VALIDATED_PRS_CSV}" "#${pr_num}")
|
|
636
|
+
log "QA: PR #${pr_num} — added '${VALIDATED_LABEL}' label (tests passing)"
|
|
620
637
|
;;
|
|
621
638
|
issues_found)
|
|
622
639
|
ISSUES_FOUND_PRS_CSV=$(append_csv "${ISSUES_FOUND_PRS_CSV}" "#${pr_num}")
|
|
640
|
+
# Remove e2e-validated label if present
|
|
641
|
+
gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
642
|
+
log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (issues found)"
|
|
623
643
|
;;
|
|
624
644
|
no_tests_needed)
|
|
625
645
|
NO_TESTS_PRS_CSV=$(append_csv "${NO_TESTS_PRS_CSV}" "#${pr_num}")
|
|
646
|
+
# Remove e2e-validated label — no tests doesn't prove acceptance
|
|
647
|
+
gh pr edit "${pr_num}" --remove-label "${VALIDATED_LABEL}" 2>/dev/null || true
|
|
648
|
+
log "QA: PR #${pr_num} — removed '${VALIDATED_LABEL}' label (no tests needed)"
|
|
626
649
|
;;
|
|
627
650
|
*)
|
|
628
651
|
UNCLASSIFIED_PRS_CSV=$(append_csv "${UNCLASSIFIED_PRS_CSV}" "#${pr_num}")
|
|
@@ -646,6 +669,7 @@ cleanup_worktrees "${PROJECT_DIR}"
|
|
|
646
669
|
|
|
647
670
|
FINAL_PROCESSED_PRS_CSV="${PROCESSED_PRS_CSV:-${PRS_NEEDING_QA_CSV}}"
|
|
648
671
|
PASSING_PRS_SUMMARY=$(csv_or_none "${PASSING_PRS_CSV}")
|
|
672
|
+
VALIDATED_PRS_SUMMARY=$(csv_or_none "${VALIDATED_PRS_CSV}")
|
|
649
673
|
ISSUES_FOUND_PRS_SUMMARY=$(csv_or_none "${ISSUES_FOUND_PRS_CSV}")
|
|
650
674
|
NO_TESTS_PRS_SUMMARY=$(csv_or_none "${NO_TESTS_PRS_CSV}")
|
|
651
675
|
UNCLASSIFIED_PRS_SUMMARY=$(csv_or_none "${UNCLASSIFIED_PRS_CSV}")
|
|
@@ -664,6 +688,7 @@ Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
|
664
688
|
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
665
689
|
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
666
690
|
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
691
|
+
E2E validated: ${VALIDATED_PRS_SUMMARY}
|
|
667
692
|
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
668
693
|
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
669
694
|
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}
|
|
@@ -680,9 +705,9 @@ ${QA_SCREENSHOT_SUMMARY}"
|
|
|
680
705
|
fi
|
|
681
706
|
send_telegram_status_message "🧪 Night Watch QA: warning" "${TELEGRAM_WARNING_BODY}"
|
|
682
707
|
if [ -n "${REPO}" ]; then
|
|
683
|
-
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}|repo=${REPO}"
|
|
708
|
+
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}|repo=${REPO}"
|
|
684
709
|
else
|
|
685
|
-
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}"
|
|
710
|
+
emit_result "warning_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|warnings=${WARNING_PRS_SUMMARY}"
|
|
686
711
|
fi
|
|
687
712
|
else
|
|
688
713
|
log "DONE: QA runner completed successfully"
|
|
@@ -691,6 +716,7 @@ Provider (model): ${PROVIDER_MODEL_DISPLAY}
|
|
|
691
716
|
Artifacts: ${QA_ARTIFACTS_DESC} (mode=${QA_ARTIFACTS})
|
|
692
717
|
Processed PRs: ${FINAL_PROCESSED_PRS_CSV}
|
|
693
718
|
Passing tests: ${PASSING_PRS_SUMMARY}
|
|
719
|
+
E2E validated: ${VALIDATED_PRS_SUMMARY}
|
|
694
720
|
Issues found by tests: ${ISSUES_FOUND_PRS_SUMMARY}
|
|
695
721
|
No tests needed: ${NO_TESTS_PRS_SUMMARY}
|
|
696
722
|
Reported (unclassified): ${UNCLASSIFIED_PRS_SUMMARY}"
|
|
@@ -701,9 +727,9 @@ ${QA_SCREENSHOT_SUMMARY}"
|
|
|
701
727
|
fi
|
|
702
728
|
send_telegram_status_message "🧪 Night Watch QA: completed" "${TELEGRAM_SUCCESS_BODY}"
|
|
703
729
|
if [ -n "${REPO}" ]; then
|
|
704
|
-
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
|
|
730
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}|repo=${REPO}"
|
|
705
731
|
else
|
|
706
|
-
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
|
|
732
|
+
emit_result "success_qa" "prs=${FINAL_PROCESSED_PRS_CSV}|passing=${PASSING_PRS_SUMMARY}|validated=${VALIDATED_PRS_SUMMARY}|issues=${ISSUES_FOUND_PRS_SUMMARY}|no_tests=${NO_TESTS_PRS_SUMMARY}|unclassified=${UNCLASSIFIED_PRS_SUMMARY}"
|
|
707
733
|
fi
|
|
708
734
|
fi
|
|
709
735
|
elif [ ${EXIT_CODE} -eq 124 ]; then
|
|
@@ -75,3 +75,48 @@ teardown() {
|
|
|
75
75
|
|
|
76
76
|
[ "${result}" = "02-test-prd.md" ]
|
|
77
77
|
}
|
|
78
|
+
|
|
79
|
+
# ── pr-resolver lock acquisition ─────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
@test "pr-resolver lock acquisition: acquire_lock succeeds when no lock exists" {
|
|
82
|
+
local test_lock="/tmp/nw-test-resolver-$$.lock"
|
|
83
|
+
|
|
84
|
+
# Ensure clean state
|
|
85
|
+
rm -f "${test_lock}"
|
|
86
|
+
|
|
87
|
+
run acquire_lock "${test_lock}"
|
|
88
|
+
[ "$status" -eq 0 ]
|
|
89
|
+
[ -f "${test_lock}" ]
|
|
90
|
+
|
|
91
|
+
# PID written to lock file must be the current test process
|
|
92
|
+
local lock_pid
|
|
93
|
+
lock_pid=$(cat "${test_lock}")
|
|
94
|
+
[ -n "${lock_pid}" ]
|
|
95
|
+
|
|
96
|
+
rm -f "${test_lock}"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
@test "pr-resolver lock acquisition: acquire_lock fails when active lock exists" {
|
|
100
|
+
local test_lock="/tmp/nw-test-resolver-active-$$.lock"
|
|
101
|
+
|
|
102
|
+
# Write current PID as an active lock holder
|
|
103
|
+
echo $$ > "${test_lock}"
|
|
104
|
+
|
|
105
|
+
run acquire_lock "${test_lock}"
|
|
106
|
+
[ "$status" -eq 1 ]
|
|
107
|
+
|
|
108
|
+
rm -f "${test_lock}"
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
@test "pr-resolver lock acquisition: acquire_lock removes stale lock and succeeds" {
|
|
112
|
+
local test_lock="/tmp/nw-test-resolver-stale-$$.lock"
|
|
113
|
+
|
|
114
|
+
# Write a PID that does not exist (use a very high number unlikely to be running)
|
|
115
|
+
echo "999999999" > "${test_lock}"
|
|
116
|
+
|
|
117
|
+
run acquire_lock "${test_lock}"
|
|
118
|
+
[ "$status" -eq 0 ]
|
|
119
|
+
[ -f "${test_lock}" ]
|
|
120
|
+
|
|
121
|
+
rm -f "${test_lock}"
|
|
122
|
+
}
|