@jonit-dev/night-watch-cli 1.8.13 → 1.8.14-beta.1

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.
@@ -3,17 +3,7 @@
3
3
  * Designed for bash script integration.
4
4
  */
5
5
  import { basename } from 'path';
6
- import { loadConfig, sendNotifications } from '@night-watch/core';
7
- const VALID_EVENTS = [
8
- 'run_started',
9
- 'run_succeeded',
10
- 'run_failed',
11
- 'run_timeout',
12
- 'review_completed',
13
- 'rate_limit_fallback',
14
- 'pr_auto_merged',
15
- 'qa_completed',
16
- ];
6
+ import { NOTIFICATION_EVENTS, loadConfig, sendNotifications } from '@night-watch/core';
17
7
  export function notifyCommand(program) {
18
8
  program
19
9
  .command('notify <event> <projectDir>')
@@ -24,8 +14,8 @@ export function notifyCommand(program) {
24
14
  .option('--exit-code <n>', 'Exit code', '0')
25
15
  .option('--pr-number <n>', 'PR number')
26
16
  .action(async (event, projectDir, options) => {
27
- if (!VALID_EVENTS.includes(event)) {
28
- process.stderr.write(`Invalid event: ${event}. Must be one of: ${VALID_EVENTS.join(', ')}\n`);
17
+ if (!NOTIFICATION_EVENTS.includes(event)) {
18
+ process.stderr.write(`Invalid event: ${event}. Must be one of: ${NOTIFICATION_EVENTS.join(', ')}\n`);
29
19
  process.exit(2);
30
20
  }
31
21
  const config = loadConfig(projectDir);
@@ -1 +1 @@
1
- {"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAEhC,OAAO,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGlE,MAAM,YAAY,GAAwB;IACxC,aAAa;IACb,eAAe;IACf,YAAY;IACZ,aAAa;IACb,kBAAkB;IAClB,qBAAqB;IACrB,gBAAgB;IAChB,cAAc;CACf,CAAC;AAEF,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO;SACJ,OAAO,CAAC,6BAA6B,CAAC;SACtC,WAAW,CAAC,mDAAmD,CAAC;SAChE,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC;SAClC,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAAC;SACxC,MAAM,CAAC,mBAAmB,EAAE,eAAe,CAAC;SAC5C,MAAM,CAAC,iBAAiB,EAAE,WAAW,EAAE,GAAG,CAAC;SAC3C,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;SACtC,MAAM,CACL,KAAK,EACH,KAAa,EACb,UAAkB,EAClB,OAMC,EACD,EAAE;QACF,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,KAA0B,CAAC,EAAE,CAAC;YACvD,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kBAAkB,KAAK,qBAAqB,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CACxE,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QAEtC,MAAM,iBAAiB,CAAC,MAAM,EAAE;YAC9B,KAAK,EAAE,KAA0B;YACjC,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC;YACjC,OAAO,EAAE,OAAO,CAAC,GAAG;YACpB,UAAU,EAAE,OAAO,CAAC,MAAM;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;YAC7C,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC;YAC7C,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;SACxE,CAAC,CAAC;IACL,CAAC,CACF,CAAC;AACN,CAAC"}
1
+ {"version":3,"file":"notify.js","sourceRoot":"","sources":["../../src/commands/notify.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,EAAE,QAAQ,EAAE,MAAM,MAAM,CAAC;AAEhC,OAAO,EAAE,mBAAmB,EAAE,UAAU,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AAGvF,MAAM,UAAU,aAAa,CAAC,OAAgB;IAC5C,OAAO;SACJ,OAAO,CAAC,6BAA6B,CAAC;SACtC,WAAW,CAAC,mDAAmD,CAAC;SAChE,MAAM,CAAC,cAAc,EAAE,UAAU,CAAC;SAClC,MAAM,CAAC,iBAAiB,EAAE,aAAa,CAAC;SACxC,MAAM,CAAC,mBAAmB,EAAE,eAAe,CAAC;SAC5C,MAAM,CAAC,iBAAiB,EAAE,WAAW,EAAE,GAAG,CAAC;SAC3C,MAAM,CAAC,iBAAiB,EAAE,WAAW,CAAC;SACtC,MAAM,CACL,KAAK,EACH,KAAa,EACb,UAAkB,EAClB,OAMC,EACD,EAAE;QACF,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,KAA0B,CAAC,EAAE,CAAC;YAC9D,OAAO,CAAC,MAAM,CAAC,KAAK,CAClB,kBAAkB,KAAK,qBAAqB,mBAAmB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,CAC/E,CAAC;YACF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,UAAU,CAAC,UAAU,CAAC,CAAC;QAEtC,MAAM,iBAAiB,CAAC,MAAM,EAAE;YAC9B,KAAK,EAAE,KAA0B;YACjC,WAAW,EAAE,QAAQ,CAAC,UAAU,CAAC;YACjC,OAAO,EAAE,OAAO,CAAC,GAAG;YACpB,UAAU,EAAE,OAAO,CAAC,MAAM;YAC1B,QAAQ,EAAE,OAAO,CAAC,QAAQ,IAAI,MAAM,CAAC,QAAQ;YAC7C,QAAQ,EAAE,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,IAAI,CAAC;YAC7C,QAAQ,EAAE,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;SACxE,CAAC,CAAC;IACL,CAAC,CACF,CAAC;AACN,CAAC"}
@@ -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_POLICY=fallback-local - CI gate: ci-only|fallback-local|ignore
18
+ # NW_MERGER_LOCAL_CHECK_COMMAND=... - Command run in temp PR worktree for local fallback
17
19
  # NW_MERGER_CI_MAX_WAIT=300 - Max seconds to wait for checks after rebase
18
20
  # NW_MERGER_CI_POLL_INTERVAL=15 - Seconds between check polls after rebase
19
21
  # NW_DRY_RUN=0 - Set to 1 for dry-run mode
@@ -30,6 +32,8 @@ REBASE_BEFORE_MERGE="${NW_MERGER_REBASE_BEFORE_MERGE:-1}"
30
32
  MAX_PRS_PER_RUN="${NW_MERGER_MAX_PRS_PER_RUN:-0}"
31
33
  CI_MAX_WAIT="${NW_MERGER_CI_MAX_WAIT:-300}"
32
34
  CI_POLL_INTERVAL="${NW_MERGER_CI_POLL_INTERVAL:-15}"
35
+ CI_POLICY="${NW_MERGER_CI_POLICY:-fallback-local}"
36
+ LOCAL_CHECK_COMMAND="${NW_MERGER_LOCAL_CHECK_COMMAND:-yarn install --frozen-lockfile && yarn verify && yarn test}"
33
37
  BRANCH_PATTERNS_RAW="${NW_MERGER_BRANCH_PATTERNS:-}"
34
38
  READY_TO_MERGE_LABEL="${NW_PR_RESOLVER_READY_LABEL:-ready-to-merge}"
35
39
  SCRIPT_START_TIME=$(date +%s)
@@ -55,6 +59,10 @@ case "${MERGE_METHOD}" in
55
59
  squash|merge|rebase) ;;
56
60
  *) MERGE_METHOD="squash" ;;
57
61
  esac
62
+ case "${CI_POLICY}" in
63
+ ci-only|fallback-local|ignore) ;;
64
+ *) CI_POLICY="fallback-local" ;;
65
+ esac
58
66
 
59
67
  mkdir -p "${LOG_DIR}"
60
68
 
@@ -261,6 +269,97 @@ wait_for_ci_passing_on_head() {
261
269
  [ "${LAST_CI_STATUS}" = "passing" ]
262
270
  }
263
271
 
272
+ run_local_checks_for_head() {
273
+ local pr_number="${1}"
274
+ local expected_head="${2}"
275
+ local temp_parent=""
276
+ local worktree_dir=""
277
+ local temp_ref=""
278
+ local check_exit=1
279
+
280
+ if [ -z "${LOCAL_CHECK_COMMAND}" ]; then
281
+ log "INFO: PR #${pr_number}: Local check command is empty, treating fallback as failed"
282
+ return 1
283
+ fi
284
+
285
+ temp_parent=$(mktemp -d "${TMPDIR:-/tmp}/night-watch-merger-${pr_number}.XXXXXX")
286
+ worktree_dir="${temp_parent}/worktree"
287
+ temp_ref="refs/night-watch/merger/${pr_number}-${expected_head}"
288
+
289
+ cleanup_local_check_worktree() {
290
+ git worktree remove --force "${worktree_dir}" >/dev/null 2>&1 || true
291
+ git update-ref -d "${temp_ref}" >/dev/null 2>&1 || true
292
+ rm -rf "${temp_parent}" >/dev/null 2>&1 || true
293
+ }
294
+
295
+ log "INFO: PR #${pr_number}: Running local checks for head ${expected_head}: ${LOCAL_CHECK_COMMAND}"
296
+
297
+ if ! git cat-file -e "${expected_head}^{commit}" >/dev/null 2>&1; then
298
+ log "INFO: PR #${pr_number}: Fetching PR head ${expected_head} for local checks"
299
+ git fetch --quiet --force origin "pull/${pr_number}/head:${temp_ref}" >/dev/null 2>&1 || {
300
+ log "INFO: PR #${pr_number}: Unable to fetch PR head for local checks"
301
+ cleanup_local_check_worktree
302
+ return 1
303
+ }
304
+ fi
305
+
306
+ if ! git worktree add --detach --quiet "${worktree_dir}" "${expected_head}" >/dev/null 2>&1; then
307
+ if ! git worktree add --detach --quiet "${worktree_dir}" "${temp_ref}" >/dev/null 2>&1; then
308
+ log "INFO: PR #${pr_number}: Unable to create local check worktree"
309
+ cleanup_local_check_worktree
310
+ return 1
311
+ fi
312
+ fi
313
+
314
+ set +e
315
+ (
316
+ cd "${worktree_dir}"
317
+ bash -lc "${LOCAL_CHECK_COMMAND}"
318
+ ) 2>&1 | tee -a "${LOG_FILE}"
319
+ check_exit=${PIPESTATUS[0]}
320
+ set -e
321
+
322
+ cleanup_local_check_worktree
323
+
324
+ if [ "${check_exit}" -eq 0 ]; then
325
+ log "INFO: PR #${pr_number}: Local checks passed for head ${expected_head}"
326
+ return 0
327
+ fi
328
+
329
+ log "INFO: PR #${pr_number}: Local checks failed for head ${expected_head} (exit ${check_exit})"
330
+ return 1
331
+ }
332
+
333
+ ci_gate_allows_head() {
334
+ local pr_number="${1}"
335
+ local pr_branch="${2}"
336
+ local expected_head="${3}"
337
+ local context="${4:-CI}"
338
+ local ci_status
339
+
340
+ if [ "${CI_POLICY}" = "ignore" ]; then
341
+ log "INFO: PR #${pr_number} (${pr_branch}): CI policy is ignore; skipping ${context} gate for head ${expected_head}"
342
+ return 0
343
+ fi
344
+
345
+ ci_status=$(ci_status_for_head "${pr_number}" "${expected_head}")
346
+ if [ "${ci_status}" = "passing" ]; then
347
+ return 0
348
+ fi
349
+
350
+ if [ "${CI_POLICY}" = "fallback-local" ]; then
351
+ log "INFO: PR #${pr_number} (${pr_branch}): ${context} not passing on head ${expected_head} (${ci_status}); trying local checks"
352
+ if run_local_checks_for_head "${pr_number}" "${expected_head}"; then
353
+ return 0
354
+ fi
355
+ log "INFO: PR #${pr_number} (${pr_branch}): Local check fallback failed, skipping"
356
+ return 1
357
+ fi
358
+
359
+ log "INFO: PR #${pr_number} (${pr_branch}): ${context} not passing on head ${expected_head} (${ci_status}), skipping"
360
+ return 1
361
+ }
362
+
264
363
  # Rebase a PR against its base branch
265
364
  rebase_pr() {
266
365
  local pr_number="${1}"
@@ -308,7 +407,7 @@ cd "${PROJECT_DIR}"
308
407
 
309
408
  log "========================================"
310
409
  log "RUN-START: merger invoked project=${PROJECT_DIR} dry_run=${DRY_RUN}"
311
- log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s ready_label=${READY_TO_MERGE_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
410
+ log "CONFIG: merge_method=${MERGE_METHOD} min_review_score=${MIN_REVIEW_SCORE} rebase_before_merge=${REBASE_BEFORE_MERGE} max_prs=${MAX_PRS_PER_RUN} max_runtime=${MAX_RUNTIME}s ci_policy=${CI_POLICY} local_check_command=${LOCAL_CHECK_COMMAND} ready_label=${READY_TO_MERGE_LABEL} branch_patterns=${BRANCH_PATTERNS_RAW:-<all>}"
312
411
  log "========================================"
313
412
 
314
413
  if ! acquire_lock "${LOCK_FILE}"; then
@@ -391,10 +490,8 @@ while IFS= read -r pr_json; do
391
490
  continue
392
491
  fi
393
492
 
394
- # Check CI status
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"
493
+ # Check CI status, optionally falling back to local checks for provider/billing failures.
494
+ if ! ci_gate_allows_head "${pr_number}" "${pr_branch}" "${pr_head_oid}" "CI"; then
398
495
  continue
399
496
  fi
400
497
 
@@ -436,10 +533,22 @@ while IFS= read -r pr_json; do
436
533
  log "INFO: PR #${pr_number}: Head unchanged after rebase (${pr_head_after_rebase}); confirming CI"
437
534
  fi
438
535
 
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"
442
- continue
536
+ if [ "${CI_POLICY}" = "ignore" ]; then
537
+ log "INFO: PR #${pr_number}: CI policy is ignore; skipping fresh CI gate after rebase"
538
+ else
539
+ # Poll CI until all checks attached to the post-rebase head are complete and passing.
540
+ if ! wait_for_ci_passing_on_head "${pr_number}" "${pr_head_after_rebase}"; then
541
+ 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)"
542
+ if [ "${CI_POLICY}" = "fallback-local" ]; then
543
+ if ! run_local_checks_for_head "${pr_number}" "${pr_head_after_rebase}"; then
544
+ log "INFO: PR #${pr_number}: Fresh local check fallback failed after rebase, skipping"
545
+ continue
546
+ fi
547
+ else
548
+ log "INFO: PR #${pr_number}: Skipping because CI policy requires GitHub CI"
549
+ continue
550
+ fi
551
+ fi
443
552
  fi
444
553
  fi
445
554