autonomous-sdlc-harness 0.6.0 → 0.6.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +0 -0
- package/dist/remote/githubActions.js +5 -2
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/github/workflows/harness-control.yml +43 -13
- package/templates/github/workflows/harness-run.yml +28 -3
- package/templates/scripts/autonomous-watcher.sh +133 -28
- package/templates/scripts/lib/harness-run-lib.sh +59 -27
- package/templates/scripts/remote-run.sh +697 -102
- package/templates/state-dir/clarifications/README.md +1 -1
|
@@ -12,7 +12,8 @@
|
|
|
12
12
|
# [--park-loop-clear] [--chain <n>] [--repo <root>]
|
|
13
13
|
# remote-run.sh pause <branch> [--repo <root>]
|
|
14
14
|
# remote-run.sh warm [--repo <root>]
|
|
15
|
-
# remote-run.sh stop <branch> [--actor <login>] [--
|
|
15
|
+
# remote-run.sh stop <branch> [--actor <login>] [--note <text>] [--pr <n>]
|
|
16
|
+
# [--branch-gone] [--repo <root>]
|
|
16
17
|
# remote-run.sh status <branch> [--repo <root>]
|
|
17
18
|
# remote-run.sh sync <branch> [--repo <root>]
|
|
18
19
|
# remote-run.sh fetch <branch> <out_dir> [--repo <root>]
|
|
@@ -273,7 +274,8 @@
|
|
|
273
274
|
# whose head is <branch> when origin's <branch> carries
|
|
274
275
|
# `<state_dir>/flow_progress/<branch>_progress.md`, else to the issue named by
|
|
275
276
|
# the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
276
|
-
# task prompt, else nowhere
|
|
277
|
+
# task prompt, else nowhere (`stop`'s `--pr` and `--branch-gone` change this
|
|
278
|
+
# for its own report: its paragraph). The label `STATE_LABEL_PREFIX<state>` replaces any
|
|
277
279
|
# other state label on that issue and that pull request, each when known; the
|
|
278
280
|
# label is a view, and the run list stays the authority. The state map:
|
|
279
281
|
# `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
|
|
@@ -287,12 +289,16 @@
|
|
|
287
289
|
# `<!-- sdlc-harness event=<event> branch=<branch> -->`. The pause reason and
|
|
288
290
|
# reset come from the registry record, read only when the registry file exists.
|
|
289
291
|
# `parked` instead posts one comment per open question — `open_questions_in`
|
|
290
|
-
# over `$root`'s state directory, ascending — carrying `question_<n>.md`
|
|
291
|
-
#
|
|
292
|
-
#
|
|
293
|
-
#
|
|
294
|
-
# one
|
|
295
|
-
#
|
|
292
|
+
# over `$root`'s state directory, ascending — carrying `question_<n>.md` less
|
|
293
|
+
# every line naming its own `answer_<n>.md` (another index's line stays), cut
|
|
294
|
+
# at its last whole line within `QUESTION_COMMENT_MAX_BYTES` and then naming
|
|
295
|
+
# the file in the `STATE_ARTIFACT_NAME` artifact; then the answer form, the
|
|
296
|
+
# comment's one answer instruction: a comment whose first line is
|
|
297
|
+
# `COMMAND_HANDLE answer <n>` (`<n>` optional when one question is open) and
|
|
298
|
+
# whose following lines are the answer; then a fenced copy block of that form,
|
|
299
|
+
# `COMMAND_HANDLE answer <n>` over `<your answer>`; the marker adds
|
|
300
|
+
# `question=<n>`. With no question open it posts nothing, sets no label and
|
|
301
|
+
# prints one `::error::` line naming the branch. The label
|
|
296
302
|
# is set once per target, not per question. On a public repository a question
|
|
297
303
|
# comment and its answer are public, as the artifact already is
|
|
298
304
|
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
@@ -356,8 +362,9 @@
|
|
|
356
362
|
# and outside the sending-verb gate: it gates itself after reading the event,
|
|
357
363
|
# so a refusal can still be replied to. The workflow's `if:` only saves a
|
|
358
364
|
# runner; every rule below holds without it. It handles `GITHUB_EVENT_NAME`
|
|
359
|
-
# `issue_comment
|
|
360
|
-
#
|
|
365
|
+
# `issue_comment`, `pull_request_review` (THE REVIEW, below), and `issues`,
|
|
366
|
+
# `pull_request` and `delete` (THE CLOSE, below); any other name, or an event
|
|
367
|
+
# file it cannot read, exits 1.
|
|
361
368
|
# It reads `.action`, `.comment.body`, `.issue.number`,
|
|
362
369
|
# `.issue.pull_request.url`, `.sender.login` and `.sender.type`, each by `jq`
|
|
363
370
|
# into a variable (data, never shell source), plus `trigger`'s environment.
|
|
@@ -429,7 +436,25 @@
|
|
|
429
436
|
# way on. On 0 with no other question open, a reply that the run resumes and
|
|
430
437
|
# `running` on its issue and pull request; with others open, a reply naming
|
|
431
438
|
# them, and the label stays `parked`. An answer becomes a comment on the item,
|
|
432
|
-
# public on a public repository, as the question already is.
|
|
439
|
+
# public on a public repository, as the question already is. A STOPPED RUN IS
|
|
440
|
+
# NAMED `stopped`: a stop leaves the bundle as it was, so after `pause`,
|
|
441
|
+
# `resume`, `answer` and `clear` read the state, `control_state_word_var` asks
|
|
442
|
+
# `remote_branch_stopped` (one extra run listing) whether a `running`,
|
|
443
|
+
# `parked`, `park_loop` or `paused` run was stopped, and every reply names it
|
|
444
|
+
# `stopped`, with the state underneath where the way on depends on it. The
|
|
445
|
+
# states each verb accepts are unchanged, except that `pause` refuses a
|
|
446
|
+
# stopped `running` run, whose cancelled job is still finishing: `resume`
|
|
447
|
+
# still resumes a stopped `paused` run, and on a stopped `parked` run the
|
|
448
|
+
# answer, on a stopped `park_loop` run the clear, is its resume. `status` is
|
|
449
|
+
# READ-ONLY and accepted in every state, `none` and a run in flight included:
|
|
450
|
+
# it reads the state (`control_state_var`, `control_state_word_var`) and the
|
|
451
|
+
# first `- [ ] ` entry of the flow-progress ledger on origin's tip with the
|
|
452
|
+
# `## ` heading above it, and replies once — the state by `CS_WORD` with its
|
|
453
|
+
# pause reason, the state underneath a stopped one, an expired bundle's detail,
|
|
454
|
+
# the next ledger entry (or that every entry is ticked, or that the ledger
|
|
455
|
+
# could not be read), each open question with its `answer <n>` form, and the
|
|
456
|
+
# latest run's URL. It sets no label and runs no child verb but `fetch`; a
|
|
457
|
+
# failed state read is a refusal and exit 3.
|
|
433
458
|
# THE REVIEW. A `pull_request_review` event reads `.action`, `.review.state`,
|
|
434
459
|
# `.review.body`, `.review.id`, `.review.html_url`, `.review.submitted_at`,
|
|
435
460
|
# `.pull_request.number`, `.pull_request.head.ref`,
|
|
@@ -451,7 +476,10 @@
|
|
|
451
476
|
# `your review was collected`, naming the state and saying the next round
|
|
452
477
|
# starts by itself when that run finishes, plus the state's way on: `answer
|
|
453
478
|
# <n>` for `parked`, `clear` for `park_loop`, `resume` for any `paused` but
|
|
454
|
-
# `usage`, and for `usage` that it resumes after the reset.
|
|
479
|
+
# `usage`, and for `usage` that it resumes after the reset. A run of those
|
|
480
|
+
# states that `remote_branch_stopped` finds stopped is named `stopped`, and its
|
|
481
|
+
# way on is the one `resume` names for it: `answer <n>`, `clear`, `resume`, or
|
|
482
|
+
# for `running` `resume` once the cancelled job has ended. The review stays on
|
|
455
483
|
# the pull request, and a later round collects it. Settled: a round is CUMULATIVE,
|
|
456
484
|
# built by `round_collect` in a fresh file under `RUNNER_TEMP` from ONE
|
|
457
485
|
# paginated `pulls/<n>/reviews` and ONE paginated `pulls/<n>/comments` listing
|
|
@@ -505,11 +533,59 @@
|
|
|
505
533
|
# naming its last stderr line, and exit 3. Every reply goes to the item the comment was
|
|
506
534
|
# typed on, opens `@<login>`, and carries the `reply` marker; a refusal reads
|
|
507
535
|
# `@<login>: `<verb>` was not run: <reason>. <way on>`.
|
|
508
|
-
#
|
|
509
|
-
#
|
|
536
|
+
# THE CLOSE. Whatever the run's phase, it is stopped when its issue is closed,
|
|
537
|
+
# its pull request closed or merged, or its branch deleted; the routing is by
|
|
538
|
+
# event name, never by verb, so a comment naming `close` is not one. `issues`
|
|
539
|
+
# reads `.action`, `.issue.number`, `.sender.login`, `.sender.type`;
|
|
540
|
+
# `pull_request` reads `.action`, `.pull_request.number`,
|
|
541
|
+
# `.pull_request.merged`, `.pull_request.head.ref`,
|
|
542
|
+
# `.pull_request.head.repo.full_name` and the sender; `delete` reads `.ref`,
|
|
543
|
+
# `.ref_type` and the sender — each by `event_field`. Ignored, with one line
|
|
544
|
+
# and no `gh` call: an action other than `closed`; a pull request whose head
|
|
545
|
+
# repository is not `GITHUB_REPOSITORY`; a `ref_type` other than `branch`, or
|
|
546
|
+
# a ref `valid_branch` rejects. A CLOSE IS NEVER REPLIED TO: each gate below,
|
|
547
|
+
# in order, is one line and exit 0, with no comment, label or dispatch —
|
|
548
|
+
# 1. `HARNESS_REMOTE_STOP` is set
|
|
549
|
+
# 2. `forge_on` fails
|
|
550
|
+
# 3. the actor: on `issues` and `pull_request`, `authorise_actor` refused
|
|
551
|
+
# (statuses 1–3), the line naming the login and `AUTH_WHY`; on `delete`, only a `Bot`
|
|
552
|
+
# sender `trigger_bot_listed` does not list, since deleting a branch
|
|
553
|
+
# already needs write access
|
|
554
|
+
# 4. no branch: an issue's from `control_issue_branch_var` (no genuine start
|
|
555
|
+
# comment), a pull request's head ref, a deletion's `.ref`; then not a
|
|
556
|
+
# valid branch name
|
|
557
|
+
# 5. `hr_branch_is_protected` does not answer 1
|
|
558
|
+
# 6. on `pull_request`, the head branch absent on origin (`remote_branch_exists`
|
|
559
|
+
# answers 1): GitHub closed the pull request because the branch was
|
|
560
|
+
# deleted, and the `delete` event's job stops the run; an `ls-remote` that
|
|
561
|
+
# cannot answer is one line and proceeds
|
|
562
|
+
# There is no ledger-at-tip check (`forge_recognised`): a merged or deleted
|
|
563
|
+
# branch may no longer carry one, and a listed `harness run <b>` run is the
|
|
564
|
+
# proof. The state is read by `control_state_var`: only `running`, `parked`,
|
|
565
|
+
# `park_loop` and `paused` are acted on; `completed`, `failed` and `none` are
|
|
566
|
+
# `left alone: the run on <b> is <state>`, and one `remote_branch_stopped`
|
|
567
|
+
# already finds stopped is `already stopped` — each one line and exit 0. Then
|
|
568
|
+
# `stop <b> --actor <login> --note <note>` runs as a child (`control_child`),
|
|
569
|
+
# the note `Stopped because @<login> closed issue #<n>.`, `… closed pull
|
|
570
|
+
# request #<n>.` or `… merged pull request #<n>.` (both with `--pr <n>`), or
|
|
571
|
+
# `… deleted the branch `<b>`.` (with `--branch-gone`). Every note but the
|
|
572
|
+
# deletion's adds that the workflow runs and their `harness-state` artifacts
|
|
573
|
+
# are kept; a closed pull request's `stopped` comment says to reopen it, or to
|
|
574
|
+
# use the issue, before commenting `@sdlc-harness resume`. The runs and
|
|
575
|
+
# artifacts are kept. Reopening the issue or the pull
|
|
576
|
+
# request does nothing. A `pull_request` close job runs the pull request's
|
|
577
|
+
# merge-commit copy of the workflow, as a review job does
|
|
578
|
+
# (docs/github-integration-research.md -> C2); the script stays the default
|
|
579
|
+
# branch's.
|
|
580
|
+
# 0 handled (replied), or ignored; a close stopped, or ignored (one line)
|
|
581
|
+
# 1 not an event named above, or the event could not be read
|
|
510
582
|
# 2 refused (replied)
|
|
511
583
|
# 3 a `gh` step failed: the reply could not be posted (an `::error::`
|
|
512
|
-
# line), or the action failed and was replied to
|
|
584
|
+
# line), or the action failed and was replied to; for a close, an
|
|
585
|
+
# `::error::` line and no reply, because the item is closed: the stop
|
|
586
|
+
# child failed (naming `CHILD_LAST`), or the repository name, the
|
|
587
|
+
# closer's permission, the issue's comments or the run's state could
|
|
588
|
+
# not be read
|
|
513
589
|
# 4 a review's round could not be placed; nothing was dispatched (replied)
|
|
514
590
|
#
|
|
515
591
|
# `restore` AND `save` ARE THE JOB-SIDE VERBS: the run workflow calls them in
|
|
@@ -606,12 +682,17 @@
|
|
|
606
682
|
# run URL, no dispatch
|
|
607
683
|
# decision continue refused, in this order: `HARNESS_REMOTE_STOP` set (one
|
|
608
684
|
# `paused`); the branch stopped (one log line, no
|
|
609
|
-
# notification — the user asked for it);
|
|
685
|
+
# notification — the user asked for it); the branch absent
|
|
686
|
+
# on origin (one log line, no notification; an
|
|
687
|
+
# `ls-remote` that cannot answer is one line and proceeds,
|
|
688
|
+
# since the dispatch then fails loudly); chain unreadable
|
|
610
689
|
# or `chain + 1` over `HARNESS_MAX_CHAIN` (one `failed`);
|
|
611
690
|
# otherwise re-dispatched. A dispatch that fails is one
|
|
612
691
|
# `paused` naming its error
|
|
613
|
-
# decision wait-poller the branch stopped: one log
|
|
614
|
-
#
|
|
692
|
+
# decision wait-poller the branch stopped, or absent on origin: one log
|
|
693
|
+
# line (an `ls-remote` that cannot answer is one line and
|
|
694
|
+
# proceeds). Otherwise `gh workflow enable
|
|
695
|
+
# WORKFLOW_RESUME_FILE`; a failed enable
|
|
615
696
|
# (the job token's enable permission is unverified) is one
|
|
616
697
|
# `paused` saying auto-resume is unavailable
|
|
617
698
|
# decision stop nothing: job mode has already notified
|
|
@@ -647,8 +728,10 @@
|
|
|
647
728
|
# says `status: paused` / `pause_reason: usage` it is waiting, because the job
|
|
648
729
|
# uploads before its `continue` step enables the poller; an artifact lookup or
|
|
649
730
|
# download that fails for it is one line and waiting. For a `completed` run,
|
|
650
|
-
# skipped, not waiting: a stopped branch, a
|
|
651
|
-
#
|
|
731
|
+
# skipped, not waiting: a stopped branch, a branch absent on origin (its state
|
|
732
|
+
# entry dropped, so a later branch of the name starts clean; an `ls-remote`
|
|
733
|
+
# that cannot answer is one line and proceeds), a bundle that cannot be
|
|
734
|
+
# downloaded (one line), and anything but `status: paused` / `pause_reason: usage` with an
|
|
652
735
|
# integer `usage_resume_at`, and a run whose state entry says `notified`. Due
|
|
653
736
|
# (reset passed): re-dispatched under the same chain limit — a refusal is one
|
|
654
737
|
# `failed` and not waiting; a success drops the branch's state entry. A
|
|
@@ -777,11 +860,24 @@
|
|
|
777
860
|
# local registry record exists, it writes `remote_stopped_at` and sets `status`
|
|
778
861
|
# to `failed` in one `hr_registry_set` call; a partial stop leaves the record alone
|
|
779
862
|
# and exits 3, so running `stop` again is the remedy. (4) A complete stop is then
|
|
780
|
-
# reported as `report stopped` with the note
|
|
781
|
-
# `--actor` (a login, the trigger's shape plus an
|
|
782
|
-
# is a usage error), else one naming a local
|
|
783
|
-
#
|
|
784
|
-
#
|
|
863
|
+
# reported as `report stopped` with the note `--note <text>` when given, else
|
|
864
|
+
# `Stopped by @<actor>.` under `--actor` (a login, the trigger's shape plus an
|
|
865
|
+
# optional `[bot]`; anything else is a usage error), else one naming a local
|
|
866
|
+
# stop; a partial stop reports nothing. Under `--pr <n>` that report's comment
|
|
867
|
+
# and label go to pull request <n> whatever its state — the open-PR lookup
|
|
868
|
+
# cannot find a closed one — and the issue is still labelled. The cancelled
|
|
869
|
+
# job's own `failed` is then posted nowhere, because `report` finds the branch
|
|
870
|
+
# stopped. `--branch-gone` is for a branch GitHub no longer has: (1)'s marker
|
|
871
|
+
# is dispatched with `--ref` set to GitHub's own default branch (read as `warm`
|
|
872
|
+
# reads it; a failed read exits 3 before anything is sent) and still carries
|
|
873
|
+
# `-f branch=<branch>`, so its `harness stop <branch>` title is unchanged;
|
|
874
|
+
# (2)'s listing also reads `headSha,createdAt`; (4) reads the issue from the
|
|
875
|
+
# task prompt at the newest `harness run <branch>` run's `headSha` through the
|
|
876
|
+
# contents API rather than from `origin/<branch>` (a failed read is one line
|
|
877
|
+
# and no issue), and its text says the branch was deleted, so the run cannot
|
|
878
|
+
# be resumed, and that its workflow runs and artifacts are kept. That read
|
|
879
|
+
# rests on GitHub serving a commit no branch points at, which is unverified
|
|
880
|
+
# (`docs/github-run-control.md` -> `## 8. What is not verified here`).
|
|
785
881
|
#
|
|
786
882
|
# `warm` dispatches action=warm on GitHub's OWN default branch (`gh repo view
|
|
787
883
|
# --json defaultBranchRef`), which may differ from the configured
|
|
@@ -1103,7 +1199,7 @@ LEGACY_TRIGGER_LABEL='harness'
|
|
|
1103
1199
|
TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
1104
1200
|
WORKFLOW_CONTROL_FILE='harness-control.yml'
|
|
1105
1201
|
COMMAND_HANDLE='@sdlc-harness'
|
|
1106
|
-
COMMAND_VERBS='answer pause resume stop clear'
|
|
1202
|
+
COMMAND_VERBS='answer pause resume stop clear status'
|
|
1107
1203
|
COMMENT_MARKER='<!-- sdlc-harness'
|
|
1108
1204
|
REVIEW_ROUND_STATE='changes_requested'
|
|
1109
1205
|
STATE_LABEL_PREFIX='sdlc-harness: '
|
|
@@ -1165,7 +1261,7 @@ usage() {
|
|
|
1165
1261
|
echo "usage: remote-run.sh dispatch <branch> --engine <task|user_review|docs> [--resume none|answer|pause] [--answers-from <clar_dir> --indexes \"<n> ...\"] [--park-loop-clear] [--chain <n>] [--repo <root>]" >&2
|
|
1166
1262
|
echo " remote-run.sh pause <branch> [--repo <root>]" >&2
|
|
1167
1263
|
echo " remote-run.sh warm [--repo <root>]" >&2
|
|
1168
|
-
echo " remote-run.sh stop <branch> [--actor <login>] [--repo <root>]" >&2
|
|
1264
|
+
echo " remote-run.sh stop <branch> [--actor <login>] [--note <text>] [--pr <n>] [--branch-gone] [--repo <root>]" >&2
|
|
1169
1265
|
echo " remote-run.sh status <branch> [--repo <root>]" >&2
|
|
1170
1266
|
echo " remote-run.sh sync <branch> [--repo <root>]" >&2
|
|
1171
1267
|
echo " remote-run.sh fetch <branch> <out_dir> [--repo <root>]" >&2
|
|
@@ -1282,6 +1378,7 @@ source_arg=""
|
|
|
1282
1378
|
reviewers_arg=""
|
|
1283
1379
|
allow_no_run=0
|
|
1284
1380
|
pr_arg=""
|
|
1381
|
+
branch_gone=0
|
|
1285
1382
|
|
|
1286
1383
|
while [ "$#" -gt 0 ]; do
|
|
1287
1384
|
case "$1" in
|
|
@@ -1314,7 +1411,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
1314
1411
|
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
1315
1412
|
review_file="$2"; shift 2 ;;
|
|
1316
1413
|
--note)
|
|
1317
|
-
[ "$verb" = report ] || usage "$1 is a report option"
|
|
1414
|
+
[ "$verb" = report ] || [ "$verb" = stop ] || usage "$1 is a report or stop option"
|
|
1318
1415
|
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1319
1416
|
report_note="$2"; shift 2 ;;
|
|
1320
1417
|
--actor)
|
|
@@ -1333,12 +1430,15 @@ while [ "$#" -gt 0 ]; do
|
|
|
1333
1430
|
[ "$verb" = review ] || usage "$1 is a review option"
|
|
1334
1431
|
allow_no_run=1; shift ;;
|
|
1335
1432
|
--pr)
|
|
1336
|
-
[ "$verb" = collect ] || usage "$1 is a collect option"
|
|
1433
|
+
[ "$verb" = collect ] || [ "$verb" = stop ] || usage "$1 is a collect or stop option"
|
|
1337
1434
|
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1338
1435
|
case "$2" in
|
|
1339
1436
|
''|*[!0-9]*|0*) usage "--pr needs a positive pull request number" ;;
|
|
1340
1437
|
esac
|
|
1341
1438
|
pr_arg="$2"; shift 2 ;;
|
|
1439
|
+
--branch-gone)
|
|
1440
|
+
[ "$verb" = stop ] || usage "$1 is a stop option"
|
|
1441
|
+
branch_gone=1; shift ;;
|
|
1342
1442
|
-*)
|
|
1343
1443
|
usage "unknown option '$1'" ;;
|
|
1344
1444
|
*)
|
|
@@ -1634,29 +1734,48 @@ verb_pause() {
|
|
|
1634
1734
|
echo "remote-run.sh: dispatched action=pause for $branch"
|
|
1635
1735
|
}
|
|
1636
1736
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1737
|
+
# github_default_branch_var — GITHUB_DEFAULT_BRANCH, GitHub's own default
|
|
1738
|
+
# branch; exits 3 when it cannot be read.
|
|
1739
|
+
GITHUB_DEFAULT_BRANCH=""
|
|
1740
|
+
github_default_branch_var() {
|
|
1639
1741
|
gh_call repo view --json defaultBranchRef || gh_fail "reading GitHub's default branch failed"
|
|
1640
|
-
|
|
1641
|
-
if ! valid_branch "$
|
|
1742
|
+
GITHUB_DEFAULT_BRANCH=$(printf '%s' "$GH_OUT" | jq -r '.defaultBranchRef.name // empty' 2>/dev/null)
|
|
1743
|
+
if ! valid_branch "$GITHUB_DEFAULT_BRANCH"; then
|
|
1642
1744
|
GH_ERR="no defaultBranchRef.name in its output"
|
|
1643
1745
|
gh_fail "reading GitHub's default branch failed"
|
|
1644
1746
|
fi
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1749
|
+
verb_warm() {
|
|
1750
|
+
local default_branch
|
|
1751
|
+
github_default_branch_var
|
|
1752
|
+
default_branch="$GITHUB_DEFAULT_BRANCH"
|
|
1645
1753
|
gh_call workflow run "$WORKFLOW_RUN_FILE" --ref "$default_branch" -f "action=warm" -f "branch=$default_branch" || gh_fail "warm-up on '$default_branch' failed"
|
|
1646
1754
|
echo "remote-run.sh: dispatched action=warm on $default_branch"
|
|
1647
1755
|
}
|
|
1648
1756
|
|
|
1649
1757
|
verb_stop() {
|
|
1650
|
-
local ids id failed=0 first_err="" registry stopped_at
|
|
1758
|
+
local ids id failed=0 first_err="" registry stopped_at marker_ref="$branch" fields=databaseId,displayTitle,status
|
|
1759
|
+
local gone="" gone_sha="" note
|
|
1651
1760
|
stopped_at=$(date +%s)
|
|
1652
|
-
|
|
1761
|
+
if [ "$branch_gone" -eq 1 ]; then
|
|
1762
|
+
github_default_branch_var
|
|
1763
|
+
marker_ref="$GITHUB_DEFAULT_BRANCH"
|
|
1764
|
+
gone=gone
|
|
1765
|
+
fields="$fields,headSha,createdAt"
|
|
1766
|
+
fi
|
|
1767
|
+
gh_call workflow run "$WORKFLOW_RUN_FILE" --ref "$marker_ref" -f "action=stop" -f "branch=$branch" || gh_fail "stop marker for '$branch' failed, nothing cancelled"
|
|
1653
1768
|
echo "remote-run.sh: dispatched the action=stop marker for $branch"
|
|
1654
1769
|
|
|
1655
|
-
gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json
|
|
1770
|
+
gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json "$fields" --limit 100 || gh_fail "listing the runs of '$branch' failed"
|
|
1656
1771
|
ids=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" '.[] | select(.displayTitle == $t and (.status == "queued" or .status == "in_progress" or .status == "waiting")) | .databaseId' 2>/dev/null) || {
|
|
1657
1772
|
GH_ERR="its run list is not the expected JSON"
|
|
1658
1773
|
gh_fail "listing the runs of '$branch' failed"
|
|
1659
1774
|
}
|
|
1775
|
+
if [ -n "$gone" ]; then
|
|
1776
|
+
gone_sha=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" \
|
|
1777
|
+
'[.[] | select(.displayTitle == $t)] | max_by(.createdAt // "") | .headSha // empty' 2>/dev/null) || gone_sha=""
|
|
1778
|
+
fi
|
|
1660
1779
|
for id in $ids; do
|
|
1661
1780
|
if gh_call run cancel "$id"; then
|
|
1662
1781
|
echo "remote-run.sh: asked GitHub to cancel run $id of $branch"
|
|
@@ -1677,11 +1796,14 @@ verb_stop() {
|
|
|
1677
1796
|
|| echo "remote-run.sh: stopped on GitHub, but the local record of $branch could not be updated" >&2
|
|
1678
1797
|
fi
|
|
1679
1798
|
echo "remote-run.sh: stopped $branch"
|
|
1680
|
-
if [ -n "$
|
|
1681
|
-
|
|
1799
|
+
if [ -n "$report_note" ]; then
|
|
1800
|
+
note="$report_note"
|
|
1801
|
+
elif [ -n "$actor_arg" ]; then
|
|
1802
|
+
note="Stopped by @$actor_arg."
|
|
1682
1803
|
else
|
|
1683
|
-
|
|
1804
|
+
note="Stopped from a local \`remote-run.sh stop\`."
|
|
1684
1805
|
fi
|
|
1806
|
+
forge_report stopped "$branch" "$note" "$pr_arg" "$gone" "$gone_sha"
|
|
1685
1807
|
}
|
|
1686
1808
|
|
|
1687
1809
|
# list_runs — the branch's runs of the workflow into GH_OUT; exits 3 on failure.
|
|
@@ -2434,6 +2556,27 @@ remote_branch_stopped() {
|
|
|
2434
2556
|
[ "$verdict" = stopped ]
|
|
2435
2557
|
}
|
|
2436
2558
|
|
|
2559
|
+
# remote_branch_exists <branch> — 0 when origin lists refs/heads/<branch>, 1
|
|
2560
|
+
# when `ls-remote` answers 2 (no such head), 2 on any other failure, with
|
|
2561
|
+
# REMOTE_BRANCH_ERR holding its exit status and first stderr line.
|
|
2562
|
+
REMOTE_BRANCH_ERR=""
|
|
2563
|
+
remote_branch_exists() {
|
|
2564
|
+
local errfile status
|
|
2565
|
+
REMOTE_BRANCH_ERR=""
|
|
2566
|
+
errfile=$(mktemp) || { REMOTE_BRANCH_ERR="mktemp failed"; return 2; }
|
|
2567
|
+
git -C "$root" ls-remote --exit-code --heads origin "refs/heads/$1" >/dev/null 2>"$errfile"
|
|
2568
|
+
status=$?
|
|
2569
|
+
case "$status" in
|
|
2570
|
+
0) rm -f "$errfile"; return 0 ;;
|
|
2571
|
+
2) rm -f "$errfile"; return 1 ;;
|
|
2572
|
+
esac
|
|
2573
|
+
IFS= read -r REMOTE_BRANCH_ERR <"$errfile" || :
|
|
2574
|
+
[ -n "$REMOTE_BRANCH_ERR" ] || REMOTE_BRANCH_ERR="(no stderr)"
|
|
2575
|
+
REMOTE_BRANCH_ERR="git ls-remote exited $status: $REMOTE_BRANCH_ERR"
|
|
2576
|
+
rm -f "$errfile"
|
|
2577
|
+
return 2
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2437
2580
|
# redispatch <engine> <chain> — `dispatch <branch> --engine <engine> --resume
|
|
2438
2581
|
# pause --chain <chain>` for the global branch, in a subshell so dispatch's own
|
|
2439
2582
|
# exits stay its own. On failure REDISPATCH_ERR holds its first stderr line.
|
|
@@ -2495,6 +2638,11 @@ continue_redispatch() {
|
|
|
2495
2638
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); not re-dispatched"; return 0 ;;
|
|
2496
2639
|
2) notify paused "$branch" "Not re-dispatched: the stop-marker check failed ($GH_ERR). Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "")." "Not continued: whether the run was stopped could not be checked ($GH_ERR)."; return 0 ;;
|
|
2497
2640
|
esac
|
|
2641
|
+
remote_branch_exists "$branch"
|
|
2642
|
+
case $? in
|
|
2643
|
+
1) echo "remote-run.sh: $branch no longer exists on origin; not re-dispatched"; return 0 ;;
|
|
2644
|
+
2) echo "remote-run.sh: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2645
|
+
esac
|
|
2498
2646
|
if ! max_chain_var; then
|
|
2499
2647
|
notify failed "$branch" "Not re-dispatched: HARNESS_MAX_CHAIN '$MAX_CHAIN' is not a non-negative integer." "Not continued: the repository variable \`HARNESS_MAX_CHAIN\` ('$MAX_CHAIN') is not a non-negative integer."
|
|
2500
2648
|
return 0
|
|
@@ -2519,6 +2667,11 @@ continue_wait_poller() {
|
|
|
2519
2667
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); the resume poller is not enabled"; return 0 ;;
|
|
2520
2668
|
2) notify paused "$branch" "Auto-resume not enabled: the stop-marker check failed ($GH_ERR). Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "")." "The automatic resume after the usage limit was not scheduled: whether the run was stopped could not be checked ($GH_ERR). $USAGE_RESUME_NOTE"; return 0 ;;
|
|
2521
2669
|
esac
|
|
2670
|
+
remote_branch_exists "$branch"
|
|
2671
|
+
case $? in
|
|
2672
|
+
1) echo "remote-run.sh: $branch no longer exists on origin; the resume poller is not enabled"; return 0 ;;
|
|
2673
|
+
2) echo "remote-run.sh: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2674
|
+
esac
|
|
2522
2675
|
if gh_call workflow enable "$WORKFLOW_RESUME_FILE"; then
|
|
2523
2676
|
echo "remote-run.sh: enabled $WORKFLOW_RESUME_FILE for $branch"
|
|
2524
2677
|
else
|
|
@@ -2684,6 +2837,11 @@ poll_branch() {
|
|
|
2684
2837
|
0) echo "remote-run.sh: poll: $branch is stopped ($STOPPED_LINE); skipped"; return 1 ;;
|
|
2685
2838
|
2) echo "remote-run.sh: poll: the stop-marker check for $branch failed ($GH_ERR); skipped"; return 1 ;;
|
|
2686
2839
|
esac
|
|
2840
|
+
remote_branch_exists "$branch"
|
|
2841
|
+
case $? in
|
|
2842
|
+
1) poll_state_drop "$branch"; echo "remote-run.sh: poll: $branch no longer exists on origin; skipped"; return 1 ;;
|
|
2843
|
+
2) echo "remote-run.sh: poll: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2844
|
+
esac
|
|
2687
2845
|
if [ "$state" != completed ]; then
|
|
2688
2846
|
# Never dispatched from here: the job's own `continue` step enables the
|
|
2689
2847
|
# poller after its upload, and a later tick sees the run completed.
|
|
@@ -3667,7 +3825,7 @@ forge_marker() {
|
|
|
3667
3825
|
FORGE_ISSUE=""
|
|
3668
3826
|
FORGE_TRIGGER_LABEL=""
|
|
3669
3827
|
forge_issue_var() {
|
|
3670
|
-
local state_rel rel prompt
|
|
3828
|
+
local state_rel rel prompt
|
|
3671
3829
|
FORGE_ISSUE=""
|
|
3672
3830
|
FORGE_TRIGGER_LABEL=""
|
|
3673
3831
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
@@ -3677,6 +3835,41 @@ forge_issue_var() {
|
|
|
3677
3835
|
fi
|
|
3678
3836
|
rel=$(hr_task_prompt_rel "$state_rel" "$1")
|
|
3679
3837
|
prompt=$(git -C "$root" show "refs/remotes/origin/$1:$rel" 2>/dev/null) || return 0
|
|
3838
|
+
forge_provenance_parse "$prompt"
|
|
3839
|
+
}
|
|
3840
|
+
|
|
3841
|
+
# forge_issue_at_commit_var <branch> <sha> — forge_issue_var for a branch
|
|
3842
|
+
# GitHub no longer has: the task prompt is read through the contents API at
|
|
3843
|
+
# <sha>. A failed read is one line and leaves both empty.
|
|
3844
|
+
forge_issue_at_commit_var() {
|
|
3845
|
+
local state_rel path
|
|
3846
|
+
FORGE_ISSUE=""
|
|
3847
|
+
FORGE_TRIGGER_LABEL=""
|
|
3848
|
+
if ! [[ "${2-}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
|
3849
|
+
echo "remote-run.sh: report: no \`harness run $1\` run names a commit; the issue of $1 is unknown" >&2
|
|
3850
|
+
return 1
|
|
3851
|
+
fi
|
|
3852
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3853
|
+
if [ -z "$state_rel" ]; then
|
|
3854
|
+
echo "remote-run.sh: cannot resolve the state directory under '$root'" >&2
|
|
3855
|
+
return 1
|
|
3856
|
+
fi
|
|
3857
|
+
GH_ERR="its path could not be encoded"
|
|
3858
|
+
path=$(jq -rn --arg p "$(hr_task_prompt_rel "$state_rel" "$1")" '$p | split("/") | map(@uri) | join("/")') || path=""
|
|
3859
|
+
if [ -z "$path" ] || ! gh_call api "repos/$FORGE_REPO/contents/$path?ref=$2" -H "Accept: application/vnd.github.raw"; then
|
|
3860
|
+
echo "remote-run.sh: report: reading the task prompt of $1 at $2 failed: $GH_ERR" >&2
|
|
3861
|
+
return 1
|
|
3862
|
+
fi
|
|
3863
|
+
forge_provenance_parse "$GH_OUT"
|
|
3864
|
+
}
|
|
3865
|
+
|
|
3866
|
+
# forge_provenance_parse <prompt> — FORGE_ISSUE and FORGE_TRIGGER_LABEL from
|
|
3867
|
+
# the last provenance line of <prompt>, matched against this repository's own
|
|
3868
|
+
# issue URL only.
|
|
3869
|
+
forge_provenance_parse() {
|
|
3870
|
+
local prompt="$1" line rest num prefix label
|
|
3871
|
+
FORGE_ISSUE=""
|
|
3872
|
+
FORGE_TRIGGER_LABEL=""
|
|
3680
3873
|
prefix="Started from $FORGE_SERVER/$FORGE_REPO/issues/"
|
|
3681
3874
|
while IFS= read -r line; do
|
|
3682
3875
|
case "$line" in
|
|
@@ -3801,10 +3994,20 @@ forge_utc() {
|
|
|
3801
3994
|
|
|
3802
3995
|
# forge_question_body <out_file> <branch> <n> <open_count> <clar_dir> [<note>] —
|
|
3803
3996
|
# write question <n>'s park comment, without its marker, into <out_file>: the
|
|
3804
|
-
# file's bytes
|
|
3805
|
-
#
|
|
3997
|
+
# file's bytes less every line naming `answer_<n>.md` (the file channel's own
|
|
3998
|
+
# answer line; another index's stays), cut at the last whole line within
|
|
3999
|
+
# QUESTION_COMMENT_MAX_BYTES when it is over it (measured in bytes; `${#…}`
|
|
4000
|
+
# counts characters), then the answer form and its copy block. Leaves
|
|
4001
|
+
# <out_file>.q and, when cut, <out_file>.cut for the caller to remove.
|
|
3806
4002
|
forge_question_body() {
|
|
3807
|
-
local out="$1" br="$2" n="$3" count="$4" qfile="$5/question_$3.md" note="${6-}" size cut=0 last
|
|
4003
|
+
local out="$1" br="$2" n="$3" count="$4" qfile="$5/question_$3.md" note="${6-}" size cut=0 last rc
|
|
4004
|
+
[ -r "$qfile" ] || return 1
|
|
4005
|
+
# -a and LC_ALL=C: a question is text whatever bytes it holds. Status 1 is
|
|
4006
|
+
# every line dropped, not a failure.
|
|
4007
|
+
rc=0
|
|
4008
|
+
LC_ALL=C grep -avF "answer_$n.md" "$qfile" >"$out.q" || rc=$?
|
|
4009
|
+
[ "$rc" -le 1 ] || return 1
|
|
4010
|
+
qfile="$out.q"
|
|
3808
4011
|
size=$(wc -c <"$qfile") || return 1
|
|
3809
4012
|
size=$((size))
|
|
3810
4013
|
{
|
|
@@ -3826,17 +4029,19 @@ forge_question_body() {
|
|
|
3826
4029
|
"$HR_REMOTE_CLARIFY_DIR" "$br" "$n" "$STATE_ARTIFACT_NAME"
|
|
3827
4030
|
printf '\nAnswer with a comment whose first line is `%s answer %s` and whose following lines are your answer.' "$COMMAND_HANDLE" "$n"
|
|
3828
4031
|
[ "$count" -ne 1 ] || printf ' This is the only open question, so `%s` may be left out: `%s answer`.' "$n" "$COMMAND_HANDLE"
|
|
3829
|
-
printf '\n'
|
|
4032
|
+
printf '\n\n```\n%s answer %s\n<your answer>\n```\n' "$COMMAND_HANDLE" "$n"
|
|
3830
4033
|
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
3831
4034
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
3832
4035
|
} >>"$out"
|
|
3833
4036
|
}
|
|
3834
4037
|
|
|
3835
|
-
# forge_report <event> <branch> [<note>] — one lifecycle
|
|
3836
|
-
# one per open question) and the state label, by the
|
|
3837
|
-
#
|
|
4038
|
+
# forge_report <event> <branch> [<note> [<pr> [gone [<sha>]]]] — one lifecycle
|
|
4039
|
+
# comment (on `parked`, one per open question) and the state label, by the
|
|
4040
|
+
# target rule above. Read on `stopped` only: <pr>, an explicit pull request
|
|
4041
|
+
# that is the target whatever its state; `gone`, the branch deleted on GitHub,
|
|
4042
|
+
# its issue read from the task prompt at <sha>. Always 0.
|
|
3838
4043
|
forge_report() {
|
|
3839
|
-
local event="$1" br="$2" note="${3-}" state reason="" resume_at="" when registry_file
|
|
4044
|
+
local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
|
|
3840
4045
|
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
|
|
3841
4046
|
case "$event" in
|
|
3842
4047
|
parked|park_loop) state=parked ;;
|
|
@@ -3872,10 +4077,22 @@ forge_report() {
|
|
|
3872
4077
|
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the failure" >&2
|
|
3873
4078
|
fi
|
|
3874
4079
|
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
4080
|
+
[ "$event" = stopped ] || { pr=""; gone=""; }
|
|
4081
|
+
if [ -n "$gone" ]; then
|
|
4082
|
+
forge_issue_at_commit_var "$br" "$gone_sha" || FORGE_ISSUE=""
|
|
4083
|
+
else
|
|
4084
|
+
forge_fetch_branch "$br"
|
|
4085
|
+
forge_issue_var "$br" || FORGE_ISSUE=""
|
|
4086
|
+
fi
|
|
4087
|
+
if [ -n "$pr" ]; then
|
|
4088
|
+
FORGE_PR="$pr"
|
|
4089
|
+
elif [ -n "$gone" ]; then
|
|
4090
|
+
# GitHub closes a pull request whose head is deleted, so none is open.
|
|
4091
|
+
FORGE_PR=""
|
|
4092
|
+
else
|
|
4093
|
+
forge_pr_var "$br" || FORGE_PR=""
|
|
4094
|
+
fi
|
|
4095
|
+
if [ -z "$pr" ] && [ -n "$FORGE_PR" ] && ! forge_recognised "$br"; then
|
|
3879
4096
|
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries no flow-progress ledger; it is not a target"
|
|
3880
4097
|
FORGE_PR=""
|
|
3881
4098
|
fi
|
|
@@ -3905,8 +4122,6 @@ forge_report() {
|
|
|
3905
4122
|
fi ;;
|
|
3906
4123
|
park_loop)
|
|
3907
4124
|
text="The harness run on \`$br\` is on hold: it parked on its questions again and again without progress. Comment \`${COMMAND_HANDLE} clear\` to clear the hold and let it continue." ;;
|
|
3908
|
-
parked)
|
|
3909
|
-
text="The harness run on \`$br\` is waiting for an answer. Its questions are in the run's \`$STATE_ARTIFACT_NAME\` artifact." ;;
|
|
3910
4125
|
resumed)
|
|
3911
4126
|
text="The harness run on \`$br\` resumed." ;;
|
|
3912
4127
|
failed)
|
|
@@ -3917,7 +4132,14 @@ forge_report() {
|
|
|
3917
4132
|
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."
|
|
3918
4133
|
fi ;;
|
|
3919
4134
|
stopped)
|
|
3920
|
-
|
|
4135
|
+
if [ -n "$gone" ]; then
|
|
4136
|
+
text="The harness run on \`$br\` was stopped: its branch was deleted, so the run cannot be resumed. Its workflow runs and their artifacts are kept."
|
|
4137
|
+
elif [ -n "$pr" ]; then
|
|
4138
|
+
# Only a close passes <pr>, and every command on a closed pull request is refused.
|
|
4139
|
+
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."
|
|
4140
|
+
else
|
|
4141
|
+
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."
|
|
4142
|
+
fi ;;
|
|
3921
4143
|
round)
|
|
3922
4144
|
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again." ;;
|
|
3923
4145
|
esac
|
|
@@ -3926,6 +4148,10 @@ forge_report() {
|
|
|
3926
4148
|
if [ "$event" = parked ]; then
|
|
3927
4149
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3928
4150
|
[ -z "$state_rel" ] || open_questions_in "$root/${state_rel%/}"
|
|
4151
|
+
if [ -z "$OPEN_QUESTIONS" ]; then
|
|
4152
|
+
echo "::error::remote-run.sh: report: $br was classified parked with no open question; nothing posted"
|
|
4153
|
+
return 0
|
|
4154
|
+
fi
|
|
3929
4155
|
fi
|
|
3930
4156
|
|
|
3931
4157
|
tmp="${RUNNER_TEMP-}"
|
|
@@ -3943,7 +4169,7 @@ forge_report() {
|
|
|
3943
4169
|
else
|
|
3944
4170
|
echo "remote-run.sh: report: cannot write question $n's comment for #$target; not posted" >&2
|
|
3945
4171
|
fi
|
|
3946
|
-
rm -f "$file" "$file.cut"
|
|
4172
|
+
rm -f "$file" "$file.cut" "$file.q"
|
|
3947
4173
|
else
|
|
3948
4174
|
echo "remote-run.sh: report: cannot create question $n's comment file for #$target; not posted" >&2
|
|
3949
4175
|
fi
|
|
@@ -4256,6 +4482,62 @@ control_state_var() {
|
|
|
4256
4482
|
return 0
|
|
4257
4483
|
}
|
|
4258
4484
|
|
|
4485
|
+
# control_branch_stopped <state> — 0 when <state> is unfinished (`running`,
|
|
4486
|
+
# `parked`, `park_loop`, `paused`) and `remote_branch_stopped` finds
|
|
4487
|
+
# CONTROL_BRANCH stopped; else 1, after one stderr line when the check failed.
|
|
4488
|
+
# The run list, not the bundle, records a stop.
|
|
4489
|
+
control_branch_stopped() {
|
|
4490
|
+
local status=0
|
|
4491
|
+
case "$1" in
|
|
4492
|
+
running|parked|park_loop|paused) ;;
|
|
4493
|
+
*) return 1 ;;
|
|
4494
|
+
esac
|
|
4495
|
+
remote_branch_stopped "$CONTROL_BRANCH" || status=$?
|
|
4496
|
+
case "$status" in
|
|
4497
|
+
0) return 0 ;;
|
|
4498
|
+
1) ;;
|
|
4499
|
+
*) echo "remote-run.sh: control: whether \`$CONTROL_BRANCH\` is stopped could not be read ($GH_ERR); the reply names the state as read" >&2 ;;
|
|
4500
|
+
esac
|
|
4501
|
+
return 1
|
|
4502
|
+
}
|
|
4503
|
+
|
|
4504
|
+
# control_state_word_var — called right after a successful control_state_var:
|
|
4505
|
+
# CS_STOPPED 1 when control_branch_stopped answers 0 for CS_STATE, else 0;
|
|
4506
|
+
# CS_WORD `stopped` then, else CS_STATE. Every reply names the state by
|
|
4507
|
+
# CS_WORD; every test of which states a verb accepts reads CS_STATE.
|
|
4508
|
+
CS_STOPPED=0; CS_WORD=""
|
|
4509
|
+
control_state_word_var() {
|
|
4510
|
+
CS_STOPPED=0
|
|
4511
|
+
CS_WORD="$CS_STATE"
|
|
4512
|
+
if control_branch_stopped "$CS_STATE"; then
|
|
4513
|
+
CS_STOPPED=1
|
|
4514
|
+
CS_WORD=stopped
|
|
4515
|
+
fi
|
|
4516
|
+
return 0
|
|
4517
|
+
}
|
|
4518
|
+
|
|
4519
|
+
# control_stopped_refuse — the refusal of a verb on a stopped run (CS_STOPPED
|
|
4520
|
+
# 1): names it `stopped`, the state underneath where the way on depends on it,
|
|
4521
|
+
# and the way on that state takes.
|
|
4522
|
+
control_stopped_refuse() {
|
|
4523
|
+
local open=""
|
|
4524
|
+
case "$CS_STATE" in
|
|
4525
|
+
parked)
|
|
4526
|
+
[ -z "$CS_OPEN" ] || open=", open: $CS_OPEN"
|
|
4527
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\` (it was parked, waiting for an answer$open)" \
|
|
4528
|
+
"Comment \`$COMMAND_HANDLE answer <n>\` with the answer to question <n> on the lines below it; the answer resumes it." ;;
|
|
4529
|
+
park_loop)
|
|
4530
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\` (it was held by the park-loop guard)" \
|
|
4531
|
+
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
4532
|
+
running)
|
|
4533
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`; its cancelled job is still finishing" \
|
|
4534
|
+
"Comment \`$COMMAND_HANDLE resume\` once it has ended." ;;
|
|
4535
|
+
*)
|
|
4536
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`" \
|
|
4537
|
+
"Comment \`$COMMAND_HANDLE resume\` to continue it from its committed ledger." ;;
|
|
4538
|
+
esac
|
|
4539
|
+
}
|
|
4540
|
+
|
|
4259
4541
|
# control_check_branch <branch> — the refusals both paths share, in order: a
|
|
4260
4542
|
# branch not answered 1 by hr_branch_is_protected, then (after a fetch) one
|
|
4261
4543
|
# whose origin tip carries no flow-progress ledger. Each is a reply and exit 2.
|
|
@@ -4302,20 +4584,28 @@ control_branch_from_pr() {
|
|
|
4302
4584
|
control_check_branch "$head"
|
|
4303
4585
|
}
|
|
4304
4586
|
|
|
4305
|
-
#
|
|
4306
|
-
# start comment: by `github-actions[bot]`, its first line opening
|
|
4307
|
-
# trigger's start sentence and a backticked <b>, and its last non-empty
|
|
4308
|
-
# exactly `forge_marker started <b>`. A marker anywhere else is never
|
|
4309
|
-
|
|
4310
|
-
|
|
4587
|
+
# control_issue_branch_var <number> — ISSUE_BRANCH, the branch of the issue's
|
|
4588
|
+
# last genuine start comment: by `github-actions[bot]`, its first line opening
|
|
4589
|
+
# with the trigger's start sentence and a backticked <b>, and its last non-empty
|
|
4590
|
+
# line exactly `forge_marker started <b>`. A marker anywhere else is never
|
|
4591
|
+
# trusted. 1 when there is none, ISSUE_BRANCH_ERR then non-empty when the
|
|
4592
|
+
# comments could not be read. Posts nothing.
|
|
4593
|
+
ISSUE_BRANCH=""
|
|
4594
|
+
ISSUE_BRANCH_ERR=""
|
|
4595
|
+
control_issue_branch_var() {
|
|
4596
|
+
local count i login body last b found="" lead='Started a harness run on the branch `'
|
|
4597
|
+
ISSUE_BRANCH=""
|
|
4598
|
+
ISSUE_BRANCH_ERR=""
|
|
4311
4599
|
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$1/comments" --jq '.[] | {login: .user.login, body: .body}'; then
|
|
4312
|
-
|
|
4600
|
+
ISSUE_BRANCH_ERR="$GH_ERR"
|
|
4601
|
+
return 1
|
|
4313
4602
|
fi
|
|
4314
4603
|
count=$(printf '%s' "$GH_OUT" | jq -s 'length' 2>/dev/null) || count=""
|
|
4315
4604
|
case "$count" in
|
|
4316
4605
|
''|*[!0-9]*)
|
|
4317
4606
|
GH_ERR="its comment list is not the expected JSON"
|
|
4318
|
-
|
|
4607
|
+
ISSUE_BRANCH_ERR="$GH_ERR"
|
|
4608
|
+
return 1 ;;
|
|
4319
4609
|
esac
|
|
4320
4610
|
for ((i = 0; i < count; i++)); do
|
|
4321
4611
|
login=$(printf '%s' "$GH_OUT" | jq -s -r --argjson i "$i" '.[$i].login // ""' 2>/dev/null) || continue
|
|
@@ -4338,17 +4628,27 @@ control_branch_from_issue() {
|
|
|
4338
4628
|
[ "$last" = "$(forge_marker started "$b")" ] || continue
|
|
4339
4629
|
found="$b"
|
|
4340
4630
|
done
|
|
4341
|
-
|
|
4631
|
+
[ -n "$found" ] || return 1
|
|
4632
|
+
ISSUE_BRANCH="$found"
|
|
4633
|
+
}
|
|
4634
|
+
|
|
4635
|
+
# control_branch_from_issue <number> — control_issue_branch_var, its failures
|
|
4636
|
+
# refused, then control_check_branch's refusals.
|
|
4637
|
+
control_branch_from_issue() {
|
|
4638
|
+
if ! control_issue_branch_var "$1"; then
|
|
4639
|
+
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
4640
|
+
control_refuse "$EXIT_GH" "the comments of issue #$1 could not be read ($ISSUE_BRANCH_ERR)" "Comment again to retry."
|
|
4641
|
+
fi
|
|
4342
4642
|
control_refuse "$EXIT_REFUSED" "no harness run was started from this issue" \
|
|
4343
4643
|
"Comment on the pull request of the run's branch instead."
|
|
4344
4644
|
fi
|
|
4345
|
-
control_check_branch "$
|
|
4645
|
+
control_check_branch "$ISSUE_BRANCH"
|
|
4346
4646
|
}
|
|
4347
4647
|
|
|
4348
4648
|
# control_verb_handled <verb> — 0 when an arm below carries out <verb>.
|
|
4349
4649
|
control_verb_handled() {
|
|
4350
4650
|
case "$1" in
|
|
4351
|
-
answer|pause|stop|resume|clear) return 0 ;;
|
|
4651
|
+
answer|pause|stop|resume|clear|status) return 0 ;;
|
|
4352
4652
|
esac
|
|
4353
4653
|
return 1
|
|
4354
4654
|
}
|
|
@@ -4365,7 +4665,7 @@ control_resume_dispatch() {
|
|
|
4365
4665
|
route=$(hr_github_resume_route "$CONTROL_BRANCH" "<task, user_review or docs: the one the run was started with>")
|
|
4366
4666
|
route=${route#or from GitHub: }
|
|
4367
4667
|
[ "$#" -eq 0 ] || clear=", with park_loop_clear true as well"
|
|
4368
|
-
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (\`$
|
|
4668
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (\`$CS_WORD\`${CS_REASON:+, \`$CS_REASON\`}) records no engine, and the harness does not guess one" \
|
|
4369
4669
|
"Resume it with the **Run workflow** form instead: $route$clear."
|
|
4370
4670
|
fi
|
|
4371
4671
|
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
@@ -4390,11 +4690,15 @@ control_resume() {
|
|
|
4390
4690
|
local open
|
|
4391
4691
|
control_state_var "$CONTROL_BRANCH" \
|
|
4392
4692
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4693
|
+
control_state_word_var
|
|
4393
4694
|
case "$CS_STATE" in
|
|
4394
4695
|
paused)
|
|
4395
4696
|
# Every pause reason, `expired` and `killed` included, resumes from the
|
|
4396
4697
|
# committed ledger, as the local route does.
|
|
4397
4698
|
control_resume_dispatch "Resume requested by @$CONTROL_ACTOR: \`$CONTROL_BRANCH\` continues from its committed ledger." ;;
|
|
4699
|
+
esac
|
|
4700
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4701
|
+
case "$CS_STATE" in
|
|
4398
4702
|
park_loop)
|
|
4399
4703
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard" \
|
|
4400
4704
|
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
@@ -4406,20 +4710,24 @@ control_resume() {
|
|
|
4406
4710
|
running)
|
|
4407
4711
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is already \`running\`" "Nothing needs resuming." ;;
|
|
4408
4712
|
*)
|
|
4409
|
-
control_refuse "$EXIT_REFUSED" "only a paused run can be resumed, and the run on \`$CONTROL_BRANCH\` is \`${
|
|
4713
|
+
control_refuse "$EXIT_REFUSED" "only a paused run can be resumed, and the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4410
4714
|
"A finished run continues by a review requesting changes on its pull request, or by applying the trigger label to its issue again." ;;
|
|
4411
4715
|
esac
|
|
4412
4716
|
}
|
|
4413
4717
|
|
|
4414
4718
|
control_clear() {
|
|
4719
|
+
local subject="the run"
|
|
4415
4720
|
control_state_var "$CONTROL_BRANCH" \
|
|
4416
4721
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4722
|
+
control_state_word_var
|
|
4417
4723
|
if [ "$CS_STATE" != park_loop ]; then
|
|
4418
|
-
|
|
4724
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4725
|
+
control_refuse "$EXIT_REFUSED" "there is no park-loop hold to clear: the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4419
4726
|
"Only a run held by the park-loop guard is cleared."
|
|
4420
4727
|
fi
|
|
4728
|
+
[ "$CS_STOPPED" != 1 ] || subject="the stopped run"
|
|
4421
4729
|
# On GitHub, typing `clear` is the confirmation `branch-resume` asks for.
|
|
4422
|
-
control_resume_dispatch "Park-loop hold on \`$CONTROL_BRANCH\` cleared by @$CONTROL_ACTOR;
|
|
4730
|
+
control_resume_dispatch "Park-loop hold on \`$CONTROL_BRANCH\` cleared by @$CONTROL_ACTOR; $subject resumes from its committed ledger." \
|
|
4423
4731
|
--park-loop-clear
|
|
4424
4732
|
}
|
|
4425
4733
|
|
|
@@ -4427,8 +4735,11 @@ control_pause() {
|
|
|
4427
4735
|
local out
|
|
4428
4736
|
control_state_var "$CONTROL_BRANCH" \
|
|
4429
4737
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4738
|
+
control_state_word_var
|
|
4739
|
+
# A stopped `running` run is a cancel still finishing: nothing to pause.
|
|
4740
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4430
4741
|
if [ "$CS_STATE" != running ]; then
|
|
4431
|
-
control_refuse "$EXIT_REFUSED" "only a running run can be paused, and the run on \`$CONTROL_BRANCH\` is \`$
|
|
4742
|
+
control_refuse "$EXIT_REFUSED" "only a running run can be paused, and the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`" \
|
|
4432
4743
|
"Nothing needs pausing."
|
|
4433
4744
|
fi
|
|
4434
4745
|
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
@@ -4463,12 +4774,78 @@ control_stop() {
|
|
|
4463
4774
|
esac
|
|
4464
4775
|
}
|
|
4465
4776
|
|
|
4777
|
+
# control_ledger_next_var — LEDGER_NEXT, the first `- [ ] ` line of
|
|
4778
|
+
# CONTROL_BRANCH's flow-progress ledger on origin's tip without that prefix,
|
|
4779
|
+
# and LEDGER_SECTION, the last `## ` heading above it; both empty when every
|
|
4780
|
+
# entry is ticked. 1 when the ledger cannot be read.
|
|
4781
|
+
LEDGER_NEXT=""; LEDGER_SECTION=""
|
|
4782
|
+
control_ledger_next_var() {
|
|
4783
|
+
local state_rel text line section=""
|
|
4784
|
+
LEDGER_NEXT=""; LEDGER_SECTION=""
|
|
4785
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
4786
|
+
[ -n "$state_rel" ] || return 1
|
|
4787
|
+
text=$(git -C "$root" show "refs/remotes/origin/$CONTROL_BRANCH:${state_rel%/}/flow_progress/${CONTROL_BRANCH}_progress.md" 2>/dev/null) \
|
|
4788
|
+
|| return 1
|
|
4789
|
+
while IFS= read -r line; do
|
|
4790
|
+
line=${line%$'\r'}
|
|
4791
|
+
case "$line" in
|
|
4792
|
+
'## '*) section=${line#'## '} ;;
|
|
4793
|
+
'- [ ] '*)
|
|
4794
|
+
LEDGER_NEXT=${line#'- [ ] '}
|
|
4795
|
+
LEDGER_SECTION="$section"
|
|
4796
|
+
return 0 ;;
|
|
4797
|
+
esac
|
|
4798
|
+
done <<<"$text"
|
|
4799
|
+
return 0
|
|
4800
|
+
}
|
|
4801
|
+
|
|
4802
|
+
# control_status — the read-only reply: the state, the next ledger entry, the
|
|
4803
|
+
# open questions and the latest run. Sets no label and dispatches nothing.
|
|
4804
|
+
control_status() {
|
|
4805
|
+
local text v questions="" para=$'\n\n'
|
|
4806
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4807
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4808
|
+
control_state_word_var
|
|
4809
|
+
if [ "$CS_STATE" = none ]; then
|
|
4810
|
+
text="@$CONTROL_ACTOR: no harness run is listed for \`$CONTROL_BRANCH\`."
|
|
4811
|
+
else
|
|
4812
|
+
text="@$CONTROL_ACTOR: \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`${CS_REASON:+ (\`$CS_REASON\`)}"
|
|
4813
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
4814
|
+
case "$CS_STATE" in
|
|
4815
|
+
parked) text="$text; it was parked, waiting for an answer" ;;
|
|
4816
|
+
park_loop) text="$text; it was held by the park-loop guard" ;;
|
|
4817
|
+
running) text="$text; its cancelled job is still finishing" ;;
|
|
4818
|
+
esac
|
|
4819
|
+
fi
|
|
4820
|
+
# An expired bundle is reported as expired, never as absent.
|
|
4821
|
+
[ "$CS_REASON" != expired ] || text="$text: ${CS_DETAIL:-the state bundle of the run has expired}"
|
|
4822
|
+
text="$text."
|
|
4823
|
+
fi
|
|
4824
|
+
if control_ledger_next_var; then
|
|
4825
|
+
if [ -n "$LEDGER_NEXT" ]; then
|
|
4826
|
+
text="$text${para}Next in the flow-progress ledger: $LEDGER_NEXT${LEDGER_SECTION:+ (under \`$LEDGER_SECTION\`)}"
|
|
4827
|
+
else
|
|
4828
|
+
text="$text${para}Every entry of the flow-progress ledger is ticked."
|
|
4829
|
+
fi
|
|
4830
|
+
else
|
|
4831
|
+
text="$text${para}The flow-progress ledger could not be read, so its next entry is left out."
|
|
4832
|
+
fi
|
|
4833
|
+
if [ -n "$CS_OPEN" ]; then
|
|
4834
|
+
for v in $CS_OPEN; do
|
|
4835
|
+
questions="$questions${questions:+, }$v (\`$COMMAND_HANDLE answer $v\`)"
|
|
4836
|
+
done
|
|
4837
|
+
text="$text${para}Open questions: $questions, each answered by its own comment with the answer on the lines below the command."
|
|
4838
|
+
fi
|
|
4839
|
+
[ -z "$CS_URL" ] || text="$text${para}Latest run: $CS_URL"
|
|
4840
|
+
control_reply "$EXIT_OK" "$text"
|
|
4841
|
+
}
|
|
4842
|
+
|
|
4466
4843
|
# control_answer — one answer, one `resume: answer` dispatch with one entry. A
|
|
4467
4844
|
# park left partly answered is safe: run_job stops an `answer` job whose park
|
|
4468
4845
|
# is not fully answered before any session, and its bundle then carries the
|
|
4469
4846
|
# `answer_<n>.md` restore wrote, so the next answer's job finds the set complete.
|
|
4470
4847
|
control_answer() {
|
|
4471
|
-
local first short="" below="" text n="" v open_list="" rest="" cmds="" route form dir out status="$EXIT_OK"
|
|
4848
|
+
local first short="" below="" text n="" v open_list="" rest="" cmds="" route form dir out status="$EXIT_OK" named
|
|
4472
4849
|
first=${CONTROL_ARGS%%[$' \t']*}
|
|
4473
4850
|
if [[ "$first" =~ ^[1-9][0-9]*$ ]]; then
|
|
4474
4851
|
n="$first"
|
|
@@ -4494,6 +4871,14 @@ control_answer() {
|
|
|
4494
4871
|
|
|
4495
4872
|
control_state_var "$CONTROL_BRANCH" \
|
|
4496
4873
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4874
|
+
control_state_word_var
|
|
4875
|
+
# On a stopped parked run the answer dispatch is its resume; any other
|
|
4876
|
+
# stopped state is refused naming `stopped`.
|
|
4877
|
+
if [ "$CS_STOPPED" = 1 ] && [ "$CS_STATE" != parked ]; then
|
|
4878
|
+
control_stopped_refuse
|
|
4879
|
+
fi
|
|
4880
|
+
named="\`$CS_WORD\`"
|
|
4881
|
+
[ "$CS_STOPPED" != 1 ] || named="$named (it was parked)"
|
|
4497
4882
|
case "$CS_STATE:$CS_REASON" in
|
|
4498
4883
|
park_loop:*)
|
|
4499
4884
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard, not waiting for an answer" \
|
|
@@ -4510,11 +4895,11 @@ control_answer() {
|
|
|
4510
4895
|
;;
|
|
4511
4896
|
parked:*) ;;
|
|
4512
4897
|
*)
|
|
4513
|
-
control_refuse "$EXIT_REFUSED" "only a parked run can be answered, and the run on \`$CONTROL_BRANCH\` is \`${
|
|
4898
|
+
control_refuse "$EXIT_REFUSED" "only a parked run can be answered, and the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4514
4899
|
"Nothing is waiting for an answer." ;;
|
|
4515
4900
|
esac
|
|
4516
4901
|
if [ -z "$CS_OPEN" ]; then
|
|
4517
|
-
control_refuse "$EXIT_REFUSED" "only a parked run with an open question can be answered, and the run on \`$CONTROL_BRANCH\` is
|
|
4902
|
+
control_refuse "$EXIT_REFUSED" "only a parked run with an open question can be answered, and the run on \`$CONTROL_BRANCH\` is $named with none open" \
|
|
4518
4903
|
"Nothing is waiting for an answer."
|
|
4519
4904
|
fi
|
|
4520
4905
|
for v in $CS_OPEN; do
|
|
@@ -4540,7 +4925,7 @@ control_answer() {
|
|
|
4540
4925
|
route=${route#or from GitHub: }
|
|
4541
4926
|
form="resume answer and answers \`{\"$n\": \"<the answer>\"}\`"
|
|
4542
4927
|
route=${route/and resume pause/$form}
|
|
4543
|
-
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (
|
|
4928
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` ($named) records no engine, and the harness does not guess one" \
|
|
4544
4929
|
"Answer it with the **Run workflow** form instead: $route."
|
|
4545
4930
|
fi
|
|
4546
4931
|
|
|
@@ -4579,10 +4964,18 @@ control_answer() {
|
|
|
4579
4964
|
done
|
|
4580
4965
|
if [ -n "$rest" ]; then
|
|
4581
4966
|
# The label stays `parked` until the last answer: only that job resumes.
|
|
4967
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
4968
|
+
control_reply "$EXIT_OK" "Answer to question $n received from @$CONTROL_ACTOR and sent; the stopped run on \`$CONTROL_BRANCH\` resumes once question(s) $rest are answered: $cmds."
|
|
4969
|
+
fi
|
|
4582
4970
|
control_reply "$EXIT_OK" "Answer to question $n received from @$CONTROL_ACTOR and sent; question(s) $rest still need an answer: $cmds."
|
|
4583
4971
|
fi
|
|
4584
|
-
|
|
4585
|
-
|
|
4972
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
4973
|
+
control_post "Answer to question $n received from @$CONTROL_ACTOR; every open question is answered, so the stopped run on \`$CONTROL_BRANCH\` resumes." \
|
|
4974
|
+
|| status="$EXIT_GH"
|
|
4975
|
+
else
|
|
4976
|
+
control_post "Answer to question $n received from @$CONTROL_ACTOR; every open question is answered, so \`$CONTROL_BRANCH\` resumes." \
|
|
4977
|
+
|| status="$EXIT_GH"
|
|
4978
|
+
fi
|
|
4586
4979
|
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
4587
4980
|
forge_pr_var "$CONTROL_BRANCH" || FORGE_PR=""
|
|
4588
4981
|
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" running || :
|
|
@@ -4884,18 +5277,33 @@ control_review_in_flight() {
|
|
|
4884
5277
|
state="\`$BS_STATE\`"
|
|
4885
5278
|
[ -z "$BS_REASON" ] || state="$state (\`$BS_REASON\`)"
|
|
4886
5279
|
fi
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
way=" The run
|
|
4895
|
-
|
|
4896
|
-
way="
|
|
4897
|
-
|
|
4898
|
-
|
|
5280
|
+
if control_branch_stopped "$BS_STATE"; then
|
|
5281
|
+
# A stopped run continues only by the command its underlying state takes.
|
|
5282
|
+
state="\`stopped\`"
|
|
5283
|
+
case "$BS_STATE" in
|
|
5284
|
+
parked)
|
|
5285
|
+
way=" The run was stopped while waiting for an answer: comment \`$COMMAND_HANDLE answer <n>\` with the answer to its open question <n> on the lines below it; the answer resumes it." ;;
|
|
5286
|
+
park_loop)
|
|
5287
|
+
way=" The run was stopped while held by the park-loop guard: comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
5288
|
+
running)
|
|
5289
|
+
way=" Its cancelled job is still finishing: comment \`$COMMAND_HANDLE resume\` once it has ended." ;;
|
|
5290
|
+
*)
|
|
5291
|
+
way=" Comment \`$COMMAND_HANDLE resume\` to resume the run from its committed ledger." ;;
|
|
5292
|
+
esac
|
|
5293
|
+
else
|
|
5294
|
+
case "$BS_STATE" in
|
|
5295
|
+
parked)
|
|
5296
|
+
way=" The run waits for an answer: comment \`$COMMAND_HANDLE answer <n>\` with the answer to its open question <n> on the lines below it." ;;
|
|
5297
|
+
park_loop)
|
|
5298
|
+
way=" The run is held by the park-loop guard: comment \`$COMMAND_HANDLE clear\` to release the hold." ;;
|
|
5299
|
+
paused)
|
|
5300
|
+
if [ "$BS_REASON" = usage ]; then
|
|
5301
|
+
way=" The run resumes by itself once the usage limit resets."
|
|
5302
|
+
else
|
|
5303
|
+
way=" Comment \`$COMMAND_HANDLE resume\` to resume the run from its committed ledger."
|
|
5304
|
+
fi ;;
|
|
5305
|
+
esac
|
|
5306
|
+
fi
|
|
4899
5307
|
branch="$CONTROL_BRANCH"
|
|
4900
5308
|
if round_markers_read "$REVIEW_ID" && [ -n "$RC_EVENT_ROUND" ]; then
|
|
4901
5309
|
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: your review is part of round $RC_EVENT_ROUND, which is $state on \`$CONTROL_BRANCH\`."
|
|
@@ -5054,14 +5462,193 @@ control_comment_intake() {
|
|
|
5054
5462
|
return 0
|
|
5055
5463
|
}
|
|
5056
5464
|
|
|
5465
|
+
# The close: CLOSE_KIND (`issue`, `pr_closed`, `pr_merged`, `deleted`) and
|
|
5466
|
+
# CLOSE_REF, the event's own branch (a pull request's head, a deleted ref;
|
|
5467
|
+
# empty for an issue). CONTROL_NUMBER is the item, empty for a deletion.
|
|
5468
|
+
CLOSE_KIND=""
|
|
5469
|
+
CLOSE_REF=""
|
|
5470
|
+
|
|
5471
|
+
control_event_unreadable() {
|
|
5472
|
+
echo "remote-run.sh: control: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
5473
|
+
exit "$EXIT_USAGE"
|
|
5474
|
+
}
|
|
5475
|
+
|
|
5476
|
+
# control_issues_intake — read an `issues` event; 1 when ignored, after one line.
|
|
5477
|
+
control_issues_intake() {
|
|
5478
|
+
local action
|
|
5479
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5480
|
+
&& event_field '.issue.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
5481
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5482
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5483
|
+
if [ "$action" != closed ]; then
|
|
5484
|
+
echo "remote-run.sh: control: ignored, an issue $action, not closed"
|
|
5485
|
+
return 1
|
|
5486
|
+
fi
|
|
5487
|
+
CLOSE_KIND=issue
|
|
5488
|
+
CONTROL_VERB=close
|
|
5489
|
+
return 0
|
|
5490
|
+
}
|
|
5491
|
+
|
|
5492
|
+
# control_pull_request_intake — read a `pull_request` event; 1 when ignored,
|
|
5493
|
+
# after one line.
|
|
5494
|
+
control_pull_request_intake() {
|
|
5495
|
+
local action merged head_repo
|
|
5496
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5497
|
+
&& event_field '.pull_request.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
5498
|
+
&& event_field '.pull_request.merged // false' && merged="$EVENT_VALUE" \
|
|
5499
|
+
&& event_field '.pull_request.head.ref // ""' && CLOSE_REF="$EVENT_VALUE" \
|
|
5500
|
+
&& event_field '.pull_request.head.repo.full_name // ""' && head_repo="$EVENT_VALUE" \
|
|
5501
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5502
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5503
|
+
if [ "$action" != closed ]; then
|
|
5504
|
+
echo "remote-run.sh: control: ignored, a pull request $action, not closed"
|
|
5505
|
+
return 1
|
|
5506
|
+
fi
|
|
5507
|
+
if [ -z "$head_repo" ] || [ "$head_repo" != "${GITHUB_REPOSITORY-}" ]; then
|
|
5508
|
+
echo "remote-run.sh: control: ignored, a pull request from ${head_repo:-an unnamed repository}, not ${GITHUB_REPOSITORY:-this repository}"
|
|
5509
|
+
return 1
|
|
5510
|
+
fi
|
|
5511
|
+
CLOSE_KIND=pr_closed
|
|
5512
|
+
[ "$merged" != true ] || CLOSE_KIND=pr_merged
|
|
5513
|
+
CONTROL_VERB=close
|
|
5514
|
+
return 0
|
|
5515
|
+
}
|
|
5516
|
+
|
|
5517
|
+
# control_delete_intake — read a `delete` event; 1 when ignored, after one line.
|
|
5518
|
+
control_delete_intake() {
|
|
5519
|
+
local ref_type
|
|
5520
|
+
{ event_field '.ref // ""' && CLOSE_REF="$EVENT_VALUE" \
|
|
5521
|
+
&& event_field '.ref_type // ""' && ref_type="$EVENT_VALUE" \
|
|
5522
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5523
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5524
|
+
if [ "$ref_type" != branch ]; then
|
|
5525
|
+
echo "remote-run.sh: control: ignored, a deleted ${ref_type:-ref}, not a branch"
|
|
5526
|
+
return 1
|
|
5527
|
+
fi
|
|
5528
|
+
if ! valid_branch "$CLOSE_REF"; then
|
|
5529
|
+
echo "remote-run.sh: control: ignored, a deleted branch with no valid name"
|
|
5530
|
+
return 1
|
|
5531
|
+
fi
|
|
5532
|
+
CLOSE_KIND=deleted
|
|
5533
|
+
CONTROL_VERB=close
|
|
5534
|
+
return 0
|
|
5535
|
+
}
|
|
5536
|
+
|
|
5537
|
+
# control_close_ignore <reason> — the one line every close refusal is, and exit
|
|
5538
|
+
# 0: a close is never replied to, labelled or dispatched for.
|
|
5539
|
+
control_close_ignore() {
|
|
5540
|
+
echo "remote-run.sh: control: close ignored: $1"
|
|
5541
|
+
exit "$EXIT_OK"
|
|
5542
|
+
}
|
|
5543
|
+
|
|
5544
|
+
# control_close — THE CLOSE (the header): the quiet gates in order, the state
|
|
5545
|
+
# rule, then the `stop` child with the event's note.
|
|
5546
|
+
control_close() {
|
|
5547
|
+
local status=0 protected=0 b="" what note out
|
|
5548
|
+
local stop_flags=()
|
|
5549
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5550
|
+
control_close_ignore "the repository variable HARNESS_REMOTE_STOP is set"
|
|
5551
|
+
fi
|
|
5552
|
+
forge_on || control_close_ignore "the default branch's harness.config.json does not turn run control on"
|
|
5553
|
+
if [ "$CLOSE_KIND" = deleted ]; then
|
|
5554
|
+
# Deleting a branch already needs write access; only a bot is screened.
|
|
5555
|
+
if [ "$CONTROL_SENDER_TYPE" = Bot ] && ! trigger_bot_listed "$CONTROL_ACTOR"; then
|
|
5556
|
+
control_close_ignore "@$CONTROL_ACTOR is a bot not listed in HARNESS_TRIGGER_ALLOWED_BOTS"
|
|
5557
|
+
fi
|
|
5558
|
+
else
|
|
5559
|
+
case "$CONTROL_NUMBER" in
|
|
5560
|
+
''|*[!0-9]*|0*)
|
|
5561
|
+
echo "remote-run.sh: control: the event carries no issue number" >&2
|
|
5562
|
+
exit "$EXIT_USAGE" ;;
|
|
5563
|
+
esac
|
|
5564
|
+
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
|
|
5565
|
+
case "$status" in
|
|
5566
|
+
0) ;;
|
|
5567
|
+
4)
|
|
5568
|
+
echo "::error::remote-run.sh: control: the close by @$CONTROL_ACTOR was not acted on: ${AUTH_WHY%.}"
|
|
5569
|
+
exit "$EXIT_GH" ;;
|
|
5570
|
+
*) control_close_ignore "@$CONTROL_ACTOR is not authorised: ${AUTH_WHY%.}" ;;
|
|
5571
|
+
esac
|
|
5572
|
+
fi
|
|
5573
|
+
|
|
5574
|
+
case "$CLOSE_KIND" in
|
|
5575
|
+
issue)
|
|
5576
|
+
if ! forge_repo_var; then
|
|
5577
|
+
echo "::error::remote-run.sh: control: the repository's name could not be read ($GH_ERR)"
|
|
5578
|
+
exit "$EXIT_GH"
|
|
5579
|
+
fi
|
|
5580
|
+
if ! control_issue_branch_var "$CONTROL_NUMBER"; then
|
|
5581
|
+
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
5582
|
+
echo "::error::remote-run.sh: control: the comments of issue #$CONTROL_NUMBER could not be read ($ISSUE_BRANCH_ERR)"
|
|
5583
|
+
exit "$EXIT_GH"
|
|
5584
|
+
fi
|
|
5585
|
+
control_close_ignore "no harness run was started from issue #$CONTROL_NUMBER"
|
|
5586
|
+
fi
|
|
5587
|
+
b="$ISSUE_BRANCH" ;;
|
|
5588
|
+
*) b="$CLOSE_REF" ;;
|
|
5589
|
+
esac
|
|
5590
|
+
if ! valid_branch "$b" || ! git check-ref-format --branch "$b" >/dev/null 2>&1; then
|
|
5591
|
+
control_close_ignore "\`$b\` is not a valid branch name"
|
|
5592
|
+
fi
|
|
5593
|
+
hr_branch_is_protected "$root" "$b" || protected=$?
|
|
5594
|
+
case "$protected" in
|
|
5595
|
+
1) ;;
|
|
5596
|
+
0) control_close_ignore "\`$b\` is a protected branch" ;;
|
|
5597
|
+
*) control_close_ignore "whether \`$b\` is protected could not be judged from harness.config.json" ;;
|
|
5598
|
+
esac
|
|
5599
|
+
CONTROL_BRANCH="$b"
|
|
5600
|
+
if [ "$CLOSE_KIND" = pr_closed ] || [ "$CLOSE_KIND" = pr_merged ]; then
|
|
5601
|
+
# GitHub closes a pull request whose head is deleted; the `delete` event's
|
|
5602
|
+
# own job stops that run from the default branch, so this one stays quiet.
|
|
5603
|
+
remote_branch_exists "$b"
|
|
5604
|
+
case $? in
|
|
5605
|
+
1) control_close_ignore "the branch \`$b\` of pull request #$CONTROL_NUMBER is gone from origin; the deletion's own job stops the run" ;;
|
|
5606
|
+
2) echo "remote-run.sh: control: whether \`$b\` exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
5607
|
+
esac
|
|
5608
|
+
fi
|
|
5609
|
+
|
|
5610
|
+
# No forge_recognised check: a merged or deleted branch may no longer carry
|
|
5611
|
+
# its ledger, and a listed `harness run <b>` run is what proves a harness run.
|
|
5612
|
+
if ! control_state_var "$b"; then
|
|
5613
|
+
echo "::error::remote-run.sh: control: the state of the run on \`$b\` could not be read ($CS_ERR)"
|
|
5614
|
+
exit "$EXIT_GH"
|
|
5615
|
+
fi
|
|
5616
|
+
case "$CS_STATE" in
|
|
5617
|
+
running|parked|park_loop|paused) ;;
|
|
5618
|
+
*) control_close_ignore "left alone: the run on \`$b\` is \`${CS_STATE:-unknown}\`" ;;
|
|
5619
|
+
esac
|
|
5620
|
+
control_branch_stopped "$CS_STATE" && control_close_ignore "the run on \`$b\` is already stopped"
|
|
5621
|
+
|
|
5622
|
+
case "$CLOSE_KIND" in
|
|
5623
|
+
issue) what="closed issue #$CONTROL_NUMBER" ;;
|
|
5624
|
+
pr_closed) what="closed pull request #$CONTROL_NUMBER"; stop_flags=(--pr "$CONTROL_NUMBER") ;;
|
|
5625
|
+
pr_merged) what="merged pull request #$CONTROL_NUMBER"; stop_flags=(--pr "$CONTROL_NUMBER") ;;
|
|
5626
|
+
deleted) what="deleted the branch \`$b\`"; stop_flags=(--branch-gone) ;;
|
|
5627
|
+
esac
|
|
5628
|
+
note="Stopped because @$CONTROL_ACTOR $what."
|
|
5629
|
+
if [ "$CLOSE_KIND" != deleted ]; then
|
|
5630
|
+
note="$note Its workflow runs and their \`$STATE_ARTIFACT_NAME\` artifacts are kept."
|
|
5631
|
+
fi
|
|
5632
|
+
echo "remote-run.sh: control: close of $b: @$CONTROL_ACTOR $what"
|
|
5633
|
+
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
5634
|
+
control_child "$out" stop "$b" --actor "$CONTROL_ACTOR" --note "$note" ${stop_flags[@]+"${stop_flags[@]}"} --repo "$root"
|
|
5635
|
+
[ "$out" = /dev/null ] || { cat "$out"; rm -f "$out"; }
|
|
5636
|
+
if [ "$CHILD_STATUS" -ne 0 ]; then
|
|
5637
|
+
echo "::error::remote-run.sh: control: the stop of \`$b\` after @$CONTROL_ACTOR $what failed: $CHILD_LAST"
|
|
5638
|
+
exit "$EXIT_GH"
|
|
5639
|
+
fi
|
|
5640
|
+
exit "$EXIT_OK"
|
|
5641
|
+
}
|
|
5642
|
+
|
|
5057
5643
|
verb_control() {
|
|
5058
5644
|
local LC_ALL=C
|
|
5059
|
-
local review=0 forge="" target="" status verbs="" v
|
|
5645
|
+
local review=0 close=0 forge="" target="" status verbs="" v
|
|
5060
5646
|
case "${GITHUB_EVENT_NAME-}" in
|
|
5061
5647
|
issue_comment) ;;
|
|
5062
5648
|
pull_request_review) review=1 ;;
|
|
5649
|
+
issues|pull_request|delete) close=1 ;;
|
|
5063
5650
|
*)
|
|
5064
|
-
echo "remote-run.sh: control handles GITHUB_EVENT_NAME issue_comment or
|
|
5651
|
+
echo "remote-run.sh: control handles GITHUB_EVENT_NAME issue_comment, pull_request_review, issues, pull_request or delete, not '${GITHUB_EVENT_NAME-}'" >&2
|
|
5065
5652
|
exit "$EXIT_USAGE" ;;
|
|
5066
5653
|
esac
|
|
5067
5654
|
if [ -z "${GITHUB_EVENT_PATH-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ] || [ ! -r "$GITHUB_EVENT_PATH" ]; then
|
|
@@ -5070,18 +5657,22 @@ verb_control() {
|
|
|
5070
5657
|
fi
|
|
5071
5658
|
hr_have_jq || { echo "remote-run.sh: control needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
5072
5659
|
|
|
5073
|
-
|
|
5074
|
-
control_review_intake || return 0
|
|
5075
|
-
|
|
5076
|
-
|
|
5077
|
-
|
|
5078
|
-
|
|
5079
|
-
case "$CONTROL_NUMBER" in
|
|
5080
|
-
''|*[!0-9]*|0*)
|
|
5081
|
-
echo "remote-run.sh: control: the event carries no issue number" >&2
|
|
5082
|
-
exit "$EXIT_USAGE" ;;
|
|
5660
|
+
case "$GITHUB_EVENT_NAME" in
|
|
5661
|
+
pull_request_review) control_review_intake || return 0 ;;
|
|
5662
|
+
issue_comment) control_comment_intake || return 0 ;;
|
|
5663
|
+
issues) control_issues_intake || return 0 ;;
|
|
5664
|
+
pull_request) control_pull_request_intake || return 0 ;;
|
|
5665
|
+
delete) control_delete_intake || return 0 ;;
|
|
5083
5666
|
esac
|
|
5084
5667
|
|
|
5668
|
+
if [ "$close" -eq 0 ]; then
|
|
5669
|
+
case "$CONTROL_NUMBER" in
|
|
5670
|
+
''|*[!0-9]*|0*)
|
|
5671
|
+
echo "remote-run.sh: control: the event carries no issue number" >&2
|
|
5672
|
+
exit "$EXIT_USAGE" ;;
|
|
5673
|
+
esac
|
|
5674
|
+
fi
|
|
5675
|
+
|
|
5085
5676
|
control_tmp="${RUNNER_TEMP-}"
|
|
5086
5677
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5087
5678
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: control: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
@@ -5089,6 +5680,9 @@ verb_control() {
|
|
|
5089
5680
|
fi
|
|
5090
5681
|
trap control_cleanup EXIT
|
|
5091
5682
|
|
|
5683
|
+
# Decided by the event name, never CONTROL_VERB: a comment can name `close`.
|
|
5684
|
+
[ "$close" -eq 0 ] || control_close
|
|
5685
|
+
|
|
5092
5686
|
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5093
5687
|
control_refuse "$EXIT_REFUSED" "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command" \
|
|
5094
5688
|
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
@@ -5133,6 +5727,7 @@ verb_control() {
|
|
|
5133
5727
|
stop) control_stop ;;
|
|
5134
5728
|
resume) control_resume ;;
|
|
5135
5729
|
clear) control_clear ;;
|
|
5730
|
+
status) control_status ;;
|
|
5136
5731
|
review) control_review ;;
|
|
5137
5732
|
esac
|
|
5138
5733
|
}
|