autonomous-sdlc-harness 0.6.2 → 0.6.3
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/commands/init.js +1 -1
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +3 -2
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +1 -1
- package/dist/config/model.js.map +1 -1
- package/dist/doctor/checks.js +7 -6
- package/dist/doctor/checks.js.map +1 -1
- package/dist/remote/githubActions.js +3 -2
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/github/workflows/harness-resume.yml +6 -5
- package/templates/github/workflows/harness-run.yml +29 -7
- package/templates/scripts/autonomous-watcher.sh +121 -21
- package/templates/scripts/lib/harness-run-lib.sh +140 -13
- package/templates/scripts/push-branch.sh +69 -11
- package/templates/scripts/remote-run.sh +1249 -242
- package/templates/state-dir/user_reviews/README.md +1 -1
|
@@ -32,6 +32,8 @@
|
|
|
32
32
|
# remote-run.sh discard <dir> [--repo <root>]
|
|
33
33
|
# remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]
|
|
34
34
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
35
|
+
# remote-run.sh open <branch> [--repo <root>]
|
|
36
|
+
# (always 0, 1 only on a usage error: its paragraph)
|
|
35
37
|
# remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]
|
|
36
38
|
# (always 0, 1 only on a usage error: its paragraph)
|
|
37
39
|
# remote-run.sh collect <branch> [--pr <n>] [--repo <root>]
|
|
@@ -53,9 +55,7 @@
|
|
|
53
55
|
# 1 usage error, or the library or the configuration could not be
|
|
54
56
|
# resolved; for fetch, <out_dir> is not an existing, empty directory;
|
|
55
57
|
# for discard, <dir>'s parent does not resolve or the removal failed;
|
|
56
|
-
# for sync and restore, a local copy or write failed
|
|
57
|
-
# pause-requested, also NO such run — a caller that reads 1 as "no
|
|
58
|
-
# pause" passes arguments it has already validated
|
|
58
|
+
# for sync and restore, a local copy or write failed
|
|
59
59
|
# 2 refused, nothing sent or written: execution.target is not
|
|
60
60
|
# github-actions (sending verbs, fetch, review and list); for
|
|
61
61
|
# review, a protected branch, a review file that is not a readable
|
|
@@ -91,6 +91,7 @@
|
|
|
91
91
|
# copy (the branch checked out in another working copy included), the
|
|
92
92
|
# fast-forward, the commit or the push — and nothing was dispatched; a
|
|
93
93
|
# copy it cut was removed
|
|
94
|
+
# 5 pause-requested only: the read succeeded and found no such run
|
|
94
95
|
#
|
|
95
96
|
# `start` IS THE ADAPTERS' ONE ENTRY: every trigger (an issue event, a forge
|
|
96
97
|
# dispatch, anything later) reduces to a branch and a task text and ends here.
|
|
@@ -285,17 +286,35 @@
|
|
|
285
286
|
# as `trigger` is, and outside the sending-verb gate: it does nothing, with one
|
|
286
287
|
# line, unless `hr_forge` is `github` and `hr_execution_target` is
|
|
287
288
|
# `github-actions`. The comment goes to the open same-repository pull request
|
|
288
|
-
# whose head is <branch> when origin's <branch> carries
|
|
289
|
-
# `<state_dir>/flow_progress/<branch>_progress.md
|
|
290
|
-
# the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
289
|
+
# whose head is <branch> when origin's <branch> carries its task prompt or
|
|
290
|
+
# `<state_dir>/flow_progress/<branch>_progress.md` (`forge_recognised`), else to
|
|
291
|
+
# the issue named by the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
291
292
|
# task prompt, else nowhere (`stop`'s `--pr` and `--branch-gone` change this
|
|
292
293
|
# for its own report: its paragraph). The label `STATE_LABEL_PREFIX<state>` replaces any
|
|
293
294
|
# other state label on that issue and that pull request, each when known; the
|
|
294
295
|
# label is a view, and the run list stays the authority. The state map:
|
|
295
296
|
# `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
|
|
296
|
-
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running
|
|
297
|
-
#
|
|
298
|
-
# `
|
|
297
|
+
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running,
|
|
298
|
+
# `not_started` (collect's) -> paused or failed, as its caller says. A
|
|
299
|
+
# `not_started` comment says GitHub did not start the run's job, so nothing ran
|
|
300
|
+
# and the branch is unchanged, and names its way on: `COMMAND_HANDLE resume`
|
|
301
|
+
# when the state is paused and an engine was recovered, else the **Run
|
|
302
|
+
# workflow** form with that engine, or with the three to choose from.
|
|
303
|
+
# On a pull-request target that is not a draft, `round` runs `gh pr ready
|
|
304
|
+
# --undo` before its comment, with the caller's own token, and its comment says
|
|
305
|
+
# the pull request is a draft again until the round completes; a refusal (a
|
|
306
|
+
# plan without drafts) is one `::warning::` line naming gh's error, and the
|
|
307
|
+
# comment then says turning it back was refused and it stays ready for review
|
|
308
|
+
# while the round works. A `failed` comment on a pull request reads the
|
|
309
|
+
# registry record's `engine`: for
|
|
310
|
+
# `user_review` it names a review requesting changes and says the pull request
|
|
311
|
+
# stays open; otherwise it says the draft stays open, to close to discard the
|
|
312
|
+
# run, or, when an issue is known, to re-apply the trigger label there for a
|
|
313
|
+
# new run on the next indexed branch. A plain `stopped` on a pull request says
|
|
314
|
+
# its draft stays open and closing it discards the run. No other event changes
|
|
315
|
+
# a pull request's draft state.
|
|
316
|
+
# Every job event — `parked`, `park_loop`, `paused`, `resumed`, `round`,
|
|
317
|
+
# `failed` and `not_started` — posts nothing and sets no label when `remote_branch_stopped`,
|
|
299
318
|
# asked afresh, finds the branch stopped, so a job a stop overtook never
|
|
300
319
|
# overwrites `stopped`; a failed listing reports anyway, and `stopped` is never
|
|
301
320
|
# withheld. `completed` (deliver's) and `launched` (the trigger's
|
|
@@ -319,36 +338,127 @@
|
|
|
319
338
|
# comment and its answer are public, as the artifact already is
|
|
320
339
|
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
321
340
|
# repository's Actions runs can see*).
|
|
341
|
+
# `progress` (`forge_progress`) keeps ONE comment per run or round on the pull
|
|
342
|
+
# request, never on the issue, and sets no label. Gated in order, each stop one
|
|
343
|
+
# line: `forge_on`; `hr_progress_comments` (`execution.progressComments` false,
|
|
344
|
+
# or unreadable); `remote_branch_stopped`, as for the job events above; no task
|
|
345
|
+
# or user-review ledger at `$root`'s `<state_dir>/flow_progress/<branch>_progress.md`
|
|
346
|
+
# (`hr_ledger_phases` — a docs-engine ledger posts nothing); no open
|
|
347
|
+
# same-repository pull request that `forge_recognised` holds for. The body is
|
|
348
|
+
# *Progress of the harness run on `<branch>`:* or *Progress of user-review round
|
|
349
|
+
# <n> on `<branch>`:*, then `- <label>: done`, `in progress` (the first pending
|
|
350
|
+
# phase) or `not started` for `Planning`, `Implementation`, `Branch review`,
|
|
351
|
+
# `Done` (a round: `Fix plan`, `Fix implementation`, `Branch review`, `Done`) —
|
|
352
|
+
# plain list items, no timestamp, no run URL — then the marker
|
|
353
|
+
# `<!-- sdlc-harness event=progress branch=<branch> -->`, gaining ` round=<n>`
|
|
354
|
+
# for a round. One paginated comment listing picks the newest
|
|
355
|
+
# `github-actions[bot]` comment whose last non-empty line is that marker: none
|
|
356
|
+
# is one `forge_comment`; a body equal to the render (carriage returns and
|
|
357
|
+
# trailing newlines aside) is no call and one `already current` line; otherwise
|
|
358
|
+
# one `PATCH` of that comment — the only comment this file ever edits. A refused
|
|
359
|
+
# listing, create or edit is one `::warning::` line.
|
|
322
360
|
# It never fails its caller: every problem is one line and exit 0.
|
|
323
361
|
#
|
|
362
|
+
# `open` OPENS THE RUN'S DRAFT PULL REQUEST at the run's start, so its issue
|
|
363
|
+
# links it from the moment it exists. Job-side for its root and self-gated, as
|
|
364
|
+
# `deliver` is (`forge_on`, one line when off); it takes <branch> and no
|
|
365
|
+
# bundle. A pull-request lookup that fails is one `::warning::` line and opens
|
|
366
|
+
# nothing; an open same-repository pull request whose head is <branch> is one
|
|
367
|
+
# line, and none is opened. Otherwise `forge_open_pr` — the one opener, which
|
|
368
|
+
# `deliver` also calls — creates it against `defaultBranch` with `deliver`'s
|
|
369
|
+
# title, body, token and retry rules: a `--draft` create with
|
|
370
|
+
# `HARNESS_PR_TOKEN` as `GH_TOKEN` when set, no retry on `PR_CREATE_FORBIDDEN`,
|
|
371
|
+
# one retry without `--draft` on any other failure. Opened, the pull request is
|
|
372
|
+
# labelled `STATE_LABEL_PREFIX`running, and when the task prompt names an issue
|
|
373
|
+
# one comment there names it — as a draft, or as not a draft because the
|
|
374
|
+
# repository's plan may not offer drafts — says the run's questions, lifecycle
|
|
375
|
+
# comments and progress go to it from now on while `COMMAND_HANDLE` commands
|
|
376
|
+
# keep working on the issue, adds this run's URL when `GITHUB_RUN_ID` is set,
|
|
377
|
+
# and ends with the `opened` marker. With no issue nothing is posted. Not
|
|
378
|
+
# opened is one `::warning::` line naming why and saying the run goes on, its
|
|
379
|
+
# comments reach the issue and `deliver` tries again at the run's end; nothing
|
|
380
|
+
# is posted, since `deliver`'s `completed` comment is the one notification.
|
|
381
|
+
# It never fails its caller: every outcome but a usage error is exit 0.
|
|
382
|
+
#
|
|
324
383
|
# `deliver` HANDS A COMPLETED RUN TO REVIEW: the run workflow's step after the
|
|
325
384
|
# job's own `push-branch.sh`, which opens no pull request. Job-side for its
|
|
326
385
|
# root and self-gated, as `report` is (`forge_on`, one line when off). Anything
|
|
327
386
|
# but `status: completed` in <bundle_dir>/status.json, or no status.json, is
|
|
328
|
-
# one line and nothing sent.
|
|
329
|
-
#
|
|
330
|
-
#
|
|
331
|
-
#
|
|
332
|
-
#
|
|
333
|
-
#
|
|
334
|
-
#
|
|
335
|
-
#
|
|
336
|
-
#
|
|
337
|
-
#
|
|
338
|
-
#
|
|
339
|
-
#
|
|
340
|
-
#
|
|
341
|
-
#
|
|
342
|
-
#
|
|
343
|
-
# branch
|
|
344
|
-
#
|
|
345
|
-
#
|
|
346
|
-
#
|
|
347
|
-
#
|
|
348
|
-
#
|
|
349
|
-
#
|
|
350
|
-
#
|
|
351
|
-
#
|
|
387
|
+
# one line and nothing sent. The bundle's `engine` names what completed:
|
|
388
|
+
# `user_review` is a round, anything else a run. An open same-repository pull
|
|
389
|
+
# request whose head is <branch> is reused and no second one is opened. With
|
|
390
|
+
# none open and a lookup that succeeded, `forge_open_pr` is the fallback — one
|
|
391
|
+
# line says none was open at completion, because the start's attempt failed or
|
|
392
|
+
# the workflow predates `open`. It is created against `defaultBranch`, titled
|
|
393
|
+
# from the task prompt's `# ` first line (cut to `PR_TITLE_MAX_CHARS`, else
|
|
394
|
+
# <branch>), with a body naming the issue as `Started from #<n>.` — a plain
|
|
395
|
+
# mention, never a closing keyword — stating what a review requesting changes
|
|
396
|
+
# and the `COMMAND_HANDLE` commands do, and ending with the `pull-request`
|
|
397
|
+
# marker: a `--draft` create with `HARNESS_PR_TOKEN` as `GH_TOKEN` when set, so
|
|
398
|
+
# the adopter's CI runs without an approval click, no retry on
|
|
399
|
+
# `PR_CREATE_FORBIDDEN`, one retry without `--draft` on any other failure. A
|
|
400
|
+
# pull request so opened is authored by that token's owner, who therefore
|
|
401
|
+
# cannot request changes on it: use a machine account's token, another
|
|
402
|
+
# reviewer, or a local `branch-user-review` round. A lookup that fails opens
|
|
403
|
+
# nothing. Then the ready flip, with the job's token and never
|
|
404
|
+
# `HARNESS_PR_TOKEN`: a draft gets one `gh pr ready` (flipped), and a refusal
|
|
405
|
+
# is one `::warning::` line naming gh's error (refused); a pull request that is
|
|
406
|
+
# not a draft — no drafts on the plan, or a person marked it ready — gets no
|
|
407
|
+
# call (not a draft) and is left alone. A round's review threads are handled
|
|
408
|
+
# next, after the flip and before the comments, with the job's token, per THE
|
|
409
|
+
# REVIEW-COMMENT LINE below: only when the pull request is known, the
|
|
410
|
+
# highest-numbered round file on origin's tip is the highest-numbered marked
|
|
411
|
+
# one (`round_markers_read`) — a round placed by the local command consumes no
|
|
412
|
+
# pull-request item — and its marker's `comments=` is non-empty; otherwise one
|
|
413
|
+
# line. Its fix plan is `<branch>_fix_plan.md` and `<branch>_fix_plan/` for
|
|
414
|
+
# round 1, `<branch>_fix_plan_<n>.md` and `<branch>_fix_plan_<n>/` for round
|
|
415
|
+
# <n>, read from origin's tip. A verdict is kept only for an id the round
|
|
416
|
+
# collected: `fixed <sha>` for an implemented finding, `reason <text>` for an
|
|
417
|
+
# out-of-scope bullet; a collected id with none is one line, left alone. One
|
|
418
|
+
# paginated `api graphql` listing of the pull request's `reviewThreads` (`id`,
|
|
419
|
+
# `isResolved`, each comment's `databaseId`, `createdAt` and `body`); the
|
|
420
|
+
# thread holding a collected id is that id's thread. Left alone, one line
|
|
421
|
+
# each: a thread already resolved; a thread with a comment created after the
|
|
422
|
+
# round's `collected_at` that is not a collected id and carries no
|
|
423
|
+
# `COMMENT_MARKER` — the reviewer replied after the collection; and a thread
|
|
424
|
+
# already carrying this round's `thread` marker (` round=<n>` included), so a
|
|
425
|
+
# re-run of the same round replies nothing twice while a later round still
|
|
426
|
+
# handles a thread an earlier round replied to. Otherwise one reply, through
|
|
427
|
+
# `pulls/<pr>/comments/<id>/replies` to the thread's first comment, ending with
|
|
428
|
+
# `forge_marker thread <branch>` and the round's ` round=<n>` — whose
|
|
429
|
+
# `COMMENT_MARKER` keeps the next `round_collect` from collecting it: for
|
|
430
|
+
# `fixed`, ``Addressed in `<sha>`.`` and, only once that reply is posted, one
|
|
431
|
+
# `resolveReviewThread` mutation; for `reason`, `Not changed in this round:
|
|
432
|
+
# <reason>`, the thread left open. A refused listing, reply or resolve is one
|
|
433
|
+
# `::warning::` line. It never dismisses a review.
|
|
434
|
+
# THE REVIEW-COMMENT LINE is this file's contract, written by one writer,
|
|
435
|
+
# `plugin/agents/user-review-fix-plan-writer.md`, and read by `deliver` alone:
|
|
436
|
+
# - in a per-finding file `<state_dir>/user_reviews/<branch>_fix_plan[_<n>]/
|
|
437
|
+
# finding_<K>.md`, one line beginning `**Review comments:** ` followed by
|
|
438
|
+
# one or more comment ids separated by `, `, e.g.
|
|
439
|
+
# `**Review comments:** 2735551234, 2735551240`;
|
|
440
|
+
# - in the fix-plan index's `## Out of scope / verified-OK` section, a bullet
|
|
441
|
+
# (a line beginning `- `) ending with ` **Review comments:** <id>[, <id>…]`,
|
|
442
|
+
# its text before that suffix the reason the reply quotes;
|
|
443
|
+
# - the ids are the round marker's `comments=` ids, GitHub's review-comment
|
|
444
|
+
# `id`s; an observation from no inline comment carries no such line;
|
|
445
|
+
# - finding <K> is implemented when the index carries the literal
|
|
446
|
+
# `[x] **Finding <K>**`, and its fix commit is the oldest commit on
|
|
447
|
+
# `origin/<branch>` that introduced that literal into the index (`git log
|
|
448
|
+
# --reverse -S`), else origin's tip — the committer flips the entry in the
|
|
449
|
+
# fix's own commit.
|
|
450
|
+
# Then one `completed` comment
|
|
451
|
+
# on the pull request, its readiness clause by the flip's outcome, and one on
|
|
452
|
+
# the issue naming the pull request's number and URL, each when known; with no
|
|
453
|
+
# pull request, one comment on the issue alone naming why none could be opened
|
|
454
|
+
# — the Actions setting and `HARNESS_GIT_TOKEN` on `PR_CREATE_FORBIDDEN`, else
|
|
455
|
+
# gh's error — with the branch's compare URL; nowhere when neither is known.
|
|
456
|
+
# Each names reviewing and requesting changes as the next action, and with
|
|
457
|
+
# `phases.qa` true the local `branch-qa-test` still owed. Then
|
|
458
|
+
# `sdlc-harness: done` on the issue and the pull request, each when known. It
|
|
459
|
+
# writes at most one pull request, one flip, a round's thread replies and
|
|
460
|
+
# resolves, two comments and those labels, and never pushes. It never fails its caller: every problem, a refused flip
|
|
461
|
+
# included, is one line and exit 0.
|
|
352
462
|
#
|
|
353
463
|
# `collect` STARTS THE NEXT ROUND FROM THE REVIEWS COLLECTED DURING A RUN: the
|
|
354
464
|
# one step of the run workflow's `collect` job, which follows its `run` job, so
|
|
@@ -356,19 +466,27 @@
|
|
|
356
466
|
# Job-side for its root and self-gated, as `report` and `deliver` are
|
|
357
467
|
# (`forge_on`, one line when off). In order, each stop one line and exit 0:
|
|
358
468
|
# `HARNESS_REMOTE_STOP` set; `remote_branch_stopped` finding the branch stopped,
|
|
359
|
-
# or failing;
|
|
360
|
-
#
|
|
469
|
+
# or failing; `branch_settled_var`, read as `control` reads it, failing; the
|
|
470
|
+
# newest run's job never started — when that run is this one (or
|
|
471
|
+
# `GITHUB_RUN_ID` is unset), `report`'s `not_started` on its pull request or
|
|
472
|
+
# issue, with the state and engine the read derived, and one push notification,
|
|
473
|
+
# no round collected, since the reviews stay for the resumed run's own end;
|
|
474
|
+
# when it is another run, one line, since that run's own `collect` reports it;
|
|
475
|
+
# no pull request, from --pr or else `forge_pr_var` (a failed lookup
|
|
476
|
+
# included); the settledness read finding the
|
|
361
477
|
# branch in flight — a newer run listed, or this run ending `parked`,
|
|
362
478
|
# `park_loop` or `paused` (a budget chain's included), whose own end collects
|
|
363
|
-
# next
|
|
479
|
+
# next; `round_collect` finding no review requesting changes
|
|
364
480
|
# pending (inline comments alone start no round, as *Comment* starts none; they
|
|
365
481
|
# ride along in the next), or failing, a `::warning::` line. Otherwise `review
|
|
366
482
|
# <branch> --review-file <file> --allow-no-run --reviewers <logins> --source
|
|
367
483
|
# <pull request url>` runs as a child, as `control` runs it, placing,
|
|
368
484
|
# dispatching and reporting the round. A child that exits non-zero gets exactly
|
|
369
485
|
# one comment on the pull request, with the `reply` marker, naming its last
|
|
370
|
-
# stderr line and saying the reviews stay there and that
|
|
371
|
-
#
|
|
486
|
+
# stderr line and saying the reviews stay there and that, when `GITHUB_RUN_ID`
|
|
487
|
+
# is set, re-running this run's `collect` job retries with no new review, and
|
|
488
|
+
# that submitting a review requesting changes retries; there is no automatic
|
|
489
|
+
# retry. It never fails its
|
|
372
490
|
# caller: every outcome but a usage error is exit 0.
|
|
373
491
|
#
|
|
374
492
|
# `control` IS THE COMMENT AND REVIEW ADAPTER, the twin of `trigger` and the one
|
|
@@ -412,8 +530,20 @@
|
|
|
412
530
|
# marker quoted mid-body, in a comment not opening with that sentence, or by
|
|
413
531
|
# anyone else, is never trusted; none is a refusal. Both paths then pass
|
|
414
532
|
# `control_check_branch`: a branch `hr_branch_is_protected` does not answer 1
|
|
415
|
-
# for is refused, then it is fetched
|
|
416
|
-
# flow-progress ledger (`forge_recognised`)
|
|
533
|
+
# for is refused, then it is fetched. It is a harness branch when its origin
|
|
534
|
+
# tip carries its task prompt or the flow-progress ledger (`forge_recognised`)
|
|
535
|
+
# — the task prompt, `start`'s first commit, makes a pull request opened at
|
|
536
|
+
# the run's start commandable before the ledger's first push; or, for a command
|
|
537
|
+
# typed on an issue, when it is the branch that issue's genuine `started`
|
|
538
|
+
# marker names and it still exists on origin (`remote_branch_exists`) — the
|
|
539
|
+
# marker shows the harness started a run on it, so a first run GitHub never
|
|
540
|
+
# started, which committed only its task prompt, is still commandable; an
|
|
541
|
+
# issue keeps its marker after its branch is deleted, so a marker-named branch
|
|
542
|
+
# gone from origin is refused as deleted, and a failed existence check is a
|
|
543
|
+
# refusal naming the read; or when a `harness run <branch>` run is queued,
|
|
544
|
+
# waiting, requested, pending or in progress (`control_run_in_flight`), so a
|
|
545
|
+
# run is commandable from its first minute on its pull request too. Otherwise
|
|
546
|
+
# it is not a harness branch. A failed listing is a refusal naming the read.
|
|
417
547
|
# THE ARMS. `pause`: the state by a `fetch` child (`control_state_var`); only
|
|
418
548
|
# `running` sends `pause <branch>` as a child and replies that the run yields at
|
|
419
549
|
# its next clean checkpoint; any other state is a refusal naming it. `stop`:
|
|
@@ -428,9 +558,11 @@
|
|
|
428
558
|
# accepts only `park_loop`, the GitHub form of `branch-resume`'s confirmation;
|
|
429
559
|
# any other state is a refusal naming it. Each sends the local relay's dispatch,
|
|
430
560
|
# `dispatch <branch> --engine <the state's engine> --resume pause --chain 0`,
|
|
431
|
-
# `clear` with `park_loop_clear` set, and no other command sets it.
|
|
432
|
-
#
|
|
433
|
-
#
|
|
561
|
+
# `clear` with `park_loop_clear` set, and no other command sets it. A run whose
|
|
562
|
+
# job GitHub never started first takes the engine its dispatch's comment
|
|
563
|
+
# records (`forge_dispatch_engine_var`); an engine still empty after that is
|
|
564
|
+
# refused, never guessed (the `engine` input defaults to `task`), and the
|
|
565
|
+
# refusal names the Run workflow form. On 0 a reply, then `running` on the run's issue
|
|
434
566
|
# and pull request; on 2 a refusal and exit 2. `answer`: the first line is
|
|
435
567
|
# `answer <n>` and the answer every line below it, a trailing CR stripped from
|
|
436
568
|
# each line and its bytes otherwise unchanged; text after <n> on the first line
|
|
@@ -555,8 +687,10 @@
|
|
|
555
687
|
# submitting a review requesting changes retries now.
|
|
556
688
|
# A child's failure is a reply
|
|
557
689
|
# naming its last stderr line, and exit 3. Every reply goes to the item the comment was
|
|
558
|
-
# typed on, opens `@<login>`, and carries the `reply` marker; a
|
|
559
|
-
#
|
|
690
|
+
# typed on, opens `@<login>`, and carries the `reply` marker; a reply posted
|
|
691
|
+
# after a successful dispatch adds ` engine=<engine>` to that marker, the engine
|
|
692
|
+
# the dispatch named, so a run whose job never ran can still be resumed with
|
|
693
|
+
# it. A refusal reads `@<login>: `<verb>` was not run: <reason>. <way on>`.
|
|
560
694
|
# THE CLOSE. Whatever the run's phase, it is stopped when its issue is closed,
|
|
561
695
|
# its pull request closed or merged, or its branch deleted; the routing is by
|
|
562
696
|
# event name, never by verb, so a comment naming `close` is not one. `issues`
|
|
@@ -585,8 +719,8 @@
|
|
|
585
719
|
# answers 1): GitHub closed the pull request because the branch was
|
|
586
720
|
# deleted, and the `delete` event's job stops the run; an `ls-remote` that
|
|
587
721
|
# cannot answer is one line and proceeds
|
|
588
|
-
# There is no
|
|
589
|
-
# branch may no longer carry
|
|
722
|
+
# There is no harness-branch check (`forge_recognised`): a merged or deleted
|
|
723
|
+
# branch may no longer carry its task prompt or ledger, and a listed `harness run <b>` run is the
|
|
590
724
|
# proof. The state is read by `control_state_var`: only `running`, `parked`,
|
|
591
725
|
# `park_loop` and `paused` are acted on; `completed`, `failed` and `none` are
|
|
592
726
|
# `left alone: the run on <b> is <state>`, and one `remote_branch_stopped`
|
|
@@ -683,7 +817,9 @@
|
|
|
683
817
|
# HARNESS_MAX_CHAIN the automatic-dispatch limit; `24` when empty. Not a
|
|
684
818
|
# non-negative integer: nothing is dispatched
|
|
685
819
|
# HARNESS_POLL_MAX_DISPATCH_FAILURES `poll` only: failed re-dispatches of
|
|
686
|
-
# one paused run
|
|
820
|
+
# one paused run, and consecutive failed downloads of
|
|
821
|
+
# one run's listed bundle, each counted apart, before it
|
|
822
|
+
# gives up; `3` when empty
|
|
687
823
|
# HARNESS_POLL_GIVE_UP_AFTER_MINUTES `poll` only: minutes after a run's
|
|
688
824
|
# `usage_resume_at` past which a failed re-dispatch gives
|
|
689
825
|
# up; `360` when empty. Either one not a non-negative
|
|
@@ -694,7 +830,9 @@
|
|
|
694
830
|
# GITHUB_RUN_ID, GITHUB_SERVER_URL, GITHUB_REPOSITORY the run URL
|
|
695
831
|
# Notifications go through the sibling `autonomous-notify.sh`, as `paused` or
|
|
696
832
|
# `failed`, and each is then reported as `report` reports that event, with a
|
|
697
|
-
# note of its own that names no slash command and no shell command.
|
|
833
|
+
# note of its own that names no slash command and no shell command. The one
|
|
834
|
+
# exception is `poll`'s push-only `bundle_unreadable`, which posts no comment
|
|
835
|
+
# and sets no label. A re-dispatch is `dispatch <branch> --engine <status.json engine>
|
|
698
836
|
# --resume pause --chain <chain + 1>`, composed by `dispatch` itself.
|
|
699
837
|
#
|
|
700
838
|
# `chain` HAS ONE SOURCE: the bundle's `status.json`, whose `chain` is the
|
|
@@ -741,7 +879,8 @@
|
|
|
741
879
|
# than `GITHUB_RUN_ID` (bounded) carrying an unexpired `harness-poll-state`
|
|
742
880
|
# artifact is downloaded to `<state_dir>/autonomous_logs/poll_state/previous/`;
|
|
743
881
|
# any failure to find or read it is one line and an empty state. Its
|
|
744
|
-
# `poll_state.json` is {"<branch>": {"run_id", "failures", "notified"
|
|
882
|
+
# `poll_state.json` is {"<branch>": {"run_id", "failures", "notified",
|
|
883
|
+
# "download_failures"}}, the last absent until a download fails; an
|
|
745
884
|
# entry whose `run_id` is not the branch's newest `harness run` run is dropped,
|
|
746
885
|
# so a new run restarts the count. `poll` writes the state to `poll_state/
|
|
747
886
|
# current/` on every exit, `HARNESS_REMOTE_STOP` included, and the poller
|
|
@@ -756,9 +895,16 @@
|
|
|
756
895
|
# download that fails for it is one line and waiting. For a `completed` run,
|
|
757
896
|
# skipped, not waiting: a stopped branch, a branch absent on origin (its state
|
|
758
897
|
# entry dropped, so a later branch of the name starts clean; an `ls-remote`
|
|
759
|
-
# that cannot answer is one line and proceeds), a
|
|
760
|
-
#
|
|
761
|
-
#
|
|
898
|
+
# that cannot answer is one line and proceeds), a run whose `run` job GitHub
|
|
899
|
+
# never started (one line; its `collect` job reports it), and anything but
|
|
900
|
+
# `status: paused` / `pause_reason: usage` with an integer `usage_resume_at`,
|
|
901
|
+
# and a run whose state entry says `notified`. A failed artifact lookup on a
|
|
902
|
+
# `completed` run counts as a failed download of its bundle, below. A listed
|
|
903
|
+
# bundle that cannot be downloaded is waiting, counted per run in `download_failures` and reset by a
|
|
904
|
+
# later successful download; at `HARNESS_POLL_MAX_DISPATCH_FAILURES`
|
|
905
|
+
# consecutive failures it sends exactly one `bundle_unreadable` push
|
|
906
|
+
# notification, with no comment and no label, the entry marked `notified`, and
|
|
907
|
+
# is no longer waiting. Due
|
|
762
908
|
# (reset passed): re-dispatched under the same chain limit — a refusal is one
|
|
763
909
|
# `failed` and not waiting; a success drops the branch's state entry. A
|
|
764
910
|
# dispatch that fails counts one more failure for that run and is still
|
|
@@ -783,9 +929,12 @@
|
|
|
783
929
|
# `pause-requested` lists the branch's runs and exits 0 when one whose
|
|
784
930
|
# `displayTitle` is exactly `harness pause <branch>` has a `createdAt` at or
|
|
785
931
|
# after <since_epoch> — AT, because `createdAt` has one-second resolution and
|
|
786
|
-
# the caller
|
|
787
|
-
#
|
|
788
|
-
#
|
|
932
|
+
# the caller passes a bound that starts at the job's own starting bound and
|
|
933
|
+
# afterwards lags each query by an overlap (`CONTROL_POLL_OVERLAP_SECS` in
|
|
934
|
+
# `autonomous-watcher.sh`), floored at that starting bound, so a pause created
|
|
935
|
+
# later in a second already queried is still seen; seeing one twice is
|
|
936
|
+
# harmless, since the caller drops PAUSE once. Exit 5 is "no pause"; exit 1 is
|
|
937
|
+
# a refused call and never one. `run-created-at` prints
|
|
789
938
|
# `gh run view <run_id> --json createdAt` as an epoch second.
|
|
790
939
|
#
|
|
791
940
|
# `status` AND `sync` READ THE RECORD, NOT THE KEY. A run keeps the execution
|
|
@@ -840,12 +989,17 @@
|
|
|
840
989
|
# `engine` when the bundle names `task`, `user_review` or `docs`. A
|
|
841
990
|
# `mirror` restore places no planning draft
|
|
842
991
|
# 4. no artifact, while some bundle exists (`remote_run_id` is set, or an
|
|
843
|
-
# older finished run carries one): a job that died before its upload
|
|
992
|
+
# older finished run carries one): a job that died before its upload,
|
|
993
|
+
# or that GitHub never started (the detail says so).
|
|
844
994
|
# `paused` / `killed`, `remote_run_id` / `remote_run_url` re-pointed at
|
|
845
|
-
# THIS run,
|
|
995
|
+
# THIS run, and, for a run GitHub never started whose dispatch's comment
|
|
996
|
+
# records its engine, `engine` set to that engine; nothing restored — so
|
|
997
|
+
# a later sync with no newer run is case 1
|
|
846
998
|
# 5. no bundle in any run and an empty `remote_run_id`: `failed`. Not
|
|
847
999
|
# `paused`: with no bundle anywhere a pause resume has nothing to restore,
|
|
848
|
-
# and re-dropping the artifact is the recovery
|
|
1000
|
+
# and re-dropping the artifact is the recovery, except a run whose job
|
|
1001
|
+
# GitHub never started and whose engine its dispatch's comment records,
|
|
1002
|
+
# which `sync` records as case 4 does, `paused (killed)`
|
|
849
1003
|
#
|
|
850
1004
|
# THE `killed` AND `expired` MAPPINGS. A finished run whose bundle still says
|
|
851
1005
|
# `running` (a kill, a timeout with no chain left) syncs as `status: paused`,
|
|
@@ -854,7 +1008,16 @@
|
|
|
854
1008
|
# never carries either, because `sync` derives them from the run and its
|
|
855
1009
|
# artifact list, never from a bundle. Both are `paused` rather than `failed`
|
|
856
1010
|
# because a `failed` record has no resume path, while the ledger on the branch
|
|
857
|
-
# is intact and a resume continues from it.
|
|
1011
|
+
# is intact and a resume continues from it. A finished run with no bundle
|
|
1012
|
+
# whose `run` job GitHub never started (it ended `cancelled` or `failure` with
|
|
1013
|
+
# no step run) maps to the same states as any other run with no bundle:
|
|
1014
|
+
# `paused` / `killed` when an older run carries a bundle, else `failed`. Its
|
|
1015
|
+
# detail names GitHub's reason instead of "killed, cancelled or replaced".
|
|
1016
|
+
# A never-started run with no bundle anywhere whose engine its dispatch's
|
|
1017
|
+
# comment records maps to `paused` / `killed` too, so `resume` accepts it.
|
|
1018
|
+
# When that run was the branch's first, the branch has no ledger yet:
|
|
1019
|
+
# `restore` finds no previous bundle and the resumed job starts as the
|
|
1020
|
+
# branch's first. With no such comment it stays `failed`.
|
|
858
1021
|
#
|
|
859
1022
|
# THE WORKFLOW INPUT CONTRACT (the workflow template declares the same inputs):
|
|
860
1023
|
#
|
|
@@ -919,37 +1082,55 @@
|
|
|
919
1082
|
# listing that `trigger` and `review` make. Only `start` and `review` push, and
|
|
920
1083
|
# only through `create-worktree.sh` and `push-branch.sh`; `start`'s writes are
|
|
921
1084
|
# the prompt committed on `origin/<branch>`, through a working copy and a local
|
|
922
|
-
# branch it removes before it returns
|
|
923
|
-
#
|
|
924
|
-
#
|
|
925
|
-
#
|
|
1085
|
+
# branch it removes before it returns, and, after a push that did not land, a
|
|
1086
|
+
# fetch that force-writes that copy's `refs/remotes/origin/<branch>`
|
|
1087
|
+
# (`hr_push_landed`'s, to name the commit the remote moved to); `review`'s are
|
|
1088
|
+
# the round committed on `origin/<branch>`, through the record's mirror or a
|
|
1089
|
+
# copy and a local branch it removes, and, after a push that did not land, the
|
|
1090
|
+
# same `hr_push_landed` fetch force-writing `refs/remotes/origin/<branch>` in
|
|
1091
|
+
# that mirror or copy, the record's `status` / `engine`, the bundle download
|
|
1092
|
+
# directory `sync` uses, and `report`'s writes for the round. Its settledness
|
|
1093
|
+
# read, when the newest run's job GitHub never started, also fetches origin's
|
|
1094
|
+
# <branch>, force-writing `refs/remotes/origin/<branch>` in the root checkout:
|
|
1095
|
+
# for a local `/autonomous-sdlc-harness:branch-user-review`, the main checkout. A user's chain-0 `dispatch --resume answer|pause`
|
|
926
1096
|
# writes the record's `status`, `resumed_at` and `resume_kind` in one write
|
|
927
1097
|
# when the main checkout's registry file exists and holds a record with
|
|
928
1098
|
# `execution: github-actions`; any other `dispatch` writes nothing. `trigger` writes its snapshot and comment
|
|
929
1099
|
# files under `RUNNER_TEMP`, one comment on the issue and the label removal, or
|
|
930
1100
|
# for a dispatch event a block in the step summary. `report` writes its comment
|
|
931
1101
|
# file under `RUNNER_TEMP` (removed), one comment and the state labels on the
|
|
932
|
-
# issue and the pull request, and nothing local. `
|
|
1102
|
+
# issue and the pull request, and nothing local. `open` writes its body and
|
|
1103
|
+
# comment files under `RUNNER_TEMP` (removed), at most one pull request, its
|
|
1104
|
+
# `running` label and one comment on the issue. `deliver` writes its body and
|
|
933
1105
|
# comment files under `RUNNER_TEMP` (removed), at most one pull request, one
|
|
934
1106
|
# comment and the state labels. `control` writes its reply file and its
|
|
935
1107
|
# `fetch` directory under `RUNNER_TEMP` (removed) and one reply comment, plus
|
|
936
|
-
# what the child verb it runs writes
|
|
937
|
-
#
|
|
938
|
-
#
|
|
1108
|
+
# what the child verb it runs writes, and the fetch of origin's <branch> that
|
|
1109
|
+
# force-writes `refs/remotes/origin/<branch>` in its checkout. `collect` writes its round file and its
|
|
1110
|
+
# settledness directory under `RUNNER_TEMP` (removed), at most one comment, on
|
|
1111
|
+
# the pull request, or on the issue for a run whose job never started, and for
|
|
1112
|
+
# that event `report`'s state labels on both items and one push notification,
|
|
1113
|
+
# the fetch of origin's <branch> its settledness read makes for such a run,
|
|
1114
|
+
# force-writing `refs/remotes/origin/<branch>`, plus what its `review` child
|
|
1115
|
+
# writes. `stop`, `continue` and `poll` also make
|
|
939
1116
|
# `report`'s writes for each event they report. Every other verb's only writes are the
|
|
940
1117
|
# registry record (`stop`, `sync`) and, for `sync`, the download directory
|
|
941
1118
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
942
1119
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
943
|
-
# `hr_remote_bundle_restore` performs in the record's `worktree
|
|
1120
|
+
# `hr_remote_bundle_restore` performs in the record's `worktree`, and, when
|
|
1121
|
+
# the newest run's job GitHub never started, a fetch force-writing
|
|
1122
|
+
# `refs/remotes/origin/<branch>` in the main checkout; for
|
|
944
1123
|
# `restore`, that download directory, the job restore (the planning drafts
|
|
945
1124
|
# among it), `answer_<n>.md` and the
|
|
946
1125
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
947
1126
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
948
1127
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
949
|
-
# and `current/`. For `fetch`, <out_dir>
|
|
950
|
-
#
|
|
951
|
-
#
|
|
952
|
-
#
|
|
1128
|
+
# and `current/`. For `fetch`, <out_dir> and, when the newest run's job GitHub
|
|
1129
|
+
# never started, a fetch force-writing `refs/remotes/origin/<branch>` in the
|
|
1130
|
+
# root checkout. For `discard`, the removal of <dir> only. `pause-requested`,
|
|
1131
|
+
# `run-created-at` and `list` write nothing, and `status` writes nothing but,
|
|
1132
|
+
# with no record, that same fetch; a no-record `status` downloads into a
|
|
1133
|
+
# temporary directory it removes on exit.
|
|
953
1134
|
#
|
|
954
1135
|
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
955
1136
|
# rename there is an edit here, byte for byte:
|
|
@@ -1139,7 +1320,9 @@
|
|
|
1139
1320
|
# the job's reads: a `run list` answer whose run has `displayTitle` `harness
|
|
1140
1321
|
# pause feat_x` and `createdAt` `2026-01-01T00:00:10Z` (epoch 1767225610):
|
|
1141
1322
|
# pause-requested bash scripts/remote-run.sh pause-requested feat_x 1767225600
|
|
1142
|
-
# -> 0; with 1767225620 ->
|
|
1323
|
+
# -> 0; with 1767225620 -> 5; a stub failing `run list` -> 3;
|
|
1324
|
+
# bash scripts/remote-run.sh pause-requested feat_x (no
|
|
1325
|
+
# <since_epoch>) -> 1
|
|
1143
1326
|
# run-created-at a stub answering `run view 42 --json createdAt` with
|
|
1144
1327
|
# {"createdAt":"2026-01-01T00:00:10Z"}: bash scripts/remote-run.sh
|
|
1145
1328
|
# run-created-at 42 -> prints 1767225610, 0; a failing stub -> 3
|
|
@@ -1178,15 +1361,45 @@
|
|
|
1178
1361
|
# naming `@sdlc-harness resume`, then `api --method POST
|
|
1179
1362
|
# repos/o/r/issues/7/labels -f labels[]=sdlc-harness: paused`
|
|
1180
1363
|
# forge off `"forge": "none"`, then the same -> 0, one line, "$s.log" unchanged
|
|
1364
|
+
# progress a task ledger at sdlc-harness/flow_progress/feat_x_progress.md in
|
|
1365
|
+
# the checkout and the stub answering `pr list` with [{"number":12,
|
|
1366
|
+
# "isCrossRepository":false}]: bash scripts/remote-run.sh report
|
|
1367
|
+
# progress feat_x -> 0; "$s.log" gains `api --paginate
|
|
1368
|
+
# repos/o/r/issues/12/comments ...`, then one POST on 12 ending in
|
|
1369
|
+
# `event=progress branch=feat_x -->`; with that comment listed, no
|
|
1370
|
+
# write; with the ledger changed, one `PATCH repos/o/r/issues/comments/<id>`
|
|
1181
1371
|
#
|
|
1182
1372
|
# deliver needs report's setup, a stub answering the create with
|
|
1183
1373
|
# https://github.com/o/r/pull/12, and <b> a bundle directory whose status.json
|
|
1184
1374
|
# carries schema "1" and status completed:
|
|
1185
1375
|
# deliver bash scripts/remote-run.sh deliver feat_x <b> -> 0; "$s.log" gains
|
|
1186
|
-
# a `--draft` create with `--base main --head feat_x`, then
|
|
1187
|
-
#
|
|
1188
|
-
# 7
|
|
1376
|
+
# a `--draft` create with `--base main --head feat_x`, then `pr
|
|
1377
|
+
# ready 12` with the job's token, then one comment on 12 saying
|
|
1378
|
+
# it is now marked ready, then one on issue 7 naming #12 and
|
|
1379
|
+
# /pull/12, then `sdlc-harness: done` on 7 and 12
|
|
1380
|
+
# reused the stub answering `pr list` with [{"number":12,
|
|
1381
|
+
# "isCrossRepository":false,"isDraft":false}] -> 0, no create, no
|
|
1382
|
+
# `pr ready`, the pull request's comment saying it is not a draft
|
|
1383
|
+
# refused the stub failing `pr ready` -> 0, one `::warning::` line, both
|
|
1384
|
+
# comments posted, the pull request's saying the flip was refused
|
|
1189
1385
|
# not done status parked, or an empty <b> -> 0, one line, "$s.log" unchanged
|
|
1386
|
+
# threads <b>'s engine user_review; on feat_x a `feat_x_review.md` ending in
|
|
1387
|
+
# the marker `comments=101,103`, a `feat_x_fix_plan.md` whose
|
|
1388
|
+
# `1. [x] **Finding 1**` lands in its own commit and whose `## Out
|
|
1389
|
+
# of scope / verified-OK` bullet ends ` **Review comments:** 103`,
|
|
1390
|
+
# and `feat_x_fix_plan/finding_1.md` carrying `**Review comments:**
|
|
1391
|
+
# 101`; a stub answering `api graphql` with one open thread per id
|
|
1392
|
+
# -> 0; "$s.log" gains a reply on comment 101 naming that commit,
|
|
1393
|
+
# then one `resolveReviewThread`, then a reply on 103 quoting the
|
|
1394
|
+
# bullet and no resolve; no dismissal call
|
|
1395
|
+
#
|
|
1396
|
+
# open needs deliver's setup without the bundle:
|
|
1397
|
+
# open bash scripts/remote-run.sh open feat_x -> 0; "$s.log" gains a
|
|
1398
|
+
# `--draft` create with `--base main --head feat_x`, then
|
|
1399
|
+
# `sdlc-harness: running` on 12, then one comment on issue 7
|
|
1400
|
+
# naming /pull/12 and ending in `event=opened branch=feat_x`
|
|
1401
|
+
# reused the stub answering `pr list` with [{"number":12,
|
|
1402
|
+
# "isCrossRepository":false}] -> 0, one line, no create, no comment
|
|
1190
1403
|
#
|
|
1191
1404
|
# collect needs deliver's setup, a story index on feat_x, a stub answering
|
|
1192
1405
|
# `pr list` with [{"number":12,"isCrossRepository":false}], `run list` with a
|
|
@@ -1274,6 +1487,11 @@ RUN_JOB_NAME='run'
|
|
|
1274
1487
|
# How far before the previous round's `collected_at` `round_collect` lists
|
|
1275
1488
|
# from, absorbing runner-clock skew; the id check drops what it re-lists.
|
|
1276
1489
|
ROUND_OVERLAP_SECS=300
|
|
1490
|
+
# How far before a run's `createdAt` `forge_dispatch_engine_var` still trusts
|
|
1491
|
+
# a dispatcher's comment: the comment is posted after the dispatch request and
|
|
1492
|
+
# GitHub creates the run asynchronously, so either may carry the earlier
|
|
1493
|
+
# timestamp. Small enough that an older dispatch's comment falls outside it.
|
|
1494
|
+
DISPATCH_MARKER_SLACK_SECS=120
|
|
1277
1495
|
|
|
1278
1496
|
# GitHub's documented limit on a `workflow_dispatch` inputs payload: "The
|
|
1279
1497
|
# maximum payload for inputs is 65,535 characters."
|
|
@@ -1285,8 +1503,9 @@ EXIT_USAGE=1
|
|
|
1285
1503
|
EXIT_REFUSED=2
|
|
1286
1504
|
EXIT_GH=3
|
|
1287
1505
|
EXIT_PLACEMENT=4
|
|
1288
|
-
# pause-requested only: the read succeeded and found no pause
|
|
1289
|
-
|
|
1506
|
+
# pause-requested only: the read succeeded and found no pause; distinct from
|
|
1507
|
+
# EXIT_USAGE so a caller never reads a refused call as no pause.
|
|
1508
|
+
EXIT_NO_PAUSE=5
|
|
1290
1509
|
|
|
1291
1510
|
usage() {
|
|
1292
1511
|
echo "remote-run.sh: $1" >&2
|
|
@@ -1309,6 +1528,7 @@ usage() {
|
|
|
1309
1528
|
echo " remote-run.sh list [--repo <root>]" >&2
|
|
1310
1529
|
echo " remote-run.sh discard <dir> [--repo <root>]" >&2
|
|
1311
1530
|
echo " remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]" >&2
|
|
1531
|
+
echo " remote-run.sh open <branch> [--repo <root>]" >&2
|
|
1312
1532
|
echo " remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]" >&2
|
|
1313
1533
|
echo " remote-run.sh collect <branch> [--pr <n>] [--repo <root>]" >&2
|
|
1314
1534
|
echo " remote-run.sh control [--repo <root>]" >&2
|
|
@@ -1381,7 +1601,7 @@ verb=""
|
|
|
1381
1601
|
verb="$1"
|
|
1382
1602
|
shift
|
|
1383
1603
|
case "$verb" in
|
|
1384
|
-
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list|discard|report|deliver|collect|control) ;;
|
|
1604
|
+
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list|discard|report|open|deliver|collect|control) ;;
|
|
1385
1605
|
*) usage "unknown verb '$verb'" ;;
|
|
1386
1606
|
esac
|
|
1387
1607
|
|
|
@@ -1606,10 +1826,11 @@ fi
|
|
|
1606
1826
|
# ---------------------------------------------------------------------------
|
|
1607
1827
|
|
|
1608
1828
|
# setup_fail <message> — a configuration problem: exit 1, except for save,
|
|
1609
|
-
# report, deliver and collect, which never fail the step that calls them.
|
|
1829
|
+
# report, open, deliver and collect, which never fail the step that calls them.
|
|
1610
1830
|
setup_fail() {
|
|
1611
1831
|
echo "remote-run.sh: $1" >&2
|
|
1612
|
-
[ "$verb" != save ] && [ "$verb" != report ] && [ "$verb" !=
|
|
1832
|
+
[ "$verb" != save ] && [ "$verb" != report ] && [ "$verb" != open ] && [ "$verb" != deliver ] \
|
|
1833
|
+
&& [ "$verb" != collect ] || exit "$EXIT_OK"
|
|
1613
1834
|
exit "$EXIT_USAGE"
|
|
1614
1835
|
}
|
|
1615
1836
|
|
|
@@ -1617,8 +1838,8 @@ if [ -n "$repo_arg" ]; then
|
|
|
1617
1838
|
root=$(hr_repo_root "$repo_arg") || setup_fail "'$repo_arg' is not a git repository"
|
|
1618
1839
|
elif [ "$verb" = restore ] || [ "$verb" = save ] || [ "$verb" = continue ] || [ "$verb" = poll ] \
|
|
1619
1840
|
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ] \
|
|
1620
|
-
|| [ "$verb" = discard ] || [ "$verb" = report ] || [ "$verb" =
|
|
1621
|
-
|| [ "$verb" = control ]; then
|
|
1841
|
+
|| [ "$verb" = discard ] || [ "$verb" = report ] || [ "$verb" = open ] || [ "$verb" = deliver ] \
|
|
1842
|
+
|| [ "$verb" = collect ] || [ "$verb" = control ]; then
|
|
1622
1843
|
root=$(hr_repo_root "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
1623
1844
|
else
|
|
1624
1845
|
root=$(hr_main_repo "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
@@ -1637,7 +1858,7 @@ case "$verb" in
|
|
|
1637
1858
|
trigger|control)
|
|
1638
1859
|
# Gates itself, after reading the event, so a refusal can still be commented.
|
|
1639
1860
|
;;
|
|
1640
|
-
report|deliver|collect)
|
|
1861
|
+
report|open|deliver|collect)
|
|
1641
1862
|
# Gates itself (`forge_on`) and exits 0 on every outcome.
|
|
1642
1863
|
;;
|
|
1643
1864
|
status|sync)
|
|
@@ -1917,6 +2138,54 @@ expired_line() {
|
|
|
1917
2138
|
printf '%s' "the state bundle of run $1 expired on $BUNDLE_EXPIRES_AT: resume from the committed ledger with $RESUME_HINT $branch, or re-drop the task"
|
|
1918
2139
|
}
|
|
1919
2140
|
|
|
2141
|
+
# run_not_started_var <run_id> — whether GitHub never started the run's
|
|
2142
|
+
# RUN_JOB_NAME job: 0 when that job's `conclusion` is `cancelled` or `failure`
|
|
2143
|
+
# and its `steps` array is absent or empty, with NOT_STARTED_REASON set; 1 when
|
|
2144
|
+
# the job started or none of that name is listed; 2 with GH_ERR set when the
|
|
2145
|
+
# jobs lookup failed or was not the expected JSON. Never exits. The reason is
|
|
2146
|
+
# the first message of the job's check-run annotations (a job's `id` is its
|
|
2147
|
+
# check run's id); that lookup is best-effort, and a failed or empty one falls
|
|
2148
|
+
# back to a line naming the conclusion. Replaces GH_OUT.
|
|
2149
|
+
NOT_STARTED_REASON=""
|
|
2150
|
+
run_not_started_var() {
|
|
2151
|
+
local answer job_id conclusion message
|
|
2152
|
+
NOT_STARTED_REASON=""
|
|
2153
|
+
gh_call api "repos/{owner}/{repo}/actions/runs/$1/jobs" || return 2
|
|
2154
|
+
answer=$(printf '%s' "$GH_OUT" | jq -r --arg n "$RUN_JOB_NAME" '
|
|
2155
|
+
if type == "object" and (.jobs | type) == "array" then
|
|
2156
|
+
([.jobs[] | select(type == "object" and .name == $n)] | first) as $j
|
|
2157
|
+
| if $j == null then "absent"
|
|
2158
|
+
elif ($j.conclusion == "cancelled" or $j.conclusion == "failure")
|
|
2159
|
+
and (($j.steps // []) | if type == "array" then length else 1 end) == 0
|
|
2160
|
+
then "not_started\t" + ($j.id | tostring) + "\t" + $j.conclusion
|
|
2161
|
+
else "started" end
|
|
2162
|
+
else "invalid" end' 2>/dev/null)
|
|
2163
|
+
case "$answer" in
|
|
2164
|
+
absent|started) return 1 ;;
|
|
2165
|
+
not_started$'\t'*) ;;
|
|
2166
|
+
*) GH_ERR="its job list is not the expected JSON"; return 2 ;;
|
|
2167
|
+
esac
|
|
2168
|
+
answer="${answer#*$'\t'}"
|
|
2169
|
+
job_id="${answer%%$'\t'*}"
|
|
2170
|
+
conclusion="${answer#*$'\t'}"
|
|
2171
|
+
message=""
|
|
2172
|
+
case "$job_id" in
|
|
2173
|
+
''|*[!0-9]*) ;;
|
|
2174
|
+
*)
|
|
2175
|
+
if gh_call api "repos/{owner}/{repo}/check-runs/$job_id/annotations"; then
|
|
2176
|
+
message=$(printf '%s' "$GH_OUT" | jq -r '
|
|
2177
|
+
[.[]? | .message? | select(type == "string" and length > 0) | split("\n")[0]] | first // ""' 2>/dev/null) || message=""
|
|
2178
|
+
fi
|
|
2179
|
+
;;
|
|
2180
|
+
esac
|
|
2181
|
+
if [ -n "$message" ]; then
|
|
2182
|
+
NOT_STARTED_REASON="$message"
|
|
2183
|
+
else
|
|
2184
|
+
NOT_STARTED_REASON="its \`$RUN_JOB_NAME\` job ended \`$conclusion\` with no step run"
|
|
2185
|
+
fi
|
|
2186
|
+
return 0
|
|
2187
|
+
}
|
|
2188
|
+
|
|
1920
2189
|
set_or_fail() {
|
|
1921
2190
|
hr_registry_set "$registry" "$branch" "$1" "$2" || {
|
|
1922
2191
|
echo "remote-run.sh: writing $1 of $branch to '$registry' failed" >&2
|
|
@@ -2035,10 +2304,17 @@ sync_expired() {
|
|
|
2035
2304
|
# RS_BUNDLE is then 1. <applied_run_id>, when set, also counts as a bundle
|
|
2036
2305
|
# existing for case 4. <finished> 1 reads the newest run as finished whatever
|
|
2037
2306
|
# its `status`: `branch_settled_var` passes it once that run's `run` job has
|
|
2038
|
-
# completed.
|
|
2307
|
+
# completed. RS_RUN_CREATED_AT is the newest run's `createdAt` (ISO 8601).
|
|
2308
|
+
# RS_NOT_STARTED is 1 only in cases 4 and 5, when `run_not_started_var` finds
|
|
2309
|
+
# GitHub never started that run's RUN_JOB_NAME job; RS_ENGINE is then
|
|
2310
|
+
# `forge_dispatch_engine_var`'s answer, and case 5 with one is `paused` /
|
|
2311
|
+
# `killed`. Replaces GH_OUT in cases 4 and 5. Exits 3 when gh fails, 2
|
|
2312
|
+
# for an unrecognised bundle.
|
|
2039
2313
|
RS_RUNS=""
|
|
2040
2314
|
RS_RUN_ID=""
|
|
2041
2315
|
RS_RUN_URL=""
|
|
2316
|
+
RS_RUN_CREATED_AT=""
|
|
2317
|
+
RS_NOT_STARTED=0
|
|
2042
2318
|
RS_GH_STATUS=""
|
|
2043
2319
|
RS_STATE=""
|
|
2044
2320
|
RS_PAUSE_REASON=""
|
|
@@ -2052,7 +2328,8 @@ remote_state() {
|
|
|
2052
2328
|
local download="${1-}" applied="${2-}" finished="${3-0}" newest status_file older bundle_exists=0
|
|
2053
2329
|
RS_RUNS=""; RS_RUN_ID=""; RS_RUN_URL=""; RS_GH_STATUS=""; RS_STATE=""
|
|
2054
2330
|
RS_PAUSE_REASON=""; RS_DETAIL=""; RS_ENGINE=""; RS_USAGE_RESUME_AT=""
|
|
2055
|
-
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""
|
|
2331
|
+
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""; RS_RUN_CREATED_AT=""
|
|
2332
|
+
RS_NOT_STARTED=0
|
|
2056
2333
|
RS_RUNS=$(titled_runs "harness run $branch") || RS_RUNS='[]'
|
|
2057
2334
|
newest=$(printf '%s' "$RS_RUNS" | jq -c '.[0] // empty')
|
|
2058
2335
|
if [ -z "$newest" ]; then
|
|
@@ -2062,6 +2339,7 @@ remote_state() {
|
|
|
2062
2339
|
RS_RUN_ID=$(printf '%s' "$newest" | jq -r '.databaseId | tostring')
|
|
2063
2340
|
RS_GH_STATUS=$(printf '%s' "$newest" | jq -r '.status // ""')
|
|
2064
2341
|
RS_RUN_URL=$(printf '%s' "$newest" | jq -r '.url // ""')
|
|
2342
|
+
RS_RUN_CREATED_AT=$(printf '%s' "$newest" | jq -r '.createdAt // ""')
|
|
2065
2343
|
if [ "$RS_GH_STATUS" != completed ] && [ "$finished" != 1 ]; then
|
|
2066
2344
|
RS_STATE=running
|
|
2067
2345
|
return 0
|
|
@@ -2129,12 +2407,32 @@ remote_state() {
|
|
|
2129
2407
|
RS_STATE=paused
|
|
2130
2408
|
RS_PAUSE_REASON=killed
|
|
2131
2409
|
RS_DETAIL="run $RS_RUN_ID ended with no state bundle (killed, cancelled or replaced): $RS_RUN_URL"
|
|
2132
|
-
|
|
2410
|
+
else
|
|
2411
|
+
# Case 5 — no bundle in any run.
|
|
2412
|
+
RS_STATE=failed
|
|
2413
|
+
RS_DETAIL="no run of $branch ever uploaded a state bundle; newest: $RS_RUN_URL"
|
|
2133
2414
|
fi
|
|
2134
2415
|
|
|
2135
|
-
#
|
|
2136
|
-
|
|
2137
|
-
|
|
2416
|
+
# Cases 4 and 5 — a job GitHub never started keeps its case's state; only
|
|
2417
|
+
# the detail changes. A failed jobs lookup changes nothing but says so.
|
|
2418
|
+
run_not_started_var "$RS_RUN_ID"
|
|
2419
|
+
case $? in
|
|
2420
|
+
0)
|
|
2421
|
+
RS_NOT_STARTED=1
|
|
2422
|
+
RS_DETAIL="GitHub did not start the job of run $RS_RUN_ID ($NOT_STARTED_REASON): $RS_RUN_URL"
|
|
2423
|
+
# No bundle names the engine, so the dispatch's own comment does; with
|
|
2424
|
+
# one, case 5 becomes resumable from the ledger as case 4 is.
|
|
2425
|
+
if [ -z "$RS_ENGINE" ]; then
|
|
2426
|
+
forge_dispatch_engine_var "$branch" "$RS_RUN_CREATED_AT" || :
|
|
2427
|
+
RS_ENGINE="$FORGE_DISPATCH_ENGINE"
|
|
2428
|
+
if [ "$RS_STATE" = failed ] && [ -n "$RS_ENGINE" ]; then
|
|
2429
|
+
RS_STATE=paused
|
|
2430
|
+
RS_PAUSE_REASON=killed
|
|
2431
|
+
fi
|
|
2432
|
+
fi
|
|
2433
|
+
;;
|
|
2434
|
+
2) echo "remote-run.sh: could not tell whether GitHub started the job of run $RS_RUN_ID: $GH_ERR" >&2 ;;
|
|
2435
|
+
esac
|
|
2138
2436
|
return 0
|
|
2139
2437
|
}
|
|
2140
2438
|
|
|
@@ -2264,10 +2562,16 @@ verb_sync() {
|
|
|
2264
2562
|
return 0
|
|
2265
2563
|
fi
|
|
2266
2564
|
|
|
2267
|
-
# Case 4 — a newer run with no bundle, while some bundle exists
|
|
2565
|
+
# Case 4 — a newer run with no bundle, while some bundle exists; also a run
|
|
2566
|
+
# GitHub never started whose engine its dispatch's comment records.
|
|
2268
2567
|
if [ "$RS_STATE" = paused ]; then
|
|
2269
|
-
|
|
2270
|
-
|
|
2568
|
+
if [ "$RS_NOT_STARTED" = 1 ] && valid_engine "$RS_ENGINE"; then
|
|
2569
|
+
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
2570
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now" engine "$RS_ENGINE"
|
|
2571
|
+
else
|
|
2572
|
+
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
2573
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
2574
|
+
fi
|
|
2271
2575
|
echo "remote-run.sh: run $id of $branch left no bundle; the record is paused (killed), nothing restored"
|
|
2272
2576
|
return 0
|
|
2273
2577
|
fi
|
|
@@ -2521,12 +2825,9 @@ verb_save() {
|
|
|
2521
2825
|
return 0
|
|
2522
2826
|
}
|
|
2523
2827
|
|
|
2524
|
-
#
|
|
2525
|
-
#
|
|
2526
|
-
|
|
2527
|
-
# <forge_note> is the comment's, naming no slash command and no shell command,
|
|
2528
|
-
# and states only what happened, since `forge_report` adds the next action.
|
|
2529
|
-
notify() {
|
|
2828
|
+
# notify_push <event> <branch> <detail> — the push notification alone: it
|
|
2829
|
+
# posts no comment and sets no label. Never fails.
|
|
2830
|
+
notify_push() {
|
|
2530
2831
|
if [ -n "${HARNESS_REMOTE_SLUG-}" ]; then
|
|
2531
2832
|
HARNESS_REPO_SLUG="$HARNESS_REMOTE_SLUG"
|
|
2532
2833
|
export HARNESS_REPO_SLUG
|
|
@@ -2534,6 +2835,15 @@ notify() {
|
|
|
2534
2835
|
bash "$script_dir/autonomous-notify.sh" "$1" "$2" "" "$3" \
|
|
2535
2836
|
|| echo "remote-run.sh: the $1 notification for $2 could not be sent" >&2
|
|
2536
2837
|
echo "remote-run.sh: notified $1 for $2: $3"
|
|
2838
|
+
}
|
|
2839
|
+
|
|
2840
|
+
# notify <event> <branch> <detail> <forge_note> — notify_push, then
|
|
2841
|
+
# `forge_report` of the same event; never fails. Every call site supplies
|
|
2842
|
+
# both texts: <detail> is the push notification's, slash commands included;
|
|
2843
|
+
# <forge_note> is the comment's, naming no slash command and no shell command,
|
|
2844
|
+
# and states only what happened, since `forge_report` adds the next action.
|
|
2845
|
+
notify() {
|
|
2846
|
+
notify_push "$1" "$2" "$3"
|
|
2537
2847
|
# stdin closed: `poll` calls this inside a loop reading its run list.
|
|
2538
2848
|
forge_report "$1" "$2" "$4" </dev/null
|
|
2539
2849
|
}
|
|
@@ -2730,7 +3040,10 @@ verb_continue() {
|
|
|
2730
3040
|
}
|
|
2731
3041
|
|
|
2732
3042
|
# POLL_STATE — the poller state carried between ticks, one object keyed by
|
|
2733
|
-
# branch: {"<branch>": {"run_id", "failures", "notified"}},
|
|
3043
|
+
# branch: {"<branch>": {"run_id", "failures", "notified", "download_failures"}},
|
|
3044
|
+
# every value a string. `failures` counts failed re-dispatches only;
|
|
3045
|
+
# `download_failures`, absent until a download fails, counts consecutive failed
|
|
3046
|
+
# downloads of the run's listed bundle.
|
|
2734
3047
|
POLL_STATE='{}'
|
|
2735
3048
|
POLL_STATE_FILE_NAME='poll_state.json'
|
|
2736
3049
|
|
|
@@ -2738,9 +3051,21 @@ poll_state_get() {
|
|
|
2738
3051
|
printf '%s' "$POLL_STATE" | jq -r --arg b "$1" --arg f "$2" '.[$b][$f] // "" | tostring'
|
|
2739
3052
|
}
|
|
2740
3053
|
|
|
3054
|
+
# poll_state_put <branch> <run_id> <failures> <notified> — keeps the entry's
|
|
3055
|
+
# `download_failures` when its `run_id` is the same.
|
|
2741
3056
|
poll_state_put() {
|
|
2742
|
-
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg f "$3" --arg n "$4"
|
|
2743
|
-
|
|
3057
|
+
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg f "$3" --arg n "$4" '
|
|
3058
|
+
(.[$b] // {}) as $o
|
|
3059
|
+
| .[$b] = {run_id: $r, failures: $f, notified: $n}
|
|
3060
|
+
+ (if $o.run_id == $r and ($o | has("download_failures")) then {download_failures: $o.download_failures} else {} end)')
|
|
3061
|
+
}
|
|
3062
|
+
|
|
3063
|
+
# poll_state_downloads_put <branch> <run_id> <count> — sets `download_failures`,
|
|
3064
|
+
# on a fresh entry when the stored `run_id` differs.
|
|
3065
|
+
poll_state_downloads_put() {
|
|
3066
|
+
POLL_STATE=$(printf '%s' "$POLL_STATE" | jq -c --arg b "$1" --arg r "$2" --arg c "$3" '
|
|
3067
|
+
(.[$b] // {}) as $o
|
|
3068
|
+
| .[$b] = (if $o.run_id == $r then $o else {run_id: $r, failures: "", notified: ""} end) + {download_failures: $c}')
|
|
2744
3069
|
}
|
|
2745
3070
|
|
|
2746
3071
|
poll_state_drop() {
|
|
@@ -2859,7 +3184,7 @@ poll_usage_paused() {
|
|
|
2859
3184
|
# With may_dispatch 0 nothing is sent and nothing notified: a due run that
|
|
2860
3185
|
# would be dispatched counts as waiting, one that would be refused does not.
|
|
2861
3186
|
poll_branch() {
|
|
2862
|
-
local id="$1" state="$2" may_dispatch="$3" at engine_value now failures
|
|
3187
|
+
local id="$1" state="$2" may_dispatch="$3" at engine_value now failures downloads listed
|
|
2863
3188
|
if [ "$may_dispatch" -eq 1 ] && [ -n "$(poll_state_get "$branch" run_id)" ] \
|
|
2864
3189
|
&& [ "$(poll_state_get "$branch" run_id)" != "$id" ]; then
|
|
2865
3190
|
poll_state_drop "$branch"
|
|
@@ -2894,10 +3219,40 @@ poll_branch() {
|
|
|
2894
3219
|
echo "remote-run.sh: poll: $branch's run $id was already reported as not re-dispatchable; skipped"
|
|
2895
3220
|
return 1
|
|
2896
3221
|
fi
|
|
2897
|
-
|
|
2898
|
-
|
|
3222
|
+
bundle_listed "$id"
|
|
3223
|
+
listed=$?
|
|
3224
|
+
if [ "$listed" -eq 1 ]; then
|
|
3225
|
+
if run_not_started_var "$id"; then
|
|
3226
|
+
echo "remote-run.sh: poll: run $id of $branch never started ($NOT_STARTED_REASON); its collect job reports it; not waiting"
|
|
3227
|
+
else
|
|
3228
|
+
echo "remote-run.sh: poll: $branch skipped"
|
|
3229
|
+
fi
|
|
3230
|
+
return 1
|
|
3231
|
+
fi
|
|
3232
|
+
[ "$listed" -ne 2 ] || echo "remote-run.sh: poll: reading the artifacts of run $id ($branch) failed ($GH_ERR)"
|
|
3233
|
+
downloads=""
|
|
3234
|
+
[ "$(poll_state_get "$branch" run_id)" != "$id" ] || downloads=$(poll_state_get "$branch" download_failures)
|
|
3235
|
+
if [ "$listed" -eq 2 ] || ! poll_fetch "$id"; then
|
|
3236
|
+
case "$downloads" in
|
|
3237
|
+
''|*[!0-9]*) downloads=0 ;;
|
|
3238
|
+
esac
|
|
3239
|
+
downloads=$((10#$downloads + 1))
|
|
3240
|
+
if [ "$downloads" -lt "$((10#$POLL_MAX_FAILURES))" ]; then
|
|
3241
|
+
[ "$may_dispatch" -eq 0 ] || poll_state_downloads_put "$branch" "$id" "$downloads"
|
|
3242
|
+
echo "remote-run.sh: poll: the bundle of run $id ($branch) could not be downloaded, attempt $downloads of $POLL_MAX_FAILURES; still waiting"
|
|
3243
|
+
return 0
|
|
3244
|
+
fi
|
|
3245
|
+
if [ "$may_dispatch" -eq 1 ]; then
|
|
3246
|
+
poll_state_put "$branch" "$id" "$(poll_state_get "$branch" failures)" 1
|
|
3247
|
+
# Push only, under a word outside autonomous-notify.sh's events: the run's
|
|
3248
|
+
# state is unknown, so neither a `failed` report nor a label is true of it.
|
|
3249
|
+
notify_push bundle_unreadable "$branch" "The resume poller could not download the state bundle of run $id after $downloads attempts ($GH_ERR); it no longer watches $branch. Its state is unknown: check the run, then run $RESUME_HINT $branch if it is paused; $(hr_github_resume_route "$branch" "")."
|
|
3250
|
+
fi
|
|
2899
3251
|
return 1
|
|
2900
3252
|
fi
|
|
3253
|
+
if [ -n "$downloads" ] && [ "$may_dispatch" -eq 1 ]; then
|
|
3254
|
+
poll_state_downloads_put "$branch" "$id" ""
|
|
3255
|
+
fi
|
|
2901
3256
|
poll_usage_paused || return 1
|
|
2902
3257
|
at=$(hr_remote_status_get "$POLL_STATUS_FILE" usage_resume_at) || at=""
|
|
2903
3258
|
case "$at" in
|
|
@@ -3131,8 +3486,12 @@ verb_start() {
|
|
|
3131
3486
|
status=0
|
|
3132
3487
|
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$subject" >&2 || status=$?
|
|
3133
3488
|
[ "$status" -ne 1 ] || placement_fail "committing '$rel'"
|
|
3134
|
-
|
|
3135
|
-
|
|
3489
|
+
status=0
|
|
3490
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 || status=$?
|
|
3491
|
+
[ "$status" -ne 2 ] \
|
|
3492
|
+
|| placement_fail "pushing $branch: origin/$branch moved to $HR_PUSH_REMOTE_TIP, which this branch does not have (something else pushed)"
|
|
3493
|
+
[ "$status" -eq 0 ] \
|
|
3494
|
+
|| placement_fail "pushing $branch: the remote refused the push (push-branch.sh's lines in this job's log name why)"
|
|
3136
3495
|
start_remove_copy
|
|
3137
3496
|
trap - EXIT
|
|
3138
3497
|
|
|
@@ -3285,8 +3644,12 @@ NAMES
|
|
|
3285
3644
|
status=0
|
|
3286
3645
|
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$(hr_user_review_subject "$branch")" >&2 || status=$?
|
|
3287
3646
|
[ "$status" -eq 0 ] || review_fail "committing '$rel'"
|
|
3288
|
-
|
|
3289
|
-
|
|
3647
|
+
status=0
|
|
3648
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 || status=$?
|
|
3649
|
+
[ "$status" -ne 2 ] \
|
|
3650
|
+
|| review_fail "pushing $branch: origin/$branch moved to $HR_PUSH_REMOTE_TIP, which this branch does not have (something else pushed)"
|
|
3651
|
+
[ "$status" -eq 0 ] \
|
|
3652
|
+
|| review_fail "pushing $branch: the remote refused the push (push-branch.sh's lines in this job's log name why)"
|
|
3290
3653
|
pushed=$(git -C "$worktree" rev-parse HEAD 2>/dev/null) || pushed=""
|
|
3291
3654
|
if [ "$use_mirror" -eq 0 ]; then
|
|
3292
3655
|
start_remove_copy
|
|
@@ -3865,9 +4228,12 @@ The task is the dispatch's \`client_payload\` title and body. Sending the same d
|
|
|
3865
4228
|
# never exits, and reports a failure as one `remote-run.sh: …` line on stderr.
|
|
3866
4229
|
#
|
|
3867
4230
|
# THE TARGET RULE. A comment goes to the open same-repository pull request
|
|
3868
|
-
# whose head is the branch when `forge_recognised` holds for that branch
|
|
3869
|
-
#
|
|
3870
|
-
#
|
|
4231
|
+
# whose head is the branch when `forge_recognised` holds for that branch — its
|
|
4232
|
+
# origin tip carries its task prompt or its flow-progress ledger — else to the
|
|
4233
|
+
# issue the run was started from (`FORGE_ISSUE`), else nowhere. The task prompt
|
|
4234
|
+
# counts so that a pull request opened at the run's start is a target before
|
|
4235
|
+
# the ledger's first push. The state label goes on that issue and on that pull
|
|
4236
|
+
# request, each when known.
|
|
3871
4237
|
#
|
|
3872
4238
|
# THE LABEL IS A VIEW, NEVER AN AUTHORITY. The run list is the authority
|
|
3873
4239
|
# (`remote_state`); the harness overwrites any state label set by hand, and
|
|
@@ -3926,14 +4292,12 @@ forge_fetch_branch() {
|
|
|
3926
4292
|
return 0
|
|
3927
4293
|
}
|
|
3928
4294
|
|
|
3929
|
-
# forge_marker <event> <branch> [<question>] — the one
|
|
3930
|
-
# marker line, built from COMMENT_MARKER
|
|
4295
|
+
# forge_marker <event> <branch> [<question> [<engine> [<round>]]] — the one
|
|
4296
|
+
# producer of a comment's marker line, built from COMMENT_MARKER: ` question=<n>`,
|
|
4297
|
+
# ` engine=<engine>` then ` round=<n>`, each only when non-empty, before ` -->`.
|
|
3931
4298
|
forge_marker() {
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
else
|
|
3935
|
-
printf '%s event=%s branch=%s -->\n' "$COMMENT_MARKER" "$1" "$2"
|
|
3936
|
-
fi
|
|
4299
|
+
printf '%s event=%s branch=%s%s%s%s -->\n' "$COMMENT_MARKER" "$1" "$2" \
|
|
4300
|
+
"${3:+ question=$3}" "${4:+ engine=$4}" "${5:+ round=$5}"
|
|
3937
4301
|
}
|
|
3938
4302
|
|
|
3939
4303
|
# forge_issue_var <branch> — FORGE_ISSUE from the last provenance line
|
|
@@ -4014,38 +4378,105 @@ forge_provenance_parse() {
|
|
|
4014
4378
|
}
|
|
4015
4379
|
|
|
4016
4380
|
# forge_recognised <branch> — the harness-branch test, read from committed
|
|
4017
|
-
# state: 0 when origin's copy of the branch carries its
|
|
4381
|
+
# state: 0 when origin's copy of the branch carries its task prompt or its
|
|
4382
|
+
# flow-progress ledger, 1 otherwise.
|
|
4018
4383
|
forge_recognised() {
|
|
4019
4384
|
local state_rel
|
|
4020
4385
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
4021
4386
|
[ -n "$state_rel" ] || return 1
|
|
4022
|
-
git -C "$root" cat-file -e "refs/remotes/origin/$1:${state_rel%/}/flow_progress/$1_progress.md" 2>/dev/null
|
|
4387
|
+
git -C "$root" cat-file -e "refs/remotes/origin/$1:${state_rel%/}/flow_progress/$1_progress.md" 2>/dev/null \
|
|
4388
|
+
|| git -C "$root" cat-file -e "refs/remotes/origin/$1:$(hr_task_prompt_rel "$state_rel" "$1")" 2>/dev/null
|
|
4023
4389
|
}
|
|
4024
4390
|
|
|
4025
4391
|
# forge_pr_var <branch> — FORGE_PR, the open pull request whose head is
|
|
4026
|
-
# <branch> in this repository (a fork's same-named head is skipped), or empty
|
|
4392
|
+
# <branch> in this repository (a fork's same-named head is skipped), or empty;
|
|
4393
|
+
# FORGE_PR_DRAFT, that same pull request's `isDraft` as `true` or `false`, or
|
|
4394
|
+
# empty when FORGE_PR is.
|
|
4027
4395
|
FORGE_PR=""
|
|
4396
|
+
FORGE_PR_DRAFT=""
|
|
4028
4397
|
forge_pr_var() {
|
|
4398
|
+
local sel
|
|
4029
4399
|
FORGE_PR=""
|
|
4030
|
-
|
|
4400
|
+
FORGE_PR_DRAFT=""
|
|
4401
|
+
if ! gh_call pr list --repo "$FORGE_REPO" --head "$1" --state open --json number,isCrossRepository,isDraft --limit 10; then
|
|
4031
4402
|
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
4032
4403
|
return 1
|
|
4033
4404
|
fi
|
|
4034
|
-
if !
|
|
4035
|
-
'if type == "array" then [.[] | select(.isCrossRepository == false
|
|
4036
|
-
FORGE_PR=""
|
|
4405
|
+
if ! sel=$(printf '%s' "$GH_OUT" | jq -r \
|
|
4406
|
+
'if type == "array" then [.[] | select(.isCrossRepository == false and (.number | type) == "number")] | first // empty | "\(.number) \(.isDraft == true)" else error end' 2>/dev/null); then
|
|
4037
4407
|
GH_ERR="its pr list is not the expected JSON"
|
|
4038
4408
|
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
4039
4409
|
return 1
|
|
4040
4410
|
fi
|
|
4411
|
+
if [ -n "$sel" ]; then
|
|
4412
|
+
FORGE_PR="${sel%% *}"
|
|
4413
|
+
FORGE_PR_DRAFT="${sel#* }"
|
|
4414
|
+
fi
|
|
4415
|
+
return 0
|
|
4416
|
+
}
|
|
4417
|
+
|
|
4418
|
+
# forge_dispatch_engine_var <branch> <created_at_iso> — FORGE_DISPATCH_ENGINE,
|
|
4419
|
+
# the engine the dispatch that created the run at <created_at_iso> recorded in
|
|
4420
|
+
# its comment on the branch's issue or open pull request, or empty. Only a
|
|
4421
|
+
# `github-actions[bot]` comment counts, whose last non-empty line is exactly a
|
|
4422
|
+
# `started` or `round` marker for <branch>, either with or without an engine,
|
|
4423
|
+
# or a `reply` marker for <branch> carrying an engine, and whose `created_at`
|
|
4424
|
+
# is no earlier than <created_at_iso> less DISPATCH_MARKER_SLACK_SECS. The
|
|
4425
|
+
# newest one answers: `started` is `task`, `round` is `user_review`, `reply`
|
|
4426
|
+
# its own engine. 1 when the forge is off or the repository is unknown. Fetches
|
|
4427
|
+
# origin's <branch> into the root checkout; replaces GH_OUT.
|
|
4428
|
+
FORGE_DISPATCH_ENGINE=""
|
|
4429
|
+
forge_dispatch_engine_var() {
|
|
4430
|
+
local b="$1" created="${2-}" since markers listed="" n engine answer
|
|
4431
|
+
FORGE_DISPATCH_ENGINE=""
|
|
4432
|
+
forge_on || return 1
|
|
4433
|
+
forge_repo_var || return 1
|
|
4434
|
+
if ! since=$(jq -n -r --arg t "$created" --argjson o "$DISPATCH_MARKER_SLACK_SECS" '($t | fromdateiso8601) - $o' 2>/dev/null) \
|
|
4435
|
+
|| [ -z "$since" ]; then
|
|
4436
|
+
echo "remote-run.sh: the newest run of $b has no readable createdAt ('$created'); its dispatch's engine is not read" >&2
|
|
4437
|
+
return 0
|
|
4438
|
+
fi
|
|
4439
|
+
# Every marker line that qualifies, each with the engine it answers.
|
|
4440
|
+
markers=""
|
|
4441
|
+
for engine in task user_review docs; do
|
|
4442
|
+
markers="$markers$(forge_marker started "$b" "" "$engine")"$'\t'task$'\n'
|
|
4443
|
+
markers="$markers$(forge_marker round "$b" "" "$engine")"$'\t'user_review$'\n'
|
|
4444
|
+
markers="$markers$(forge_marker reply "$b" "" "$engine")"$'\t'"$engine"$'\n'
|
|
4445
|
+
done
|
|
4446
|
+
markers="$markers$(forge_marker started "$b")"$'\t'task$'\n'
|
|
4447
|
+
markers="$markers$(forge_marker round "$b")"$'\t'user_review$'\n'
|
|
4448
|
+
markers=$(printf '%s' "$markers" | jq -R -s -c \
|
|
4449
|
+
'[split("\n")[] | select(length > 0) | split("\t") | {(.[0]): .[1]}] | add')
|
|
4450
|
+
forge_fetch_branch "$b"
|
|
4451
|
+
forge_issue_var "$b" || FORGE_ISSUE=""
|
|
4452
|
+
forge_pr_var "$b" || FORGE_PR=""
|
|
4453
|
+
for n in $FORGE_ISSUE $FORGE_PR; do
|
|
4454
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$n/comments" --jq '.[] | {login: .user.login, at: .created_at, body: .body}'; then
|
|
4455
|
+
echo "remote-run.sh: listing the comments of #$n failed, so the engine of $b's dispatch is not read: $GH_ERR" >&2
|
|
4456
|
+
return 0
|
|
4457
|
+
fi
|
|
4458
|
+
listed="$listed$GH_OUT"$'\n'
|
|
4459
|
+
done
|
|
4460
|
+
if ! answer=$(printf '%s' "$listed" | jq -s -r --argjson m "$markers" --argjson since "$since" '
|
|
4461
|
+
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.at | type) == "string")
|
|
4462
|
+
| ((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "") as $last
|
|
4463
|
+
| select($m[$last] != null)
|
|
4464
|
+
| (.at | try fromdateiso8601 catch null) as $t
|
|
4465
|
+
| select($t != null and $t >= $since)
|
|
4466
|
+
| {t: $t, e: $m[$last]}]
|
|
4467
|
+
| sort_by(.t) | last | .e // ""' 2>/dev/null); then
|
|
4468
|
+
echo "remote-run.sh: the comments of $b's issue and pull request are not the expected JSON, so the engine of its dispatch is not read" >&2
|
|
4469
|
+
return 0
|
|
4470
|
+
fi
|
|
4471
|
+
FORGE_DISPATCH_ENGINE="$answer"
|
|
4041
4472
|
return 0
|
|
4042
4473
|
}
|
|
4043
4474
|
|
|
4044
|
-
# forge_comment <number> <event> <branch> <body_file> [<question>
|
|
4045
|
-
# marker to <body_file> and post it on issue or pull request <number>.
|
|
4475
|
+
# forge_comment <number> <event> <branch> <body_file> [<question> [<engine> [<round>]]]
|
|
4476
|
+
# — append the marker to <body_file> and post it on issue or pull request <number>.
|
|
4046
4477
|
forge_comment() {
|
|
4047
|
-
local number="$1" event="$2" branch="$3" file="$4" question="${5-}" status
|
|
4048
|
-
if ! { printf '\n'; forge_marker "$event" "$branch" "$question"; } >>"$file"; then
|
|
4478
|
+
local number="$1" event="$2" branch="$3" file="$4" question="${5-}" engine="${6-}" round="${7-}" status
|
|
4479
|
+
if ! { printf '\n'; forge_marker "$event" "$branch" "$question" "$engine" "$round"; } >>"$file"; then
|
|
4049
4480
|
echo "remote-run.sh: cannot append the marker to '$file'" >&2
|
|
4050
4481
|
return 1
|
|
4051
4482
|
fi
|
|
@@ -4160,13 +4591,20 @@ forge_question_body() {
|
|
|
4160
4591
|
# that is the target whatever its state; `gone`, the branch deleted on GitHub,
|
|
4161
4592
|
# its issue read from the task prompt at <sha>. Every event but `stopped` is
|
|
4162
4593
|
# withheld when the branch's newest `harness stop` run is newer than its newest
|
|
4163
|
-
# `harness run` run, read from a fresh listing.
|
|
4594
|
+
# `harness run` run, read from a fresh listing. `not_started` reads
|
|
4595
|
+
# REPORT_NOT_STARTED_STATE (`paused`, else `failed`) and
|
|
4596
|
+
# REPORT_NOT_STARTED_ENGINE (empty or the recovered engine), set by its caller.
|
|
4597
|
+
# Always 0.
|
|
4598
|
+
REPORT_NOT_STARTED_STATE=""
|
|
4599
|
+
REPORT_NOT_STARTED_ENGINE=""
|
|
4164
4600
|
forge_report() {
|
|
4165
4601
|
local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
|
|
4166
|
-
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
|
|
4602
|
+
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n route engine=""
|
|
4167
4603
|
case "$event" in
|
|
4168
4604
|
parked|park_loop) state=parked ;;
|
|
4169
4605
|
paused) state=paused ;;
|
|
4606
|
+
not_started)
|
|
4607
|
+
if [ "$REPORT_NOT_STARTED_STATE" = paused ]; then state=paused; else state=failed; fi ;;
|
|
4170
4608
|
resumed) state=running ;;
|
|
4171
4609
|
failed) state=failed ;;
|
|
4172
4610
|
stopped) state=stopped ;;
|
|
@@ -4216,7 +4654,7 @@ forge_report() {
|
|
|
4216
4654
|
forge_pr_var "$br" || FORGE_PR=""
|
|
4217
4655
|
fi
|
|
4218
4656
|
if [ -z "$pr" ] && [ -n "$FORGE_PR" ] && ! forge_recognised "$br"; then
|
|
4219
|
-
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries
|
|
4657
|
+
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries neither a task prompt nor a flow-progress ledger; it is not a target"
|
|
4220
4658
|
FORGE_PR=""
|
|
4221
4659
|
fi
|
|
4222
4660
|
if [ -n "$FORGE_PR" ]; then
|
|
@@ -4233,6 +4671,7 @@ forge_report() {
|
|
|
4233
4671
|
if [ -n "$registry_file" ] && [ -f "$registry_file" ]; then
|
|
4234
4672
|
reason=$(hr_registry_get "$registry_file" "$br" pause_reason)
|
|
4235
4673
|
resume_at=$(hr_registry_get "$registry_file" "$br" usage_resume_at)
|
|
4674
|
+
[ "$event" != failed ] || engine=$(hr_registry_get "$registry_file" "$br" engine)
|
|
4236
4675
|
fi
|
|
4237
4676
|
|
|
4238
4677
|
case "$event" in
|
|
@@ -4248,10 +4687,13 @@ forge_report() {
|
|
|
4248
4687
|
resumed)
|
|
4249
4688
|
text="The harness run on \`$br\` resumed." ;;
|
|
4250
4689
|
failed)
|
|
4251
|
-
|
|
4252
|
-
|
|
4690
|
+
trigger_label="${FORGE_TRIGGER_LABEL:-${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}}"
|
|
4691
|
+
if [ "$kind" = pr ] && [ "$engine" = user_review ]; then
|
|
4692
|
+
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. To start again, submit a review on this pull request requesting changes. This pull request stays open."
|
|
4693
|
+
elif [ "$kind" = pr ]; then
|
|
4694
|
+
# No review route: a round would start over a task run that never completed.
|
|
4695
|
+
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. This draft pull request stays open: close it to discard the run${FORGE_ISSUE:+, or re-apply the label \`$trigger_label\` to issue #$FORGE_ISSUE to start a new run on the next indexed branch}."
|
|
4253
4696
|
else
|
|
4254
|
-
trigger_label="${FORGE_TRIGGER_LABEL:-${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}}"
|
|
4255
4697
|
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. To start again, re-apply the label \`$trigger_label\` to this issue; that starts a new run, on the next indexed branch."
|
|
4256
4698
|
fi ;;
|
|
4257
4699
|
stopped)
|
|
@@ -4262,9 +4704,28 @@ forge_report() {
|
|
|
4262
4704
|
text="The harness run on \`$br\` was stopped. While the branch exists, reopen this pull request and comment \`${COMMAND_HANDLE} resume\` here, or comment it on the run's issue, to continue it from its committed ledger. A merged pull request cannot be reopened; after a merge, use the issue."
|
|
4263
4705
|
else
|
|
4264
4706
|
text="The harness run on \`$br\` was stopped. Comment \`${COMMAND_HANDLE} resume\` to continue it from its committed ledger. A review that requests changes is collected now, and its round starts once the resumed run finishes."
|
|
4707
|
+
[ "$kind" != pr ] || text="$text Its draft pull request stays open; closing it discards the run."
|
|
4265
4708
|
fi ;;
|
|
4266
4709
|
round)
|
|
4267
|
-
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again."
|
|
4710
|
+
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again."
|
|
4711
|
+
# The undo runs before the comment is written, so the comment states its outcome.
|
|
4712
|
+
if [ "$kind" = pr ] && [ "$FORGE_PR_DRAFT" = false ]; then
|
|
4713
|
+
if gh_call pr ready "$FORGE_PR" --repo "$FORGE_REPO" --undo; then
|
|
4714
|
+
text="$text This pull request is a draft again until the round completes."
|
|
4715
|
+
else
|
|
4716
|
+
echo "::warning::remote-run.sh: report: turning pull request #$FORGE_PR back to a draft was refused: $GH_ERR"
|
|
4717
|
+
text="$text Turning this pull request back to a draft was refused, so it stays ready for review while the round works."
|
|
4718
|
+
fi
|
|
4719
|
+
fi ;;
|
|
4720
|
+
not_started)
|
|
4721
|
+
text="GitHub did not start the job of the harness run on \`$br\`, so nothing ran and the branch is unchanged."
|
|
4722
|
+
if [ "$state" = paused ] && [ -n "$REPORT_NOT_STARTED_ENGINE" ]; then
|
|
4723
|
+
text="$text Comment \`${COMMAND_HANDLE} resume\` to start it again."
|
|
4724
|
+
else
|
|
4725
|
+
# A job that never started uploaded no artifact to read an engine from.
|
|
4726
|
+
route=$(hr_github_resume_route "$br" "${REPORT_NOT_STARTED_ENGINE:-<task, user_review or docs: the one the run was started with>}")
|
|
4727
|
+
text="$text Start it again with the **Run workflow** form: ${route#or from GitHub: }."
|
|
4728
|
+
fi ;;
|
|
4268
4729
|
esac
|
|
4269
4730
|
|
|
4270
4731
|
OPEN_QUESTIONS=""
|
|
@@ -4316,8 +4777,122 @@ forge_report() {
|
|
|
4316
4777
|
return 0
|
|
4317
4778
|
}
|
|
4318
4779
|
|
|
4780
|
+
# forge_progress <branch> — upsert the one progress comment of the run or round
|
|
4781
|
+
# on its pull request, rendered from the job checkout's flow-progress ledger:
|
|
4782
|
+
# created when none is listed, edited in place only when its body differs. No
|
|
4783
|
+
# issue fallback, no label. Always 0.
|
|
4784
|
+
forge_progress() {
|
|
4785
|
+
local br="$1" status state_rel phases engine round p line i first=1 marker
|
|
4786
|
+
local tmp made_tmp="" file pick id same
|
|
4787
|
+
local -a labels states
|
|
4788
|
+
if ! forge_on; then
|
|
4789
|
+
echo "remote-run.sh: report: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
4790
|
+
return 0
|
|
4791
|
+
fi
|
|
4792
|
+
status=0
|
|
4793
|
+
hr_progress_comments "$root" || status=$?
|
|
4794
|
+
case "$status" in
|
|
4795
|
+
0) ;;
|
|
4796
|
+
1) echo "remote-run.sh: report: progress comments are off by execution.progressComments; nothing posted"; return 0 ;;
|
|
4797
|
+
*) echo "remote-run.sh: report: execution.progressComments is unreadable; nothing posted"; return 0 ;;
|
|
4798
|
+
esac
|
|
4799
|
+
forge_repo_var || return 0
|
|
4800
|
+
ALL_RUNS_LISTED=0
|
|
4801
|
+
status=0
|
|
4802
|
+
remote_branch_stopped "$br" || status=$?
|
|
4803
|
+
if [ "$status" -eq 0 ]; then
|
|
4804
|
+
echo "remote-run.sh: report: $br was stopped, and the stop already reported the run; nothing posted"
|
|
4805
|
+
return 0
|
|
4806
|
+
fi
|
|
4807
|
+
[ "$status" -eq 1 ] \
|
|
4808
|
+
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the progress" >&2
|
|
4809
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
4810
|
+
if [ -z "$state_rel" ] || ! phases=$(hr_ledger_phases "$root/${state_rel%/}/flow_progress/${br}_progress.md"); then
|
|
4811
|
+
echo "remote-run.sh: report: $br has no task or user-review ledger; nothing posted"
|
|
4812
|
+
return 0
|
|
4813
|
+
fi
|
|
4814
|
+
read -r engine round states[0] states[1] states[2] states[3] <<<"$phases"
|
|
4815
|
+
forge_fetch_branch "$br"
|
|
4816
|
+
forge_pr_var "$br" || FORGE_PR=""
|
|
4817
|
+
if [ -z "$FORGE_PR" ] || ! forge_recognised "$br"; then
|
|
4818
|
+
echo "remote-run.sh: report: $br has no recognised open pull request; nothing posted"
|
|
4819
|
+
return 0
|
|
4820
|
+
fi
|
|
4821
|
+
|
|
4822
|
+
if [ "$engine" = user_review ]; then
|
|
4823
|
+
labels=('Fix plan' 'Fix implementation' 'Branch review' 'Done')
|
|
4824
|
+
line="Progress of user-review round $round on \`$br\`:"
|
|
4825
|
+
else
|
|
4826
|
+
labels=('Planning' 'Implementation' 'Branch review' 'Done')
|
|
4827
|
+
line="Progress of the harness run on \`$br\`:"
|
|
4828
|
+
round=""
|
|
4829
|
+
fi
|
|
4830
|
+
marker=$(forge_marker progress "$br" "" "" "$round")
|
|
4831
|
+
|
|
4832
|
+
tmp="${RUNNER_TEMP-}"
|
|
4833
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
4834
|
+
tmp=$(mktemp -d) || tmp=""
|
|
4835
|
+
made_tmp="$tmp"
|
|
4836
|
+
fi
|
|
4837
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-progress-comment.XXXXXX"); then
|
|
4838
|
+
echo "::warning::remote-run.sh: report: cannot create the progress comment file for #$FORGE_PR; nothing posted"
|
|
4839
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4840
|
+
return 0
|
|
4841
|
+
fi
|
|
4842
|
+
# No timestamp and no run URL: an unchanged ledger must render byte-identical.
|
|
4843
|
+
# Plain list items: a `- [ ]` box is clickable by anyone with write access.
|
|
4844
|
+
{
|
|
4845
|
+
printf '%s\n\n' "$line"
|
|
4846
|
+
for i in 0 1 2 3; do
|
|
4847
|
+
if [ "${states[$i]}" = done ]; then
|
|
4848
|
+
p=done
|
|
4849
|
+
elif [ "$first" -eq 1 ]; then
|
|
4850
|
+
p="in progress"; first=0
|
|
4851
|
+
else
|
|
4852
|
+
p="not started"
|
|
4853
|
+
fi
|
|
4854
|
+
printf -- '- %s: %s\n' "${labels[$i]}" "$p"
|
|
4855
|
+
done
|
|
4856
|
+
} >"$file"
|
|
4857
|
+
{ cat "$file"; printf '\n%s\n' "$marker"; } >"$file.full"
|
|
4858
|
+
|
|
4859
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$FORGE_PR/comments" --jq '.[] | {id, login: .user.login, body}'; then
|
|
4860
|
+
echo "::warning::remote-run.sh: report: listing the comments of #$FORGE_PR was refused, so the progress is not posted: $GH_ERR"
|
|
4861
|
+
elif ! pick=$(printf '%s' "$GH_OUT" | jq -s -r --arg m "$marker" --arg want "$(cat "$file.full")" '
|
|
4862
|
+
def norm: gsub("\r"; "") | sub("\n+$"; "");
|
|
4863
|
+
[.[] | select(type == "object" and .login == "github-actions[bot]" and (.id | type) == "number")
|
|
4864
|
+
| select(((.body // "") | gsub("\r"; "") | split("\n") | map(select(test("^\\s*$") | not)) | last // "") == $m)]
|
|
4865
|
+
| max_by(.id) // empty
|
|
4866
|
+
| "\(.id) \(if ((.body // "") | norm) == ($want | norm) then "same" else "differs" end)"' 2>/dev/null); then
|
|
4867
|
+
echo "::warning::remote-run.sh: report: the comments of #$FORGE_PR are not the expected JSON, so the progress is not posted"
|
|
4868
|
+
elif [ -z "$pick" ]; then
|
|
4869
|
+
if forge_comment "$FORGE_PR" progress "$br" "$file" "" "" "$round"; then
|
|
4870
|
+
echo "remote-run.sh: report: progress on $br posted on #$FORGE_PR"
|
|
4871
|
+
else
|
|
4872
|
+
echo "::warning::remote-run.sh: report: posting the progress comment on #$FORGE_PR was refused: $GH_ERR"
|
|
4873
|
+
fi
|
|
4874
|
+
else
|
|
4875
|
+
id="${pick%% *}"
|
|
4876
|
+
same="${pick#* }"
|
|
4877
|
+
if [ "$same" = same ]; then
|
|
4878
|
+
echo "remote-run.sh: report: the progress comment on #$FORGE_PR is already current"
|
|
4879
|
+
elif gh_call api --method PATCH "repos/$FORGE_REPO/issues/comments/$id" -F "body=@$file.full"; then
|
|
4880
|
+
echo "remote-run.sh: report: progress on $br edited in comment $id on #$FORGE_PR"
|
|
4881
|
+
else
|
|
4882
|
+
echo "::warning::remote-run.sh: report: editing the progress comment $id on #$FORGE_PR was refused: $GH_ERR"
|
|
4883
|
+
fi
|
|
4884
|
+
fi
|
|
4885
|
+
rm -f "$file" "$file.full"
|
|
4886
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4887
|
+
return 0
|
|
4888
|
+
}
|
|
4889
|
+
|
|
4319
4890
|
verb_report() {
|
|
4320
|
-
|
|
4891
|
+
if [ "$report_event" = progress ]; then
|
|
4892
|
+
forge_progress "$branch"
|
|
4893
|
+
else
|
|
4894
|
+
forge_report "$report_event" "$branch" "$report_note"
|
|
4895
|
+
fi
|
|
4321
4896
|
exit "$EXIT_OK"
|
|
4322
4897
|
}
|
|
4323
4898
|
|
|
@@ -4352,7 +4927,7 @@ deliver_pr_body() {
|
|
|
4352
4927
|
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
4353
4928
|
done
|
|
4354
4929
|
{
|
|
4355
|
-
printf 'This pull request carries the harness run on `%s
|
|
4930
|
+
printf 'This pull request carries the harness run on `%s`. It stays a draft while the run works, and the harness marks it ready for review when the run completes; the harness never merges it.\n' "$2"
|
|
4356
4931
|
[ -z "$FORGE_ISSUE" ] || printf '\nStarted from #%s.\n' "$FORGE_ISSUE"
|
|
4357
4932
|
printf '\nA review that requests changes starts a user-review round on this branch.\n'
|
|
4358
4933
|
printf 'Comment %s to act on the run; each says when it applies (docs/github-run-control.md in the harness documentation).\n' "$verbs"
|
|
@@ -4374,11 +4949,280 @@ deliver_create() {
|
|
|
4374
4949
|
fi
|
|
4375
4950
|
}
|
|
4376
4951
|
|
|
4377
|
-
#
|
|
4378
|
-
#
|
|
4952
|
+
# forge_open_pr <branch> — the one opener of a run's pull request, against
|
|
4953
|
+
# `defaultBranch`: a `--draft` create, then on any failure but
|
|
4954
|
+
# PR_CREATE_FORBIDDEN one retry without `--draft`. The caller has set
|
|
4955
|
+
# FORGE_REPO, FORGE_SERVER and FORGE_ISSUE. 0 when opened, setting FORGE_PR,
|
|
4956
|
+
# FORGE_PR_URL and FORGE_PR_DRAFT (`false` after the retry); 1 when not,
|
|
4957
|
+
# setting OPEN_ERR (one line) and OPEN_FORBIDDEN (1 on PR_CREATE_FORBIDDEN).
|
|
4958
|
+
# Always sets OPEN_BASE, empty when unreadable. Never posts, never exits.
|
|
4959
|
+
FORGE_PR_URL=""
|
|
4960
|
+
OPEN_ERR=""
|
|
4961
|
+
OPEN_FORBIDDEN=0
|
|
4962
|
+
OPEN_BASE=""
|
|
4963
|
+
forge_open_pr() {
|
|
4964
|
+
local br="$1" tmp made_tmp="" body="" draft=""
|
|
4965
|
+
FORGE_PR=""
|
|
4966
|
+
FORGE_PR_URL=""
|
|
4967
|
+
FORGE_PR_DRAFT=""
|
|
4968
|
+
OPEN_ERR=""
|
|
4969
|
+
OPEN_FORBIDDEN=0
|
|
4970
|
+
OPEN_BASE=$(hr_default_branch "$root" 2>/dev/null) || OPEN_BASE=""
|
|
4971
|
+
deliver_title_var "$br"
|
|
4972
|
+
if [ -z "$OPEN_BASE" ]; then
|
|
4973
|
+
OPEN_ERR="the configured defaultBranch could not be read"
|
|
4974
|
+
return 1
|
|
4975
|
+
fi
|
|
4976
|
+
tmp="${RUNNER_TEMP-}"
|
|
4977
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
4978
|
+
tmp=$(mktemp -d) || tmp=""
|
|
4979
|
+
made_tmp="$tmp"
|
|
4980
|
+
fi
|
|
4981
|
+
if [ -z "$tmp" ] || ! body=$(mktemp "$tmp/harness-pr-body.XXXXXX") || ! deliver_pr_body "$body" "$br"; then
|
|
4982
|
+
OPEN_ERR="its body file could not be written"
|
|
4983
|
+
elif deliver_create "$OPEN_BASE" "$br" "$body" --draft; then
|
|
4984
|
+
draft=true
|
|
4985
|
+
else
|
|
4986
|
+
OPEN_ERR="$GH_ERR"
|
|
4987
|
+
case "$GH_ERR" in
|
|
4988
|
+
*"$PR_CREATE_FORBIDDEN"*) OPEN_FORBIDDEN=1 ;;
|
|
4989
|
+
*)
|
|
4990
|
+
# Drafts depend on the account's plan (C3, not measured): one retry as ready.
|
|
4991
|
+
echo "remote-run.sh: $verb: opening a draft pull request failed ($GH_ERR); retrying once without --draft" >&2
|
|
4992
|
+
if deliver_create "$OPEN_BASE" "$br" "$body"; then
|
|
4993
|
+
draft=false
|
|
4994
|
+
else
|
|
4995
|
+
OPEN_ERR="$GH_ERR"
|
|
4996
|
+
fi ;;
|
|
4997
|
+
esac
|
|
4998
|
+
fi
|
|
4999
|
+
[ -z "$body" ] || rm -f "$body"
|
|
5000
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
5001
|
+
[ -n "$draft" ] || return 1
|
|
5002
|
+
FORGE_PR=$(printf '%s\n' "$GH_OUT" | sed -n 's|.*/pull/\([0-9][0-9]*\).*|\1|p' | tail -n 1)
|
|
5003
|
+
if [ -z "$FORGE_PR" ]; then
|
|
5004
|
+
OPEN_ERR="gh printed no pull request URL"
|
|
5005
|
+
return 1
|
|
5006
|
+
fi
|
|
5007
|
+
FORGE_PR_URL="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
5008
|
+
FORGE_PR_DRAFT="$draft"
|
|
5009
|
+
return 0
|
|
5010
|
+
}
|
|
5011
|
+
|
|
5012
|
+
# deliver_comment <number> <tmp> <text> — one `completed` comment on <number>:
|
|
5013
|
+
# <text>, the `phases.qa` sentence when that phase is on, this run's URL when
|
|
5014
|
+
# GITHUB_RUN_ID is set, then the marker. A failure is one line.
|
|
5015
|
+
deliver_comment() {
|
|
5016
|
+
local number="$1" tmp="$2" text="$3" file
|
|
5017
|
+
if hr_phase_enabled "$root" qa; then
|
|
5018
|
+
text="$text
|
|
5019
|
+
|
|
5020
|
+
The interactive-test phase was skipped on GitHub Actions. Before merging, run \`/autonomous-sdlc-harness:branch-qa-test $branch\` locally."
|
|
5021
|
+
fi
|
|
5022
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-deliver-comment.XXXXXX"); then
|
|
5023
|
+
echo "remote-run.sh: deliver: cannot create the comment file for #$number; no comment posted" >&2
|
|
5024
|
+
return 0
|
|
5025
|
+
fi
|
|
5026
|
+
{
|
|
5027
|
+
printf '%s\n' "$text"
|
|
5028
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
5029
|
+
} >"$file"
|
|
5030
|
+
forge_comment "$number" completed "$branch" "$file" || :
|
|
5031
|
+
rm -f "$file"
|
|
5032
|
+
}
|
|
5033
|
+
|
|
5034
|
+
# deliver_thread_reply <tmp> <comment_id> <text> — one reply to the review
|
|
5035
|
+
# thread holding <comment_id>, <text> then the `thread` marker. 0 when posted;
|
|
5036
|
+
# otherwise one `::warning::` line and 1.
|
|
5037
|
+
deliver_thread_reply() {
|
|
5038
|
+
local tmp="$1" id="$2" text="$3" file
|
|
5039
|
+
if [ -z "$tmp" ] || ! file=$(mktemp "$tmp/harness-thread-reply.XXXXXX"); then
|
|
5040
|
+
echo "::warning::remote-run.sh: deliver: cannot create the reply file for comment $id; no reply posted"
|
|
5041
|
+
return 1
|
|
5042
|
+
fi
|
|
5043
|
+
if ! { printf '%s\n\n' "$text"; forge_marker thread "$branch" "" "" "$RC_MARKED_N"; } >"$file"; then
|
|
5044
|
+
rm -f "$file"
|
|
5045
|
+
echo "::warning::remote-run.sh: deliver: cannot write the reply file for comment $id; no reply posted"
|
|
5046
|
+
return 1
|
|
5047
|
+
fi
|
|
5048
|
+
if ! gh_call api --method POST "repos/$FORGE_REPO/pulls/$FORGE_PR/comments/$id/replies" -F "body=@$file"; then
|
|
5049
|
+
rm -f "$file"
|
|
5050
|
+
echo "::warning::remote-run.sh: deliver: the reply to review comment $id was refused: $GH_ERR"
|
|
5051
|
+
return 1
|
|
5052
|
+
fi
|
|
5053
|
+
rm -f "$file"
|
|
5054
|
+
return 0
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
# deliver_round_threads <tmp> — at a round's completion, handle each inline
|
|
5058
|
+
# review thread the round collected by the fix plan's verdict on its comment:
|
|
5059
|
+
# `fixed` is a reply naming the fix commit, then the thread resolved; `reason`
|
|
5060
|
+
# is a reply quoting it, the thread left open. The wire it reads is the
|
|
5061
|
+
# header's THE REVIEW-COMMENT LINE. Never dismisses a review; never fails.
|
|
5062
|
+
deliver_round_threads() {
|
|
5063
|
+
local tmp="$1" plan index_rel dir_rel index names path name k line rest id ids
|
|
5064
|
+
local sha tip since pages threads row t_id t_resolved t_ids t_root t_later t_own
|
|
5065
|
+
local i found handled="," own_marker re
|
|
5066
|
+
local -a v_id=() v_kind=() v_val=()
|
|
5067
|
+
if ! round_markers_read ""; then
|
|
5068
|
+
echo "::warning::remote-run.sh: deliver: the round's review threads were not read: $RC_ERR"
|
|
5069
|
+
return 0
|
|
5070
|
+
fi
|
|
5071
|
+
if [ "$RC_NEWEST_N" -eq 0 ]; then
|
|
5072
|
+
echo "remote-run.sh: deliver: $branch has no round file; no review thread handled"
|
|
5073
|
+
return 0
|
|
5074
|
+
fi
|
|
5075
|
+
if [ "$RC_NEWEST_N" -ne "$RC_MARKED_N" ]; then
|
|
5076
|
+
echo "remote-run.sh: deliver: round $RC_NEWEST_N of $branch was not placed from a pull-request review; no review thread handled"
|
|
5077
|
+
return 0
|
|
5078
|
+
fi
|
|
5079
|
+
if [ -z "$RC_MARKED_C" ]; then
|
|
5080
|
+
echo "remote-run.sh: deliver: round $RC_MARKED_N of $branch collected no inline comment; no review thread handled"
|
|
5081
|
+
return 0
|
|
5082
|
+
fi
|
|
5083
|
+
|
|
5084
|
+
plan="${branch}_fix_plan"
|
|
5085
|
+
[ "$RC_MARKED_N" -eq 1 ] || plan="${plan}_$RC_MARKED_N"
|
|
5086
|
+
index_rel="$RC_STATE_REL/user_reviews/$plan.md"
|
|
5087
|
+
dir_rel="$RC_STATE_REL/user_reviews/$plan"
|
|
5088
|
+
if ! index=$(git -C "$root" show "refs/remotes/origin/$branch:$index_rel" 2>/dev/null); then
|
|
5089
|
+
echo "remote-run.sh: deliver: round $RC_MARKED_N of $branch has no fix plan at $index_rel; no review thread handled"
|
|
5090
|
+
return 0
|
|
5091
|
+
fi
|
|
5092
|
+
tip=$(git -C "$root" rev-parse "refs/remotes/origin/$branch" 2>/dev/null) || tip=""
|
|
5093
|
+
|
|
5094
|
+
# Verdicts, kept only for ids the round collected; the first verdict an id gets wins.
|
|
5095
|
+
names=$(git -C "$root" ls-tree --name-only "refs/remotes/origin/$branch" -- "$dir_rel/" 2>/dev/null) || names=""
|
|
5096
|
+
while IFS= read -r path; do
|
|
5097
|
+
name="${path##*/}"
|
|
5098
|
+
[[ "$name" =~ ^finding_([0-9]+)\.md$ ]] || continue
|
|
5099
|
+
k=$((10#${BASH_REMATCH[1]}))
|
|
5100
|
+
case "$index" in *"[x] **Finding $k**"*) ;; *) continue ;; esac
|
|
5101
|
+
line=$(git -C "$root" show "refs/remotes/origin/$branch:$path" 2>/dev/null | grep -E '^\*\*Review comments:\*\* ' | head -n 1) || line=""
|
|
5102
|
+
[ -n "$line" ] || continue
|
|
5103
|
+
sha=$(git -C "$root" log --reverse --format=%H -S "[x] **Finding $k**" "refs/remotes/origin/$branch" -- "$index_rel" 2>/dev/null | head -n 1) || sha=""
|
|
5104
|
+
[ -n "$sha" ] || sha="$tip"
|
|
5105
|
+
rest="${line#'**Review comments:** '}"
|
|
5106
|
+
rest="${rest%$'\r'}"
|
|
5107
|
+
ids="${rest//[[:space:]]/}"
|
|
5108
|
+
for id in ${ids//,/ }; do
|
|
5109
|
+
[[ "$id" =~ ^[0-9]+$ ]] || continue
|
|
5110
|
+
v_id+=("$id"); v_kind+=(fixed); v_val+=("$sha")
|
|
5111
|
+
done
|
|
5112
|
+
done <<NAMES
|
|
5113
|
+
$names
|
|
5114
|
+
NAMES
|
|
5115
|
+
re='^- (.*) \*\*Review comments:\*\* ([0-9][0-9, ]*)$'
|
|
5116
|
+
found=0
|
|
5117
|
+
while IFS= read -r line; do
|
|
5118
|
+
line="${line%$'\r'}"
|
|
5119
|
+
case "$line" in
|
|
5120
|
+
'## Out of scope / verified-OK'*) found=1; continue ;;
|
|
5121
|
+
'## '*) found=0; continue ;;
|
|
5122
|
+
esac
|
|
5123
|
+
[ "$found" -eq 1 ] || continue
|
|
5124
|
+
[[ "$line" =~ $re ]] || continue
|
|
5125
|
+
rest="${BASH_REMATCH[1]}"
|
|
5126
|
+
ids="${BASH_REMATCH[2]//[[:space:]]/}"
|
|
5127
|
+
for id in ${ids//,/ }; do
|
|
5128
|
+
[[ "$id" =~ ^[0-9]+$ ]] || continue
|
|
5129
|
+
v_id+=("$id"); v_kind+=(reason); v_val+=("$rest")
|
|
5130
|
+
done
|
|
5131
|
+
done <<INDEX
|
|
5132
|
+
$index
|
|
5133
|
+
INDEX
|
|
5134
|
+
|
|
5135
|
+
if ! since=$(jq -n -r --arg t "$RC_MARKED_AT" '$t | fromdateiso8601' 2>/dev/null) || [ -z "$since" ]; then
|
|
5136
|
+
echo "::warning::remote-run.sh: deliver: round $RC_MARKED_N's collected_at '$RC_MARKED_AT' is not a UTC time; no review thread handled"
|
|
5137
|
+
return 0
|
|
5138
|
+
fi
|
|
5139
|
+
if ! gh_call api graphql --paginate -f "owner=${FORGE_REPO%%/*}" -f "name=${FORGE_REPO#*/}" -F "number=$FORGE_PR" \
|
|
5140
|
+
-f 'query=query($owner: String!, $name: String!, $number: Int!, $endCursor: String) { repository(owner: $owner, name: $name) { pullRequest(number: $number) { reviewThreads(first: 100, after: $endCursor) { pageInfo { hasNextPage endCursor } nodes { id isResolved comments(first: 100) { nodes { databaseId createdAt body } } } } } } }'; then
|
|
5141
|
+
echo "::warning::remote-run.sh: deliver: listing the review threads of pull request #$FORGE_PR was refused: $GH_ERR"
|
|
5142
|
+
return 0
|
|
5143
|
+
fi
|
|
5144
|
+
pages="$GH_OUT"
|
|
5145
|
+
own_marker=$(forge_marker thread "$branch" "" "" "$RC_MARKED_N")
|
|
5146
|
+
# One `|`-joined row per thread (a GraphQL node id carries no `|`): id,
|
|
5147
|
+
# resolved, its comment ids, its first comment's id, whether a comment came
|
|
5148
|
+
# after the round, whether it carries this harness's reply.
|
|
5149
|
+
if ! threads=$(printf '%s' "$pages" | jq -s -r --argjson since "$since" --arg collected ",$RC_MARKED_C," \
|
|
5150
|
+
--arg marker "$COMMENT_MARKER" --arg own "$own_marker" '
|
|
5151
|
+
[ .[] | .data.repository.pullRequest.reviewThreads.nodes[] ]
|
|
5152
|
+
| .[] | (.comments.nodes // []) as $c
|
|
5153
|
+
| [ .id, (.isResolved == true | tostring),
|
|
5154
|
+
($c | map(.databaseId | tostring) | join(",")),
|
|
5155
|
+
(($c[0].databaseId // "") | tostring),
|
|
5156
|
+
(any($c[]; ("," + (.databaseId | tostring) + ",") as $k
|
|
5157
|
+
| ((.createdAt // "") | try fromdateiso8601 catch 0) > $since
|
|
5158
|
+
and (($collected | contains($k)) | not)
|
|
5159
|
+
and (((.body // "") | contains($marker)) | not)) | tostring),
|
|
5160
|
+
(any($c[]; (.body // "") | contains($own)) | tostring) ]
|
|
5161
|
+
| join("|")' 2>/dev/null); then
|
|
5162
|
+
echo "::warning::remote-run.sh: deliver: the review threads of pull request #$FORGE_PR are not the expected JSON; no review thread handled"
|
|
5163
|
+
return 0
|
|
5164
|
+
fi
|
|
5165
|
+
|
|
5166
|
+
for id in ${RC_MARKED_C//,/ }; do
|
|
5167
|
+
i=0; found=""
|
|
5168
|
+
while [ "$i" -lt "${#v_id[@]}" ]; do
|
|
5169
|
+
if [ "${v_id[$i]}" = "$id" ]; then found="$i"; break; fi
|
|
5170
|
+
i=$((i + 1))
|
|
5171
|
+
done
|
|
5172
|
+
if [ -z "$found" ]; then
|
|
5173
|
+
echo "remote-run.sh: deliver: review comment $id has no verdict in the fix plan; left alone"
|
|
5174
|
+
continue
|
|
5175
|
+
fi
|
|
5176
|
+
row=""
|
|
5177
|
+
while IFS= read -r line; do
|
|
5178
|
+
[ -n "$line" ] || continue
|
|
5179
|
+
IFS='|' read -r t_id t_resolved t_ids t_root t_later t_own <<<"$line"
|
|
5180
|
+
case ",$t_ids," in *",$id,"*) row="$line"; break ;; esac
|
|
5181
|
+
done <<THREADS
|
|
5182
|
+
$threads
|
|
5183
|
+
THREADS
|
|
5184
|
+
if [ -z "$row" ]; then
|
|
5185
|
+
echo "remote-run.sh: deliver: review comment $id is in no review thread of pull request #$FORGE_PR; left alone"
|
|
5186
|
+
continue
|
|
5187
|
+
fi
|
|
5188
|
+
case "$handled" in
|
|
5189
|
+
*",$t_id,"*) echo "remote-run.sh: deliver: review comment $id's thread was handled for another comment; left alone"; continue ;;
|
|
5190
|
+
esac
|
|
5191
|
+
handled="$handled$t_id,"
|
|
5192
|
+
if [ "$t_resolved" = true ]; then
|
|
5193
|
+
echo "remote-run.sh: deliver: review comment $id's thread is already resolved; left alone"
|
|
5194
|
+
continue
|
|
5195
|
+
fi
|
|
5196
|
+
if [ "$t_later" = true ]; then
|
|
5197
|
+
echo "remote-run.sh: deliver: review comment $id's thread has a reply newer than round $RC_MARKED_N; left alone"
|
|
5198
|
+
continue
|
|
5199
|
+
fi
|
|
5200
|
+
if [ "$t_own" = true ]; then
|
|
5201
|
+
echo "remote-run.sh: deliver: review comment $id's thread already carries this harness's reply; left alone"
|
|
5202
|
+
continue
|
|
5203
|
+
fi
|
|
5204
|
+
# Replies attach to a thread's first comment: GitHub does not take a reply to a reply.
|
|
5205
|
+
if [ "${v_kind[$found]}" = fixed ]; then
|
|
5206
|
+
deliver_thread_reply "$tmp" "$t_root" "Addressed in \`${v_val[$found]}\`." || continue
|
|
5207
|
+
if gh_call api graphql -f 'query=mutation($id: ID!) { resolveReviewThread(input: {threadId: $id}) { thread { isResolved } } }' -f "id=$t_id"; then
|
|
5208
|
+
echo "remote-run.sh: deliver: replied to review comment $id and resolved its thread"
|
|
5209
|
+
else
|
|
5210
|
+
echo "::warning::remote-run.sh: deliver: resolving review comment $id's thread was refused: $GH_ERR"
|
|
5211
|
+
fi
|
|
5212
|
+
else
|
|
5213
|
+
deliver_thread_reply "$tmp" "$t_root" "Not changed in this round: ${v_val[$found]}" || continue
|
|
5214
|
+
echo "remote-run.sh: deliver: replied to review comment $id with the reason; its thread stays open"
|
|
5215
|
+
fi
|
|
5216
|
+
done
|
|
5217
|
+
return 0
|
|
5218
|
+
}
|
|
5219
|
+
|
|
5220
|
+
# verb_deliver — after a `completed` bundle: reuse the branch's pull request or
|
|
5221
|
+
# open it, mark it ready, post `completed` on it and on the issue, and set
|
|
5222
|
+
# `done`. Always exit 0.
|
|
4379
5223
|
verb_deliver() {
|
|
4380
|
-
local status
|
|
4381
|
-
local tmp made_tmp=""
|
|
5224
|
+
local status engine noun verb_done pr_url="" lookup_failed=0 create_err="" forbidden=0 base=""
|
|
5225
|
+
local tmp made_tmp="" ready="" text readiness compare posted=""
|
|
4382
5226
|
if ! forge_on; then
|
|
4383
5227
|
echo "remote-run.sh: deliver: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
4384
5228
|
exit "$EXIT_OK"
|
|
@@ -4389,99 +5233,159 @@ verb_deliver() {
|
|
|
4389
5233
|
echo "remote-run.sh: deliver: the bundle's status is '${status:-unreadable}', not completed; nothing posted"
|
|
4390
5234
|
exit "$EXIT_OK"
|
|
4391
5235
|
fi
|
|
5236
|
+
engine=$(hr_remote_status_get "$bundle_dir/$HR_REMOTE_STATUS_FILE" engine 2>/dev/null) || engine=""
|
|
5237
|
+
if [ "$engine" = user_review ]; then
|
|
5238
|
+
noun=round; verb_done=finished
|
|
5239
|
+
else
|
|
5240
|
+
noun=run; verb_done=completed
|
|
5241
|
+
fi
|
|
4392
5242
|
forge_repo_var || exit "$EXIT_OK"
|
|
4393
5243
|
|
|
4394
5244
|
forge_fetch_branch "$branch"
|
|
4395
5245
|
forge_issue_var "$branch" || FORGE_ISSUE=""
|
|
4396
|
-
forge_pr_var "$branch" || { FORGE_PR=""; lookup_failed=1; create_err="whether a pull request is already open could not be read ($GH_ERR)"; }
|
|
4397
|
-
|
|
4398
|
-
tmp="${RUNNER_TEMP-}"
|
|
4399
|
-
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
4400
|
-
tmp=$(mktemp -d) || tmp=""
|
|
4401
|
-
made_tmp="$tmp"
|
|
4402
|
-
fi
|
|
5246
|
+
forge_pr_var "$branch" || { FORGE_PR=""; FORGE_PR_DRAFT=""; lookup_failed=1; create_err="whether a pull request is already open could not be read ($GH_ERR)"; }
|
|
4403
5247
|
|
|
4404
5248
|
if [ -n "$FORGE_PR" ]; then
|
|
4405
5249
|
pr_url="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
4406
5250
|
echo "remote-run.sh: deliver: $branch already has pull request #$FORGE_PR; none opened"
|
|
4407
5251
|
elif [ "$lookup_failed" -eq 0 ]; then
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
elif [ -z "$tmp" ] || ! body=$(mktemp "$tmp/harness-deliver-body.XXXXXX") || ! deliver_pr_body "$body" "$branch"; then
|
|
4413
|
-
create_err="its body file could not be written"
|
|
4414
|
-
elif deliver_create "$base" "$branch" "$body" --draft; then
|
|
4415
|
-
opened=1
|
|
5252
|
+
echo "remote-run.sh: deliver: $branch has no open pull request at completion — the start's attempt failed or the workflow predates the open step; opening it now"
|
|
5253
|
+
if forge_open_pr "$branch"; then
|
|
5254
|
+
pr_url="$FORGE_PR_URL"
|
|
5255
|
+
echo "remote-run.sh: deliver: opened pull request #$FORGE_PR for $branch"
|
|
4416
5256
|
else
|
|
4417
|
-
create_err="$
|
|
4418
|
-
|
|
4419
|
-
*"$PR_CREATE_FORBIDDEN"*) forbidden=1 ;;
|
|
4420
|
-
*)
|
|
4421
|
-
# Drafts depend on the account's plan (C3, not measured): one retry as ready.
|
|
4422
|
-
echo "remote-run.sh: deliver: opening a draft pull request failed ($GH_ERR); retrying once without --draft" >&2
|
|
4423
|
-
if deliver_create "$base" "$branch" "$body"; then
|
|
4424
|
-
opened=1
|
|
4425
|
-
else
|
|
4426
|
-
create_err="$GH_ERR"
|
|
4427
|
-
fi ;;
|
|
4428
|
-
esac
|
|
5257
|
+
create_err="$OPEN_ERR"
|
|
5258
|
+
forbidden="$OPEN_FORBIDDEN"
|
|
4429
5259
|
fi
|
|
4430
|
-
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
5260
|
+
base="$OPEN_BASE"
|
|
5261
|
+
fi
|
|
5262
|
+
|
|
5263
|
+
# The flip runs with the job's token, never HARNESS_PR_TOKEN.
|
|
5264
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5265
|
+
if [ "$FORGE_PR_DRAFT" = true ]; then
|
|
5266
|
+
if gh_call pr ready "$FORGE_PR" --repo "$FORGE_REPO"; then
|
|
5267
|
+
ready=flipped
|
|
5268
|
+
echo "remote-run.sh: deliver: marked pull request #$FORGE_PR ready for review"
|
|
4436
5269
|
else
|
|
4437
|
-
|
|
4438
|
-
|
|
5270
|
+
ready=refused
|
|
5271
|
+
echo "::warning::remote-run.sh: deliver: marking pull request #$FORGE_PR ready for review was refused: $GH_ERR"
|
|
4439
5272
|
fi
|
|
5273
|
+
else
|
|
5274
|
+
ready=not-draft
|
|
4440
5275
|
fi
|
|
4441
5276
|
fi
|
|
4442
5277
|
|
|
4443
|
-
|
|
4444
|
-
if [ -z "$
|
|
4445
|
-
|
|
5278
|
+
tmp="${RUNNER_TEMP-}"
|
|
5279
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5280
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5281
|
+
made_tmp="$tmp"
|
|
5282
|
+
fi
|
|
5283
|
+
|
|
5284
|
+
# After the flip, before the comments.
|
|
5285
|
+
if [ "$noun" = round ] && [ -n "$FORGE_PR" ]; then
|
|
5286
|
+
deliver_round_threads "$tmp"
|
|
5287
|
+
fi
|
|
5288
|
+
|
|
5289
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5290
|
+
case "$ready" in
|
|
5291
|
+
flipped)
|
|
5292
|
+
if [ "$noun" = round ]; then
|
|
5293
|
+
readiness="The harness round on \`$branch\` finished, and this pull request is marked ready for review again."
|
|
5294
|
+
else
|
|
5295
|
+
readiness="The harness run on \`$branch\` completed, and this pull request is now marked ready for your review."
|
|
5296
|
+
fi ;;
|
|
5297
|
+
not-draft)
|
|
5298
|
+
readiness="The harness $noun on \`$branch\` $verb_done. This pull request is not a draft — drafts may not be available on this repository's plan, or someone marked it ready — so it was left as it is." ;;
|
|
5299
|
+
*)
|
|
5300
|
+
readiness="The harness $noun on \`$branch\` $verb_done. Marking this draft ready for review was refused; mark it ready by hand." ;;
|
|
5301
|
+
esac
|
|
5302
|
+
deliver_comment "$FORGE_PR" "$tmp" "$readiness A review that requests changes starts another round."
|
|
5303
|
+
posted="#$FORGE_PR"
|
|
5304
|
+
if [ -n "$FORGE_ISSUE" ]; then
|
|
5305
|
+
if [ "$ready" = refused ]; then
|
|
5306
|
+
text="The harness $noun on \`$branch\` $verb_done. Its pull request #$FORGE_PR is still a draft — marking it ready for review was refused, so mark it ready by hand: $pr_url"
|
|
5307
|
+
elif [ "$noun" = round ]; then
|
|
5308
|
+
text="The harness round on \`$branch\` finished. Pull request #$FORGE_PR is ready for review again: $pr_url"
|
|
5309
|
+
else
|
|
5310
|
+
text="The harness run on \`$branch\` completed. Its pull request #$FORGE_PR is ready for your review: $pr_url"
|
|
5311
|
+
fi
|
|
5312
|
+
deliver_comment "$FORGE_ISSUE" "$tmp" "$text
|
|
5313
|
+
Review it there; a review that requests changes starts another round."
|
|
5314
|
+
posted="$posted and #$FORGE_ISSUE"
|
|
5315
|
+
fi
|
|
5316
|
+
elif [ -n "$FORGE_ISSUE" ]; then
|
|
5317
|
+
compare="$FORGE_SERVER/$FORGE_REPO/compare/${base:-<default branch>}...$branch?expand=1"
|
|
4446
5318
|
if [ "$forbidden" -eq 1 ]; then
|
|
4447
|
-
text="The harness
|
|
5319
|
+
text="The harness $noun on \`$branch\` $verb_done, but its pull request could not be opened: GitHub Actions is not permitted to create pull requests in this repository. Turn on *$PR_CREATE_SETTING* under $PR_CREATE_SETTING_PATH, or set the \`HARNESS_GIT_TOKEN\` secret, for the next run. For this one, open the pull request from the branch: $compare"
|
|
4448
5320
|
else
|
|
4449
|
-
text="The harness
|
|
5321
|
+
text="The harness $noun on \`$branch\` $verb_done, but its pull request could not be opened: $create_err. Open it by hand from the branch: $compare"
|
|
4450
5322
|
fi
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
text="The harness round on \`$branch\` finished. Review this pull request; a review that requests changes starts another round."
|
|
4454
|
-
elif [ -n "$FORGE_ISSUE" ]; then
|
|
4455
|
-
target="$FORGE_ISSUE"
|
|
4456
|
-
text="The harness run on \`$branch\` completed. Its pull request is ready for your review: $pr_url
|
|
4457
|
-
Review it there; a review that requests changes starts another round."
|
|
5323
|
+
deliver_comment "$FORGE_ISSUE" "$tmp" "$text"
|
|
5324
|
+
posted="#$FORGE_ISSUE"
|
|
4458
5325
|
else
|
|
4459
|
-
|
|
4460
|
-
text="The harness run on \`$branch\` completed and opened this pull request. Review it; a review that requests changes starts another round."
|
|
5326
|
+
echo "remote-run.sh: deliver: $branch has no pull request and no issue it was started from; nothing posted"
|
|
4461
5327
|
fi
|
|
4462
|
-
|
|
4463
|
-
text="$text
|
|
5328
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4464
5329
|
|
|
4465
|
-
|
|
4466
|
-
|
|
5330
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" done || :
|
|
5331
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" done || :
|
|
5332
|
+
echo "remote-run.sh: deliver: completed on $branch reported${posted:+ on $posted}"
|
|
5333
|
+
exit "$EXIT_OK"
|
|
5334
|
+
}
|
|
4467
5335
|
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
5336
|
+
# verb_open — at the run's start: open the branch's draft pull request when none
|
|
5337
|
+
# is open, set it `running`, and name it once on the source issue. Always exit 0.
|
|
5338
|
+
verb_open() {
|
|
5339
|
+
local tmp made_tmp="" file
|
|
5340
|
+
if ! forge_on; then
|
|
5341
|
+
echo "remote-run.sh: open: the forge coupling is off (forge github and execution.target github-actions); nothing opened"
|
|
5342
|
+
exit "$EXIT_OK"
|
|
5343
|
+
fi
|
|
5344
|
+
forge_repo_var || exit "$EXIT_OK"
|
|
5345
|
+
forge_fetch_branch "$branch"
|
|
5346
|
+
forge_issue_var "$branch" || FORGE_ISSUE=""
|
|
5347
|
+
if ! forge_pr_var "$branch"; then
|
|
5348
|
+
echo "::warning::remote-run.sh: open: whether $branch already has a pull request could not be read ($GH_ERR); none opened"
|
|
5349
|
+
exit "$EXIT_OK"
|
|
5350
|
+
fi
|
|
5351
|
+
if [ -n "$FORGE_PR" ]; then
|
|
5352
|
+
echo "remote-run.sh: open: $branch already has pull request #$FORGE_PR; none opened"
|
|
5353
|
+
exit "$EXIT_OK"
|
|
5354
|
+
fi
|
|
5355
|
+
if ! forge_open_pr "$branch"; then
|
|
5356
|
+
echo "::warning::remote-run.sh: open: the pull request of $branch could not be opened: $OPEN_ERR. The run goes on and its comments reach its issue; deliver tries again when the run completes"
|
|
5357
|
+
exit "$EXIT_OK"
|
|
5358
|
+
fi
|
|
5359
|
+
echo "remote-run.sh: open: opened pull request #$FORGE_PR for $branch"
|
|
5360
|
+
forge_set_state "$FORGE_PR" running || :
|
|
5361
|
+
if [ -z "$FORGE_ISSUE" ]; then
|
|
5362
|
+
echo "remote-run.sh: open: $branch was started from no issue; nothing posted"
|
|
5363
|
+
exit "$EXIT_OK"
|
|
5364
|
+
fi
|
|
5365
|
+
tmp="${RUNNER_TEMP-}"
|
|
5366
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
5367
|
+
tmp=$(mktemp -d) || tmp=""
|
|
5368
|
+
made_tmp="$tmp"
|
|
5369
|
+
fi
|
|
5370
|
+
if [ -n "$tmp" ] && file=$(mktemp "$tmp/harness-open-comment.XXXXXX"); then
|
|
4471
5371
|
{
|
|
4472
|
-
|
|
5372
|
+
if [ "$FORGE_PR_DRAFT" = false ]; then
|
|
5373
|
+
printf 'The harness run on `%s` opened its pull request #%s, not as a draft — this repository'"'"'s plan may not offer drafts: %s\n' \
|
|
5374
|
+
"$branch" "$FORGE_PR" "$FORGE_PR_URL"
|
|
5375
|
+
else
|
|
5376
|
+
printf 'The harness run on `%s` opened its draft pull request #%s: %s\n' "$branch" "$FORGE_PR" "$FORGE_PR_URL"
|
|
5377
|
+
fi
|
|
5378
|
+
printf '\nThe run'"'"'s questions, lifecycle comments and progress are posted there from now on; `%s` commands keep working on this issue.\n' \
|
|
5379
|
+
"$COMMAND_HANDLE"
|
|
4473
5380
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4474
5381
|
} >"$file"
|
|
4475
|
-
forge_comment "$
|
|
5382
|
+
forge_comment "$FORGE_ISSUE" opened "$branch" "$file" || :
|
|
4476
5383
|
rm -f "$file"
|
|
4477
5384
|
else
|
|
4478
|
-
echo "remote-run.sh:
|
|
5385
|
+
echo "remote-run.sh: open: cannot create the comment file for #$FORGE_ISSUE; no comment posted" >&2
|
|
4479
5386
|
fi
|
|
4480
5387
|
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4481
|
-
|
|
4482
|
-
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" done || :
|
|
4483
|
-
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" done || :
|
|
4484
|
-
echo "remote-run.sh: deliver: completed on $branch reported${target:+ on #$target}"
|
|
5388
|
+
echo "remote-run.sh: open: pull request #$FORGE_PR named on #$FORGE_ISSUE"
|
|
4485
5389
|
exit "$EXIT_OK"
|
|
4486
5390
|
}
|
|
4487
5391
|
|
|
@@ -4508,8 +5412,13 @@ control_cleanup() {
|
|
|
4508
5412
|
return 0
|
|
4509
5413
|
}
|
|
4510
5414
|
|
|
4511
|
-
#
|
|
4512
|
-
#
|
|
5415
|
+
# The engine a dispatch child named, set only once that child exited 0; a
|
|
5416
|
+
# non-empty value adds ` engine=<engine>` to every later reply's marker.
|
|
5417
|
+
CONTROL_REPLY_ENGINE=""
|
|
5418
|
+
|
|
5419
|
+
# control_post <text> — post <text> on CONTROL_NUMBER as a `reply` comment,
|
|
5420
|
+
# its marker carrying CONTROL_REPLY_ENGINE; 1, after an `::error::` line, when
|
|
5421
|
+
# it cannot be posted.
|
|
4513
5422
|
control_post() {
|
|
4514
5423
|
local text="$1" file status=0
|
|
4515
5424
|
if ! forge_repo_var; then
|
|
@@ -4524,7 +5433,7 @@ control_post() {
|
|
|
4524
5433
|
printf '%s\n' "$text"
|
|
4525
5434
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4526
5435
|
} >"$file"
|
|
4527
|
-
if ! forge_comment "$CONTROL_NUMBER" reply "$CONTROL_BRANCH" "$file"; then
|
|
5436
|
+
if ! forge_comment "$CONTROL_NUMBER" reply "$CONTROL_BRANCH" "$file" "" "$CONTROL_REPLY_ENGINE"; then
|
|
4528
5437
|
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
4529
5438
|
status=1
|
|
4530
5439
|
fi
|
|
@@ -4661,11 +5570,47 @@ control_stopped_refuse() {
|
|
|
4661
5570
|
esac
|
|
4662
5571
|
}
|
|
4663
5572
|
|
|
4664
|
-
#
|
|
4665
|
-
#
|
|
4666
|
-
#
|
|
5573
|
+
# control_run_in_flight <branch> — 0 when a run titled exactly
|
|
5574
|
+
# `harness run <branch>` is queued, in_progress, waiting, requested or
|
|
5575
|
+
# pending; 1 when none is; 2 with GH_ERR set when the listing failed or was
|
|
5576
|
+
# not a JSON array. Passes <branch> explicitly rather than reading the global
|
|
5577
|
+
# `branch` list_runs reads, and leaves GH_OUT as it found it.
|
|
5578
|
+
control_run_in_flight() {
|
|
5579
|
+
local saved="$GH_OUT" verdict status=0
|
|
5580
|
+
if ! gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$1" \
|
|
5581
|
+
--json displayTitle,status --limit "$RUN_LIST_LIMIT"; then
|
|
5582
|
+
GH_OUT="$saved"
|
|
5583
|
+
return 2
|
|
5584
|
+
fi
|
|
5585
|
+
verdict=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $1" '
|
|
5586
|
+
if type != "array" then error("not an array") else
|
|
5587
|
+
if any(.[]; .displayTitle == $t
|
|
5588
|
+
and (.status == "queued" or .status == "in_progress" or .status == "waiting"
|
|
5589
|
+
or .status == "requested" or .status == "pending"))
|
|
5590
|
+
then "yes" else "no" end
|
|
5591
|
+
end' 2>/dev/null) || verdict=""
|
|
5592
|
+
GH_OUT="$saved"
|
|
5593
|
+
case "$verdict" in
|
|
5594
|
+
yes) status=0 ;;
|
|
5595
|
+
no) status=1 ;;
|
|
5596
|
+
*) GH_ERR="its run list is not the expected JSON"; status=2 ;;
|
|
5597
|
+
esac
|
|
5598
|
+
return "$status"
|
|
5599
|
+
}
|
|
5600
|
+
|
|
5601
|
+
# control_check_branch <branch> [started] — the refusals every path shares, in
|
|
5602
|
+
# order: a branch not answered 1 by hr_branch_is_protected, then (after a
|
|
5603
|
+
# fetch) one that is not a harness branch. A branch passes with its task
|
|
5604
|
+
# prompt or the flow-progress ledger on its origin tip (`forge_recognised`);
|
|
5605
|
+
# or, with `started` (passed only by
|
|
5606
|
+
# control_branch_from_issue, whose issue's genuine `started` marker names it),
|
|
5607
|
+
# while it still exists on origin — gone is refused as deleted, a failed
|
|
5608
|
+
# existence check names the read, and no run list is read; or with a
|
|
5609
|
+
# `harness run <branch>` run in flight. Otherwise the refusal reads "neither a
|
|
5610
|
+
# task prompt nor a ledger, and no `harness run` in flight", and a failed listing names the read. Each
|
|
5611
|
+
# refusal is a reply and exit 2, or 3 for a failed read.
|
|
4667
5612
|
control_check_branch() {
|
|
4668
|
-
local b="$1" protected=0
|
|
5613
|
+
local b="$1" protected=0 exists=0 inflight=0
|
|
4669
5614
|
if ! valid_branch "$b" || ! git check-ref-format --branch "$b" >/dev/null 2>&1; then
|
|
4670
5615
|
control_refuse "$EXIT_REFUSED" "\`$b\` is not a valid branch name" "Comment on the pull request of the run's branch instead."
|
|
4671
5616
|
fi
|
|
@@ -4679,8 +5624,25 @@ control_check_branch() {
|
|
|
4679
5624
|
esac
|
|
4680
5625
|
forge_fetch_branch "$b"
|
|
4681
5626
|
if ! forge_recognised "$b"; then
|
|
4682
|
-
|
|
4683
|
-
"
|
|
5627
|
+
if [ "${2-}" = started ]; then
|
|
5628
|
+
remote_branch_exists "$b" || exists=$?
|
|
5629
|
+
case "$exists" in
|
|
5630
|
+
0) ;;
|
|
5631
|
+
1) control_refuse "$EXIT_REFUSED" "\`$b\` no longer exists on origin, so its run cannot be resumed or commanded" \
|
|
5632
|
+
"Start a new run from a new issue or the Run workflow form." ;;
|
|
5633
|
+
*) control_refuse "$EXIT_GH" "whether \`$b\` still exists on origin could not be read ($REMOTE_BRANCH_ERR)" \
|
|
5634
|
+
"Comment again to retry." ;;
|
|
5635
|
+
esac
|
|
5636
|
+
else
|
|
5637
|
+
control_run_in_flight "$b" || inflight=$?
|
|
5638
|
+
case "$inflight" in
|
|
5639
|
+
0) ;;
|
|
5640
|
+
1) control_refuse "$EXIT_REFUSED" "\`$b\` is not a harness branch: its tip carries neither a task prompt nor a flow-progress ledger, and no \`harness run $b\` run is queued or in progress" \
|
|
5641
|
+
"Only a branch a harness run works on can be commanded." ;;
|
|
5642
|
+
*) control_refuse "$EXIT_GH" "whether \`$b\` has a harness run in flight could not be read ($GH_ERR)" \
|
|
5643
|
+
"Comment again to retry." ;;
|
|
5644
|
+
esac
|
|
5645
|
+
fi
|
|
4684
5646
|
fi
|
|
4685
5647
|
CONTROL_BRANCH="$b"
|
|
4686
5648
|
}
|
|
@@ -4756,7 +5718,8 @@ control_issue_branch_var() {
|
|
|
4756
5718
|
}
|
|
4757
5719
|
|
|
4758
5720
|
# control_branch_from_issue <number> — control_issue_branch_var, its failures
|
|
4759
|
-
# refused, then control_check_branch
|
|
5721
|
+
# refused, then control_check_branch with `started`: the verified marker makes
|
|
5722
|
+
# the branch a harness branch while it still exists on origin.
|
|
4760
5723
|
control_branch_from_issue() {
|
|
4761
5724
|
if ! control_issue_branch_var "$1"; then
|
|
4762
5725
|
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
@@ -4765,7 +5728,7 @@ control_branch_from_issue() {
|
|
|
4765
5728
|
control_refuse "$EXIT_REFUSED" "no harness run was started from this issue" \
|
|
4766
5729
|
"Comment on the pull request of the run's branch instead."
|
|
4767
5730
|
fi
|
|
4768
|
-
control_check_branch "$ISSUE_BRANCH"
|
|
5731
|
+
control_check_branch "$ISSUE_BRANCH" started
|
|
4769
5732
|
}
|
|
4770
5733
|
|
|
4771
5734
|
# control_verb_handled <verb> — 0 when an arm below carries out <verb>.
|
|
@@ -4800,6 +5763,7 @@ control_resume_dispatch() {
|
|
|
4800
5763
|
*) control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE $CONTROL_VERB\` again to retry." ;;
|
|
4801
5764
|
esac
|
|
4802
5765
|
status="$EXIT_OK"
|
|
5766
|
+
CONTROL_REPLY_ENGINE="$CS_ENGINE"
|
|
4803
5767
|
control_post "$done_text" || status="$EXIT_GH"
|
|
4804
5768
|
# The job posts its own `resumed` comment; the labels say `running` now.
|
|
4805
5769
|
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
@@ -5102,6 +6066,7 @@ control_answer() {
|
|
|
5102
6066
|
control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" \
|
|
5103
6067
|
"Comment \`$COMMAND_HANDLE answer $n\` again to retry." ;;
|
|
5104
6068
|
esac
|
|
6069
|
+
CONTROL_REPLY_ENGINE="$CS_ENGINE"
|
|
5105
6070
|
|
|
5106
6071
|
cmds=""
|
|
5107
6072
|
for v in $CS_OPEN; do
|
|
@@ -5177,18 +6142,27 @@ RC_ERR=""
|
|
|
5177
6142
|
# origin's tip consumed, from their marker lines: RC_SEEN_R and RC_SEEN_C (the
|
|
5178
6143
|
# recorded review and comment ids, comma-wrapped), RC_MARKED_AT (the
|
|
5179
6144
|
# highest-numbered marked round's `collected_at`, empty when none is marked),
|
|
5180
|
-
#
|
|
5181
|
-
#
|
|
5182
|
-
#
|
|
6145
|
+
# RC_MARKED_N and RC_MARKED_C (that round's number, 0 when none, and its
|
|
6146
|
+
# `comments=` ids as written), RC_NEWEST_N (the highest round number of any
|
|
6147
|
+
# round file, marked or not, 0 when none), RC_EVENT_ROUND (the round recording
|
|
6148
|
+
# <event_review_id>), and RC_STATE_REL. 4 with RC_ERR when a round cannot be
|
|
6149
|
+
# listed or read. `control` also calls it on its own, to name the round a
|
|
6150
|
+
# review in flight is already part of, and `deliver` to find a round's threads.
|
|
5183
6151
|
RC_SEEN_R=","
|
|
5184
6152
|
RC_SEEN_C=","
|
|
5185
6153
|
RC_MARKED_AT=""
|
|
6154
|
+
RC_MARKED_N=0
|
|
6155
|
+
RC_MARKED_C=""
|
|
6156
|
+
RC_NEWEST_N=0
|
|
5186
6157
|
RC_STATE_REL=""
|
|
5187
6158
|
round_markers_read() {
|
|
5188
|
-
local event_id="${1-}" names name path n line
|
|
6159
|
+
local event_id="${1-}" names name path n line
|
|
5189
6160
|
RC_SEEN_R=","
|
|
5190
6161
|
RC_SEEN_C=","
|
|
5191
6162
|
RC_MARKED_AT=""
|
|
6163
|
+
RC_MARKED_N=0
|
|
6164
|
+
RC_MARKED_C=""
|
|
6165
|
+
RC_NEWEST_N=0
|
|
5192
6166
|
RC_EVENT_ROUND=""
|
|
5193
6167
|
RC_STATE_REL=$(hr_state_dir "$root" 2>/dev/null) || RC_STATE_REL=""
|
|
5194
6168
|
RC_STATE_REL="${RC_STATE_REL%/}"
|
|
@@ -5206,6 +6180,7 @@ round_markers_read() {
|
|
|
5206
6180
|
[ "${BASH_REMATCH[1]}" = "$branch" ] || continue
|
|
5207
6181
|
n="${BASH_REMATCH[3]:-1}"
|
|
5208
6182
|
n=$((10#$n))
|
|
6183
|
+
[ "$n" -le "$RC_NEWEST_N" ] || RC_NEWEST_N="$n"
|
|
5209
6184
|
if ! line=$(git -C "$root" show "refs/remotes/origin/$branch:$path" 2>/dev/null); then
|
|
5210
6185
|
RC_ERR="the previous round \`$path\` could not be read"
|
|
5211
6186
|
return 4
|
|
@@ -5215,9 +6190,10 @@ round_markers_read() {
|
|
|
5215
6190
|
[[ "$line" =~ ^"$COMMENT_MARKER round collected_at="([0-9T:Z-]+)" reviews="([0-9,]*)" comments="([0-9,]*)" -->"$ ]] || continue
|
|
5216
6191
|
RC_SEEN_R="$RC_SEEN_R${BASH_REMATCH[2]}${BASH_REMATCH[2]:+,}"
|
|
5217
6192
|
RC_SEEN_C="$RC_SEEN_C${BASH_REMATCH[3]}${BASH_REMATCH[3]:+,}"
|
|
5218
|
-
if [ "$n" -gt "$
|
|
5219
|
-
|
|
6193
|
+
if [ "$n" -gt "$RC_MARKED_N" ]; then
|
|
6194
|
+
RC_MARKED_N="$n"
|
|
5220
6195
|
RC_MARKED_AT="${BASH_REMATCH[1]}"
|
|
6196
|
+
RC_MARKED_C="${BASH_REMATCH[3]}"
|
|
5221
6197
|
fi
|
|
5222
6198
|
case ",${BASH_REMATCH[2]}," in
|
|
5223
6199
|
*",$event_id,"*) [ -z "$event_id" ] || RC_EVENT_ROUND="$n" ;;
|
|
@@ -5391,12 +6367,16 @@ REVIEW_RETRY_WAY="The reviews stay on the pull request and are collected by the
|
|
|
5391
6367
|
# control_settled_var — `branch_settled_var` for CONTROL_BRANCH, with no run
|
|
5392
6368
|
# counted as settled, run in a command substitution so that its exit on a
|
|
5393
6369
|
# failed read reaches this process as a status, not as an exit with no reply:
|
|
5394
|
-
# BS_SETTLED, BS_STATE and BS_REASON
|
|
5395
|
-
#
|
|
6370
|
+
# BS_SETTLED, BS_STATE and BS_REASON, and RS_NOT_STARTED, RS_RUN_ID, RS_ENGINE
|
|
6371
|
+
# and RS_DETAIL as BS_NOT_STARTED, BS_RUN_ID, BS_ENGINE and BS_DETAIL; 1 with
|
|
6372
|
+
# BS_ERR, its last stderr line, on a failed read. BS_DETAIL is read last, so a
|
|
6373
|
+
# `|` inside it shifts no other field.
|
|
5396
6374
|
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
6375
|
+
BS_NOT_STARTED=0; BS_RUN_ID=""; BS_ENGINE=""; BS_DETAIL=""
|
|
5397
6376
|
control_settled_var() {
|
|
5398
6377
|
local dir errfile line status=0
|
|
5399
6378
|
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
6379
|
+
BS_NOT_STARTED=0; BS_RUN_ID=""; BS_ENGINE=""; BS_DETAIL=""
|
|
5400
6380
|
if ! dir=$(mktemp -d "$control_tmp/harness-control-settled.XXXXXX"); then
|
|
5401
6381
|
BS_ERR="a state directory could not be created under '$control_tmp'"
|
|
5402
6382
|
return 1
|
|
@@ -5405,14 +6385,15 @@ control_settled_var() {
|
|
|
5405
6385
|
control_dirs="$control_dirs $dir $errfile"
|
|
5406
6386
|
branch="$CONTROL_BRANCH"
|
|
5407
6387
|
line=$(branch_settled_var "$dir" 1 >/dev/null 2>"$errfile" \
|
|
5408
|
-
&& printf '%s|%s|%s\n' "$SETTLED" "$RS_STATE" "$RS_PAUSE_REASON"
|
|
6388
|
+
&& printf '%s|%s|%s|%s|%s|%s|%s\n' "$SETTLED" "$RS_STATE" "$RS_PAUSE_REASON" \
|
|
6389
|
+
"$RS_NOT_STARTED" "$RS_RUN_ID" "$RS_ENGINE" "$RS_DETAIL") || status=$?
|
|
5409
6390
|
cat "$errfile" >&2 2>/dev/null || :
|
|
5410
6391
|
if [ "$status" -ne 0 ] || [ -z "$line" ]; then
|
|
5411
6392
|
BS_ERR=$(grep -v '^[[:space:]]*$' "$errfile" 2>/dev/null | tail -n 1)
|
|
5412
6393
|
[ -n "$BS_ERR" ] || BS_ERR="exit $status, no message"
|
|
5413
6394
|
return 1
|
|
5414
6395
|
fi
|
|
5415
|
-
IFS='|' read -r BS_SETTLED BS_STATE BS_REASON <<<"$line"
|
|
6396
|
+
IFS='|' read -r BS_SETTLED BS_STATE BS_REASON BS_NOT_STARTED BS_RUN_ID BS_ENGINE BS_DETAIL <<<"$line"
|
|
5416
6397
|
return 0
|
|
5417
6398
|
}
|
|
5418
6399
|
|
|
@@ -5758,7 +6739,7 @@ control_close() {
|
|
|
5758
6739
|
fi
|
|
5759
6740
|
|
|
5760
6741
|
# No forge_recognised check: a merged or deleted branch may no longer carry
|
|
5761
|
-
# its ledger, and a listed `harness run <b>` run is what proves a harness run.
|
|
6742
|
+
# its task prompt or ledger, and a listed `harness run <b>` run is what proves a harness run.
|
|
5762
6743
|
if ! control_state_var "$b"; then
|
|
5763
6744
|
echo "::error::remote-run.sh: control: the state of the run on \`$b\` could not be read ($CS_ERR)"
|
|
5764
6745
|
exit "$EXIT_GH"
|
|
@@ -5910,7 +6891,7 @@ collect_notify() {
|
|
|
5910
6891
|
}
|
|
5911
6892
|
|
|
5912
6893
|
verb_collect() {
|
|
5913
|
-
local status=0 dir file out
|
|
6894
|
+
local status=0 dir file out reason route way
|
|
5914
6895
|
if ! forge_on; then
|
|
5915
6896
|
echo "remote-run.sh: collect: the forge coupling is off (forge github and execution.target github-actions); nothing collected"
|
|
5916
6897
|
exit "$EXIT_OK"
|
|
@@ -5935,17 +6916,7 @@ verb_collect() {
|
|
|
5935
6916
|
exit "$EXIT_OK" ;;
|
|
5936
6917
|
esac
|
|
5937
6918
|
|
|
5938
|
-
|
|
5939
|
-
FORGE_PR="$pr_arg"
|
|
5940
|
-
elif ! forge_pr_var "$branch"; then
|
|
5941
|
-
echo "remote-run.sh: collect: the pull request of $branch could not be read; nothing collected"
|
|
5942
|
-
exit "$EXIT_OK"
|
|
5943
|
-
fi
|
|
5944
|
-
if [ -z "$FORGE_PR" ]; then
|
|
5945
|
-
echo "remote-run.sh: collect: no open pull request; nothing to collect"
|
|
5946
|
-
exit "$EXIT_OK"
|
|
5947
|
-
fi
|
|
5948
|
-
|
|
6919
|
+
# Read before the pull request is required: a first run has none.
|
|
5949
6920
|
control_tmp="${RUNNER_TEMP-}"
|
|
5950
6921
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5951
6922
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: collect: mktemp failed; nothing collected"; exit "$EXIT_OK"; }
|
|
@@ -5958,6 +6929,37 @@ verb_collect() {
|
|
|
5958
6929
|
echo "remote-run.sh: collect: the state of the run on $branch could not be read ($BS_ERR); no round started"
|
|
5959
6930
|
exit "$EXIT_OK"
|
|
5960
6931
|
fi
|
|
6932
|
+
if [ "$BS_NOT_STARTED" = 1 ]; then
|
|
6933
|
+
if [ -n "${GITHUB_RUN_ID-}" ] && [ "$BS_RUN_ID" != "$GITHUB_RUN_ID" ]; then
|
|
6934
|
+
echo "remote-run.sh: collect: run $BS_RUN_ID of $branch never started; its own collect reports it"
|
|
6935
|
+
exit "$EXIT_OK"
|
|
6936
|
+
fi
|
|
6937
|
+
reason="${BS_DETAIL#"GitHub did not start the job of run $BS_RUN_ID ("}"
|
|
6938
|
+
if [ "$reason" = "$BS_DETAIL" ]; then reason=""; else reason="${reason%"): "*}"; fi
|
|
6939
|
+
REPORT_NOT_STARTED_STATE="$BS_STATE"
|
|
6940
|
+
REPORT_NOT_STARTED_ENGINE="$BS_ENGINE"
|
|
6941
|
+
route=$(hr_github_resume_route "$branch" "${BS_ENGINE:-<task, user_review or docs: the one the run was started with>}")
|
|
6942
|
+
if [ "$BS_STATE" = paused ]; then
|
|
6943
|
+
way="Run $RESUME_HINT $branch to start it again; $route."
|
|
6944
|
+
else
|
|
6945
|
+
way="Start it again ${route#or }."
|
|
6946
|
+
fi
|
|
6947
|
+
notify not_started "$branch" "$BS_DETAIL. $way" "$reason"
|
|
6948
|
+
# The reviews stay for the resumed run's own end.
|
|
6949
|
+
echo "remote-run.sh: collect: run ${BS_RUN_ID} of $branch never started; reported, and no round collected"
|
|
6950
|
+
exit "$EXIT_OK"
|
|
6951
|
+
fi
|
|
6952
|
+
|
|
6953
|
+
if [ -n "$pr_arg" ]; then
|
|
6954
|
+
FORGE_PR="$pr_arg"
|
|
6955
|
+
elif ! forge_pr_var "$branch"; then
|
|
6956
|
+
echo "remote-run.sh: collect: the pull request of $branch could not be read; nothing collected"
|
|
6957
|
+
exit "$EXIT_OK"
|
|
6958
|
+
fi
|
|
6959
|
+
if [ -z "$FORGE_PR" ]; then
|
|
6960
|
+
echo "remote-run.sh: collect: no open pull request; nothing to collect"
|
|
6961
|
+
exit "$EXIT_OK"
|
|
6962
|
+
fi
|
|
5961
6963
|
if [ "$BS_SETTLED" != 1 ]; then
|
|
5962
6964
|
echo "remote-run.sh: collect: $branch is ${BS_STATE:-in flight}${BS_REASON:+ ($BS_REASON)}; that run's own end collects"
|
|
5963
6965
|
exit "$EXIT_OK"
|
|
@@ -5994,7 +6996,11 @@ verb_collect() {
|
|
|
5994
6996
|
echo "remote-run.sh: collect: started the next round of $branch from @${RC_REVIEWERS//,/, @}"
|
|
5995
6997
|
exit "$EXIT_OK"
|
|
5996
6998
|
fi
|
|
5997
|
-
|
|
6999
|
+
if [ -n "${GITHUB_RUN_ID-}" ]; then
|
|
7000
|
+
collect_notify "The reviews requesting changes collected during the run on \`$branch\` could not start the next round: ${CHILD_LAST%.}. They stay on the pull request. To retry, re-run this run's \`collect\` job (no new review is needed), or submit a review requesting changes."
|
|
7001
|
+
else
|
|
7002
|
+
collect_notify "The reviews requesting changes collected during the run on \`$branch\` could not start the next round: ${CHILD_LAST%.}. They stay on the pull request; submit a review requesting changes to retry."
|
|
7003
|
+
fi
|
|
5998
7004
|
echo "remote-run.sh: collect: review exited $CHILD_STATUS; the pull request was told, and nothing is retried"
|
|
5999
7005
|
exit "$EXIT_OK"
|
|
6000
7006
|
}
|
|
@@ -6078,6 +7084,7 @@ case "$verb" in
|
|
|
6078
7084
|
list) verb_list ;;
|
|
6079
7085
|
discard) verb_discard ;;
|
|
6080
7086
|
report) verb_report ;;
|
|
7087
|
+
open) verb_open ;;
|
|
6081
7088
|
deliver) verb_deliver ;;
|
|
6082
7089
|
collect) verb_collect ;;
|
|
6083
7090
|
control) verb_control ;;
|