autonomous-sdlc-harness 0.6.0 → 0.6.2
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/commands/init.js +72 -9
- package/dist/commands/init.js.map +1 -1
- package/dist/core/writer.js +3 -2
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +255 -12
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +60 -2
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/remote/githubActions.js +61 -3
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/claude/context/conventions.md +1 -1
- package/templates/claude/context/layer.md +1 -1
- package/templates/github/workflows/harness-control.yml +63 -17
- package/templates/github/workflows/harness-run.yml +144 -5
- package/templates/github/workflows/harness-trigger.yml +4 -1
- package/templates/scripts/autonomous-watcher.sh +133 -28
- package/templates/scripts/lib/harness-run-lib.sh +59 -27
- package/templates/scripts/remote-run.sh +872 -122
- 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>]
|
|
@@ -190,13 +191,18 @@
|
|
|
190
191
|
# a refusal can still be commented. It reads, only from the environment:
|
|
191
192
|
# GITHUB_EVENT_NAME, GITHUB_EVENT_PATH the event; each field is read by `jq`
|
|
192
193
|
# into a variable and is only ever an argument or file
|
|
193
|
-
# bytes, never shell source
|
|
194
|
+
# bytes, never shell source; `.repository.owner.login`
|
|
195
|
+
# and `.repository.owner.type` only when
|
|
196
|
+
# `HARNESS_RUN_ACTORS` is unset
|
|
194
197
|
# GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID the `gh` target and
|
|
195
198
|
# the URLs its comments name
|
|
196
199
|
# HARNESS_REMOTE_STOP non-empty: every start is refused
|
|
197
200
|
# HARNESS_TRIGGER_LABEL the trigger label; when empty, `DEFAULT_TRIGGER_LABEL`
|
|
198
201
|
# or `LEGACY_TRIGGER_LABEL`, per the paragraph below
|
|
199
202
|
# HARNESS_TRIGGER_ALLOWED_BOTS comma-separated bot logins allowed to start
|
|
203
|
+
# HARNESS_RUN_ACTORS comma-separated people allowed to act on a run, `*`
|
|
204
|
+
# for every writer; unset admits a `User` owner alone
|
|
205
|
+
# (`run_actor_listed`)
|
|
200
206
|
# HARNESS_TRIGGER_LOOKUP_SECS seconds between run lookups; `5` when empty.
|
|
201
207
|
# A test seam
|
|
202
208
|
# RUNNER_TEMP where the prompt snapshot is written; a `mktemp -d`
|
|
@@ -225,7 +231,13 @@
|
|
|
225
231
|
# 6. a `User` whose `collaborators/<login>/permission` is not `admin` or
|
|
226
232
|
# `write` — `maintain` reads as `write` and `triage` as `read` there; a
|
|
227
233
|
# failed call is "could not confirm write access", never a pass
|
|
228
|
-
#
|
|
234
|
+
# 7. a writer `HARNESS_RUN_ACTORS` does not admit — after refusal 6, so a
|
|
235
|
+
# non-writer is still told about write access
|
|
236
|
+
# Refusals 4 to 7 are `authorise_actor`, the one actor check `control` reuses.
|
|
237
|
+
# Before them, after refusal 2, a re-run (GITHUB_RUN_ATTEMPT above 1) whose
|
|
238
|
+
# GITHUB_TRIGGERING_ACTOR is not `github-actions[bot]` and is not admitted by
|
|
239
|
+
# `HARNESS_RUN_ACTORS` is refused (`rerun_actor_listed`), on both event kinds:
|
|
240
|
+
# a re-run replays the event, so its sender is whoever first acted.
|
|
229
241
|
# Then it fetches `origin <defaultBranch>` (a failure tolerated), derives the
|
|
230
242
|
# branch with `hr_derive_branch <title> issue_<number>`, passing `gh` so a name
|
|
231
243
|
# with run-workflow history counts as taken (2 or 3 refused), writes
|
|
@@ -251,7 +263,10 @@
|
|
|
251
263
|
# and exit 0 with no `gh` call; an empty or missing `title` is refused. It has
|
|
252
264
|
# no labeller: GitHub sends one only for a fine-grained token with Contents
|
|
253
265
|
# write or a classic token with `repo`, so the token holder is the authority,
|
|
254
|
-
# and refusals 3 to 6 do not apply.
|
|
266
|
+
# and refusals 3 to 6 do not apply. After refusal 2 a `User` sender
|
|
267
|
+
# (`.sender.login`, `.sender.type`) that `HARNESS_RUN_ACTORS` does not admit is
|
|
268
|
+
# refused with no permission call, and so is a sender with no type unless the
|
|
269
|
+
# list is `*`, failing closed; any other sender type passes. The fallback name is `task_<GITHUB_RUN_ID>`
|
|
255
270
|
# and the snapshot's provenance sentence names the event type, `source` when
|
|
256
271
|
# set, and the time. There is no issue, so no comment and no label: every
|
|
257
272
|
# outcome is printed to stdout and appended as a Markdown block to
|
|
@@ -273,26 +288,33 @@
|
|
|
273
288
|
# whose head is <branch> when origin's <branch> carries
|
|
274
289
|
# `<state_dir>/flow_progress/<branch>_progress.md`, else to the issue named by
|
|
275
290
|
# the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
276
|
-
# task prompt, else nowhere
|
|
291
|
+
# task prompt, else nowhere (`stop`'s `--pr` and `--branch-gone` change this
|
|
292
|
+
# for its own report: its paragraph). The label `STATE_LABEL_PREFIX<state>` replaces any
|
|
277
293
|
# other state label on that issue and that pull request, each when known; the
|
|
278
294
|
# label is a view, and the run list stays the authority. The state map:
|
|
279
295
|
# `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
|
|
280
296
|
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running.
|
|
281
|
-
# `
|
|
282
|
-
# `
|
|
283
|
-
#
|
|
297
|
+
# Every job event — `parked`, `park_loop`, `paused`, `resumed`, `round` and
|
|
298
|
+
# `failed` — posts nothing and sets no label when `remote_branch_stopped`,
|
|
299
|
+
# asked afresh, finds the branch stopped, so a job a stop overtook never
|
|
300
|
+
# overwrites `stopped`; a failed listing reports anyway, and `stopped` is never
|
|
301
|
+
# withheld. `completed` (deliver's) and `launched` (the trigger's
|
|
284
302
|
# own comment) are one line each, as is any other event. The comment names the
|
|
285
303
|
# next GitHub action — never a slash command — then <note> byte for byte, then
|
|
286
304
|
# this run's URL when `GITHUB_RUN_ID` is set, then the marker line
|
|
287
305
|
# `<!-- sdlc-harness event=<event> branch=<branch> -->`. The pause reason and
|
|
288
306
|
# reset come from the registry record, read only when the registry file exists.
|
|
289
307
|
# `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
|
-
#
|
|
308
|
+
# over `$root`'s state directory, ascending — carrying `question_<n>.md` less
|
|
309
|
+
# every line naming its own `answer_<n>.md` (another index's line stays), cut
|
|
310
|
+
# at its last whole line within `QUESTION_COMMENT_MAX_BYTES` and then naming
|
|
311
|
+
# the file in the `STATE_ARTIFACT_NAME` artifact; then the answer form, the
|
|
312
|
+
# comment's one answer instruction: a comment whose first line is
|
|
313
|
+
# `COMMAND_HANDLE answer <n>` (`<n>` optional when one question is open) and
|
|
314
|
+
# whose following lines are the answer; then a fenced copy block of that form,
|
|
315
|
+
# `COMMAND_HANDLE answer <n>` over `<your answer>`; the marker adds
|
|
316
|
+
# `question=<n>`. With no question open it posts nothing, sets no label and
|
|
317
|
+
# prints one `::error::` line naming the branch. The label
|
|
296
318
|
# is set once per target, not per question. On a public repository a question
|
|
297
319
|
# comment and its answer are public, as the artifact already is
|
|
298
320
|
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
@@ -356,8 +378,9 @@
|
|
|
356
378
|
# and outside the sending-verb gate: it gates itself after reading the event,
|
|
357
379
|
# so a refusal can still be replied to. The workflow's `if:` only saves a
|
|
358
380
|
# runner; every rule below holds without it. It handles `GITHUB_EVENT_NAME`
|
|
359
|
-
# `issue_comment
|
|
360
|
-
#
|
|
381
|
+
# `issue_comment`, `pull_request_review` (THE REVIEW, below), and `issues`,
|
|
382
|
+
# `pull_request` and `delete` (THE CLOSE, below); any other name, or an event
|
|
383
|
+
# file it cannot read, exits 1.
|
|
361
384
|
# It reads `.action`, `.comment.body`, `.issue.number`,
|
|
362
385
|
# `.issue.pull_request.url`, `.sender.login` and `.sender.type`, each by `jq`
|
|
363
386
|
# into a variable (data, never shell source), plus `trigger`'s environment.
|
|
@@ -372,7 +395,10 @@
|
|
|
372
395
|
# 1. `HARNESS_REMOTE_STOP` is set
|
|
373
396
|
# 2. `forge_on` fails — before any authorisation, so a disabled coupling asks
|
|
374
397
|
# GitHub nothing about the commenter
|
|
375
|
-
#
|
|
398
|
+
# then a re-run whose GITHUB_TRIGGERING_ACTOR `rerun_actor_listed`
|
|
399
|
+
# refuses, before the event's own actor is checked
|
|
400
|
+
# 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run — a
|
|
401
|
+
# writer the `HARNESS_RUN_ACTORS` allow-list admits, or a listed bot
|
|
376
402
|
# 4. the verb is empty, not a `COMMAND_VERBS` word, or one no arm carries out
|
|
377
403
|
# yet: the reply lists every command and names `docs/github-run-control.md`
|
|
378
404
|
# THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
|
|
@@ -429,7 +455,29 @@
|
|
|
429
455
|
# way on. On 0 with no other question open, a reply that the run resumes and
|
|
430
456
|
# `running` on its issue and pull request; with others open, a reply naming
|
|
431
457
|
# 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.
|
|
458
|
+
# public on a public repository, as the question already is. A STOPPED RUN IS
|
|
459
|
+
# NAMED `stopped`: a stop leaves the bundle as it was, so after `pause`,
|
|
460
|
+
# `resume`, `answer` and `clear` read the state, `control_state_word_var` asks
|
|
461
|
+
# `remote_branch_stopped` (one extra run listing) whether a `running`,
|
|
462
|
+
# `parked`, `park_loop` or `paused` run was stopped, and every reply names it
|
|
463
|
+
# `stopped`, with the state underneath where the way on depends on it. The
|
|
464
|
+
# states each verb accepts are unchanged, except that `pause` refuses a
|
|
465
|
+
# stopped `running` run, whose cancelled job is still finishing: `resume`
|
|
466
|
+
# still resumes a stopped `paused` run, and on a stopped `parked` run the
|
|
467
|
+
# answer, on a stopped `park_loop` run the clear, is its resume. `status` is
|
|
468
|
+
# READ-ONLY and accepted in every state, `none` and a run in flight included:
|
|
469
|
+
# it reads the state (`control_state_var`, `control_state_word_var`) and the
|
|
470
|
+
# first `- [ ] ` entry of the flow-progress ledger on origin's tip with the
|
|
471
|
+
# `## ` heading above it, and replies once — the state by `CS_WORD` with its
|
|
472
|
+
# pause reason, the state underneath a stopped one, an expired bundle's detail,
|
|
473
|
+
# the next ledger entry (or, on a fully ticked ledger, that a user-review round
|
|
474
|
+
# has started and its ledger is not written yet, when origin's tip carries the
|
|
475
|
+
# round's `chore: add user review for <branch>` commit after the ledger's last
|
|
476
|
+
# change; else, for a `running` run, that it is still finishing or starting a
|
|
477
|
+
# stage, never that every entry is ticked; else that every entry is ticked; or
|
|
478
|
+
# that the ledger could not be read), each open question with its `answer <n>` form, and the
|
|
479
|
+
# latest run's URL. It sets no label and runs no child verb but `fetch`; a
|
|
480
|
+
# failed state read is a refusal and exit 3.
|
|
433
481
|
# THE REVIEW. A `pull_request_review` event reads `.action`, `.review.state`,
|
|
434
482
|
# `.review.body`, `.review.id`, `.review.html_url`, `.review.submitted_at`,
|
|
435
483
|
# `.pull_request.number`, `.pull_request.head.ref`,
|
|
@@ -451,7 +499,10 @@
|
|
|
451
499
|
# `your review was collected`, naming the state and saying the next round
|
|
452
500
|
# starts by itself when that run finishes, plus the state's way on: `answer
|
|
453
501
|
# <n>` for `parked`, `clear` for `park_loop`, `resume` for any `paused` but
|
|
454
|
-
# `usage`, and for `usage` that it resumes after the reset.
|
|
502
|
+
# `usage`, and for `usage` that it resumes after the reset. A run of those
|
|
503
|
+
# states that `remote_branch_stopped` finds stopped is named `stopped`, and its
|
|
504
|
+
# way on is the one `resume` names for it: `answer <n>`, `clear`, `resume`, or
|
|
505
|
+
# for `running` `resume` once the cancelled job has ended. The review stays on
|
|
455
506
|
# the pull request, and a later round collects it. Settled: a round is CUMULATIVE,
|
|
456
507
|
# built by `round_collect` in a fresh file under `RUNNER_TEMP` from ONE
|
|
457
508
|
# paginated `pulls/<n>/reviews` and ONE paginated `pulls/<n>/comments` listing
|
|
@@ -468,9 +519,10 @@
|
|
|
468
519
|
# inline comment, by any author and whatever its review's state, whose id is
|
|
469
520
|
# unrecorded, whose body carries no `COMMENT_MARKER`, and whose `created_at` is
|
|
470
521
|
# at or after the boundary or whose review is pending. Every distinct author
|
|
471
|
-
# of a pending item passes `authorise_actor
|
|
472
|
-
#
|
|
473
|
-
#
|
|
522
|
+
# of a pending item passes `authorise_actor`, the `HARNESS_RUN_ACTORS`
|
|
523
|
+
# allow-list included; a refused author's items — a writer the list refuses
|
|
524
|
+
# among them — are dropped with one line naming the login, `AUTH_WHY` and the
|
|
525
|
+
# count, and a failed permission call (status 4) fails the collection. A round is placed only when at
|
|
474
526
|
# least one kept review has state `REVIEW_ROUND_STATE`; a *Comment* or
|
|
475
527
|
# *Approve* review rides along in the next round one requesting changes starts.
|
|
476
528
|
# The file: one `## Review by @<login>` section per pending review, oldest
|
|
@@ -505,11 +557,61 @@
|
|
|
505
557
|
# naming its last stderr line, and exit 3. Every reply goes to the item the comment was
|
|
506
558
|
# typed on, opens `@<login>`, and carries the `reply` marker; a refusal reads
|
|
507
559
|
# `@<login>: `<verb>` was not run: <reason>. <way on>`.
|
|
508
|
-
#
|
|
509
|
-
#
|
|
560
|
+
# THE CLOSE. Whatever the run's phase, it is stopped when its issue is closed,
|
|
561
|
+
# its pull request closed or merged, or its branch deleted; the routing is by
|
|
562
|
+
# event name, never by verb, so a comment naming `close` is not one. `issues`
|
|
563
|
+
# reads `.action`, `.issue.number`, `.sender.login`, `.sender.type`;
|
|
564
|
+
# `pull_request` reads `.action`, `.pull_request.number`,
|
|
565
|
+
# `.pull_request.merged`, `.pull_request.head.ref`,
|
|
566
|
+
# `.pull_request.head.repo.full_name` and the sender; `delete` reads `.ref`,
|
|
567
|
+
# `.ref_type` and the sender — each by `event_field`. Ignored, with one line
|
|
568
|
+
# and no `gh` call: an action other than `closed`; a pull request whose head
|
|
569
|
+
# repository is not `GITHUB_REPOSITORY`; a `ref_type` other than `branch`, or
|
|
570
|
+
# a ref `valid_branch` rejects. A CLOSE IS NEVER REPLIED TO: each gate below,
|
|
571
|
+
# in order, is one line and exit 0, with no comment, label or dispatch —
|
|
572
|
+
# 1. `HARNESS_REMOTE_STOP` is set
|
|
573
|
+
# 2. `forge_on` fails
|
|
574
|
+
# 3. the actor: on `issues` and `pull_request`, `authorise_actor` refused
|
|
575
|
+
# (statuses 1–3 and 5, so a writer the `HARNESS_RUN_ACTORS` allow-list
|
|
576
|
+
# refuses is ignored too), the line naming the login and `AUTH_WHY`; on
|
|
577
|
+
# `delete`, only a `Bot` sender `trigger_bot_listed` does not list, and
|
|
578
|
+
# never the allow-list: deleting a branch already needs write access, the
|
|
579
|
+
# run's branch is gone, and a stop spends no credential
|
|
580
|
+
# 4. no branch: an issue's from `control_issue_branch_var` (no genuine start
|
|
581
|
+
# comment), a pull request's head ref, a deletion's `.ref`; then not a
|
|
582
|
+
# valid branch name
|
|
583
|
+
# 5. `hr_branch_is_protected` does not answer 1
|
|
584
|
+
# 6. on `pull_request`, the head branch absent on origin (`remote_branch_exists`
|
|
585
|
+
# answers 1): GitHub closed the pull request because the branch was
|
|
586
|
+
# deleted, and the `delete` event's job stops the run; an `ls-remote` that
|
|
587
|
+
# cannot answer is one line and proceeds
|
|
588
|
+
# There is no ledger-at-tip check (`forge_recognised`): a merged or deleted
|
|
589
|
+
# branch may no longer carry one, and a listed `harness run <b>` run is the
|
|
590
|
+
# proof. The state is read by `control_state_var`: only `running`, `parked`,
|
|
591
|
+
# `park_loop` and `paused` are acted on; `completed`, `failed` and `none` are
|
|
592
|
+
# `left alone: the run on <b> is <state>`, and one `remote_branch_stopped`
|
|
593
|
+
# already finds stopped is `already stopped` — each one line and exit 0. Then
|
|
594
|
+
# `stop <b> --actor <login> --note <note>` runs as a child (`control_child`),
|
|
595
|
+
# the note `Stopped because @<login> closed issue #<n>.`, `… closed pull
|
|
596
|
+
# request #<n>.` or `… merged pull request #<n>.` (both with `--pr <n>`), or
|
|
597
|
+
# `… deleted the branch `<b>`.` (with `--branch-gone`). Every note but the
|
|
598
|
+
# deletion's adds that the workflow runs and their `harness-state` artifacts
|
|
599
|
+
# are kept; a closed pull request's `stopped` comment says to reopen it, or to
|
|
600
|
+
# use the issue, before commenting `@sdlc-harness resume`. The runs and
|
|
601
|
+
# artifacts are kept. Reopening the issue or the pull
|
|
602
|
+
# request does nothing. A `pull_request` close job runs the pull request's
|
|
603
|
+
# merge-commit copy of the workflow, as a review job does
|
|
604
|
+
# (docs/github-integration-research.md -> C2); the script stays the default
|
|
605
|
+
# branch's.
|
|
606
|
+
# 0 handled (replied), or ignored; a close stopped, or ignored (one line)
|
|
607
|
+
# 1 not an event named above, or the event could not be read
|
|
510
608
|
# 2 refused (replied)
|
|
511
609
|
# 3 a `gh` step failed: the reply could not be posted (an `::error::`
|
|
512
|
-
# line), or the action failed and was replied to
|
|
610
|
+
# line), or the action failed and was replied to; for a close, an
|
|
611
|
+
# `::error::` line and no reply, because the item is closed: the stop
|
|
612
|
+
# child failed (naming `CHILD_LAST`), or the repository name, the
|
|
613
|
+
# closer's permission, the issue's comments or the run's state could
|
|
614
|
+
# not be read
|
|
513
615
|
# 4 a review's round could not be placed; nothing was dispatched (replied)
|
|
514
616
|
#
|
|
515
617
|
# `restore` AND `save` ARE THE JOB-SIDE VERBS: the run workflow calls them in
|
|
@@ -606,12 +708,17 @@
|
|
|
606
708
|
# run URL, no dispatch
|
|
607
709
|
# decision continue refused, in this order: `HARNESS_REMOTE_STOP` set (one
|
|
608
710
|
# `paused`); the branch stopped (one log line, no
|
|
609
|
-
# notification — the user asked for it);
|
|
711
|
+
# notification — the user asked for it); the branch absent
|
|
712
|
+
# on origin (one log line, no notification; an
|
|
713
|
+
# `ls-remote` that cannot answer is one line and proceeds,
|
|
714
|
+
# since the dispatch then fails loudly); chain unreadable
|
|
610
715
|
# or `chain + 1` over `HARNESS_MAX_CHAIN` (one `failed`);
|
|
611
716
|
# otherwise re-dispatched. A dispatch that fails is one
|
|
612
717
|
# `paused` naming its error
|
|
613
|
-
# decision wait-poller the branch stopped: one log
|
|
614
|
-
#
|
|
718
|
+
# decision wait-poller the branch stopped, or absent on origin: one log
|
|
719
|
+
# line (an `ls-remote` that cannot answer is one line and
|
|
720
|
+
# proceeds). Otherwise `gh workflow enable
|
|
721
|
+
# WORKFLOW_RESUME_FILE`; a failed enable
|
|
615
722
|
# (the job token's enable permission is unverified) is one
|
|
616
723
|
# `paused` saying auto-resume is unavailable
|
|
617
724
|
# decision stop nothing: job mode has already notified
|
|
@@ -647,8 +754,10 @@
|
|
|
647
754
|
# says `status: paused` / `pause_reason: usage` it is waiting, because the job
|
|
648
755
|
# uploads before its `continue` step enables the poller; an artifact lookup or
|
|
649
756
|
# download that fails for it is one line and waiting. For a `completed` run,
|
|
650
|
-
# skipped, not waiting: a stopped branch, a
|
|
651
|
-
#
|
|
757
|
+
# skipped, not waiting: a stopped branch, a branch absent on origin (its state
|
|
758
|
+
# entry dropped, so a later branch of the name starts clean; an `ls-remote`
|
|
759
|
+
# that cannot answer is one line and proceeds), a bundle that cannot be
|
|
760
|
+
# downloaded (one line), and anything but `status: paused` / `pause_reason: usage` with an
|
|
652
761
|
# integer `usage_resume_at`, and a run whose state entry says `notified`. Due
|
|
653
762
|
# (reset passed): re-dispatched under the same chain limit — a refusal is one
|
|
654
763
|
# `failed` and not waiting; a success drops the branch's state entry. A
|
|
@@ -777,11 +886,24 @@
|
|
|
777
886
|
# local registry record exists, it writes `remote_stopped_at` and sets `status`
|
|
778
887
|
# to `failed` in one `hr_registry_set` call; a partial stop leaves the record alone
|
|
779
888
|
# 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
|
-
#
|
|
889
|
+
# reported as `report stopped` with the note `--note <text>` when given, else
|
|
890
|
+
# `Stopped by @<actor>.` under `--actor` (a login, the trigger's shape plus an
|
|
891
|
+
# optional `[bot]`; anything else is a usage error), else one naming a local
|
|
892
|
+
# stop; a partial stop reports nothing. Under `--pr <n>` that report's comment
|
|
893
|
+
# and label go to pull request <n> whatever its state — the open-PR lookup
|
|
894
|
+
# cannot find a closed one — and the issue is still labelled. The cancelled
|
|
895
|
+
# job's own `failed` is then posted nowhere, because `report` finds the branch
|
|
896
|
+
# stopped. `--branch-gone` is for a branch GitHub no longer has: (1)'s marker
|
|
897
|
+
# is dispatched with `--ref` set to GitHub's own default branch (read as `warm`
|
|
898
|
+
# reads it; a failed read exits 3 before anything is sent) and still carries
|
|
899
|
+
# `-f branch=<branch>`, so its `harness stop <branch>` title is unchanged;
|
|
900
|
+
# (2)'s listing also reads `headSha,createdAt`; (4) reads the issue from the
|
|
901
|
+
# task prompt at the newest `harness run <branch>` run's `headSha` through the
|
|
902
|
+
# contents API rather than from `origin/<branch>` (a failed read is one line
|
|
903
|
+
# and no issue), and its text says the branch was deleted, so the run cannot
|
|
904
|
+
# be resumed, and that its workflow runs and artifacts are kept. That read
|
|
905
|
+
# rests on GitHub serving a commit no branch points at, which is unverified
|
|
906
|
+
# (`docs/github-run-control.md` -> `## 8. What is not verified here`).
|
|
785
907
|
#
|
|
786
908
|
# `warm` dispatches action=warm on GitHub's OWN default branch (`gh repo view
|
|
787
909
|
# --json defaultBranchRef`), which may differ from the configured
|
|
@@ -840,6 +962,7 @@
|
|
|
840
962
|
# DEFAULT_TRIGGER_LABEL mirrors DEFAULT_TRIGGER_LABEL
|
|
841
963
|
# LEGACY_TRIGGER_LABEL mirrors LEGACY_TRIGGER_LABEL
|
|
842
964
|
# HARNESS_TRIGGER_ALLOWED_BOTS mirrors TRIGGER_ALLOWED_BOTS_VARIABLE
|
|
965
|
+
# HARNESS_RUN_ACTORS mirrors RUN_ACTORS_VARIABLE
|
|
843
966
|
# TRIGGER_DISPATCH_EVENT_TYPE mirrors TRIGGER_DISPATCH_EVENT_TYPE ('harness-task')
|
|
844
967
|
# WORKFLOW_CONTROL_FILE mirrors WORKFLOW_CONTROL_FILE
|
|
845
968
|
# COMMAND_HANDLE mirrors COMMAND_HANDLE
|
|
@@ -1027,13 +1150,18 @@
|
|
|
1027
1150
|
# "html_url":"https://github.com/o/r/issues/7","state":"open"}}, and a stub
|
|
1028
1151
|
# answering `api repos/o/r/collaborators/alice/permission` with
|
|
1029
1152
|
# {"permission":"write"}; export GITHUB_EVENT_NAME=issues GITHUB_EVENT_PATH=e.json
|
|
1030
|
-
# GITHUB_REPOSITORY=o/r HARNESS_TRIGGER_LOOKUP_SECS=0:
|
|
1153
|
+
# GITHUB_REPOSITORY=o/r HARNESS_TRIGGER_LOOKUP_SECS=0 HARNESS_RUN_ACTORS='*':
|
|
1031
1154
|
# trigger bash scripts/remote-run.sh trigger -> 0; origin/add_comments gains
|
|
1032
1155
|
# the prompt commit, "$s.log" gains `workflow run harness-run.yml
|
|
1033
1156
|
# --ref add_comments ...`, `issue comment 7 ...` naming the branch,
|
|
1034
1157
|
# then `issue edit 7 ... --remove-label sdlc-harness`
|
|
1035
1158
|
# read the permission answer {"permission":"read"} -> 2, no `workflow
|
|
1036
1159
|
# run`, one comment naming write access, the label removed
|
|
1160
|
+
# not listed HARNESS_RUN_ACTORS=bob -> 2, no `workflow run`, one comment
|
|
1161
|
+
# naming HARNESS_RUN_ACTORS, the label removed
|
|
1162
|
+
# unset HARNESS_RUN_ACTORS= and e.json gaining
|
|
1163
|
+
# "repository":{"owner":{"login":"alice","type":"User"}} -> 0, as
|
|
1164
|
+
# `trigger`; the owner `bob` instead -> 2, the comment naming @bob
|
|
1037
1165
|
# ignored e.json's label name `bug` -> 0, one line, "$s.log" unchanged
|
|
1038
1166
|
# dispatch GITHUB_EVENT_NAME=repository_dispatch GITHUB_RUN_ID=9
|
|
1039
1167
|
# GITHUB_STEP_SUMMARY=/tmp/s, e.json {"action":<TRIGGER_DISPATCH_EVENT_TYPE>,
|
|
@@ -1103,7 +1231,7 @@ LEGACY_TRIGGER_LABEL='harness'
|
|
|
1103
1231
|
TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
1104
1232
|
WORKFLOW_CONTROL_FILE='harness-control.yml'
|
|
1105
1233
|
COMMAND_HANDLE='@sdlc-harness'
|
|
1106
|
-
COMMAND_VERBS='answer pause resume stop clear'
|
|
1234
|
+
COMMAND_VERBS='answer pause resume stop clear status'
|
|
1107
1235
|
COMMENT_MARKER='<!-- sdlc-harness'
|
|
1108
1236
|
REVIEW_ROUND_STATE='changes_requested'
|
|
1109
1237
|
STATE_LABEL_PREFIX='sdlc-harness: '
|
|
@@ -1165,7 +1293,7 @@ usage() {
|
|
|
1165
1293
|
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
1294
|
echo " remote-run.sh pause <branch> [--repo <root>]" >&2
|
|
1167
1295
|
echo " remote-run.sh warm [--repo <root>]" >&2
|
|
1168
|
-
echo " remote-run.sh stop <branch> [--actor <login>] [--repo <root>]" >&2
|
|
1296
|
+
echo " remote-run.sh stop <branch> [--actor <login>] [--note <text>] [--pr <n>] [--branch-gone] [--repo <root>]" >&2
|
|
1169
1297
|
echo " remote-run.sh status <branch> [--repo <root>]" >&2
|
|
1170
1298
|
echo " remote-run.sh sync <branch> [--repo <root>]" >&2
|
|
1171
1299
|
echo " remote-run.sh fetch <branch> <out_dir> [--repo <root>]" >&2
|
|
@@ -1282,6 +1410,7 @@ source_arg=""
|
|
|
1282
1410
|
reviewers_arg=""
|
|
1283
1411
|
allow_no_run=0
|
|
1284
1412
|
pr_arg=""
|
|
1413
|
+
branch_gone=0
|
|
1285
1414
|
|
|
1286
1415
|
while [ "$#" -gt 0 ]; do
|
|
1287
1416
|
case "$1" in
|
|
@@ -1314,7 +1443,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
1314
1443
|
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
1315
1444
|
review_file="$2"; shift 2 ;;
|
|
1316
1445
|
--note)
|
|
1317
|
-
[ "$verb" = report ] || usage "$1 is a report option"
|
|
1446
|
+
[ "$verb" = report ] || [ "$verb" = stop ] || usage "$1 is a report or stop option"
|
|
1318
1447
|
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1319
1448
|
report_note="$2"; shift 2 ;;
|
|
1320
1449
|
--actor)
|
|
@@ -1333,12 +1462,15 @@ while [ "$#" -gt 0 ]; do
|
|
|
1333
1462
|
[ "$verb" = review ] || usage "$1 is a review option"
|
|
1334
1463
|
allow_no_run=1; shift ;;
|
|
1335
1464
|
--pr)
|
|
1336
|
-
[ "$verb" = collect ] || usage "$1 is a collect option"
|
|
1465
|
+
[ "$verb" = collect ] || [ "$verb" = stop ] || usage "$1 is a collect or stop option"
|
|
1337
1466
|
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1338
1467
|
case "$2" in
|
|
1339
1468
|
''|*[!0-9]*|0*) usage "--pr needs a positive pull request number" ;;
|
|
1340
1469
|
esac
|
|
1341
1470
|
pr_arg="$2"; shift 2 ;;
|
|
1471
|
+
--branch-gone)
|
|
1472
|
+
[ "$verb" = stop ] || usage "$1 is a stop option"
|
|
1473
|
+
branch_gone=1; shift ;;
|
|
1342
1474
|
-*)
|
|
1343
1475
|
usage "unknown option '$1'" ;;
|
|
1344
1476
|
*)
|
|
@@ -1634,29 +1766,48 @@ verb_pause() {
|
|
|
1634
1766
|
echo "remote-run.sh: dispatched action=pause for $branch"
|
|
1635
1767
|
}
|
|
1636
1768
|
|
|
1637
|
-
|
|
1638
|
-
|
|
1769
|
+
# github_default_branch_var — GITHUB_DEFAULT_BRANCH, GitHub's own default
|
|
1770
|
+
# branch; exits 3 when it cannot be read.
|
|
1771
|
+
GITHUB_DEFAULT_BRANCH=""
|
|
1772
|
+
github_default_branch_var() {
|
|
1639
1773
|
gh_call repo view --json defaultBranchRef || gh_fail "reading GitHub's default branch failed"
|
|
1640
|
-
|
|
1641
|
-
if ! valid_branch "$
|
|
1774
|
+
GITHUB_DEFAULT_BRANCH=$(printf '%s' "$GH_OUT" | jq -r '.defaultBranchRef.name // empty' 2>/dev/null)
|
|
1775
|
+
if ! valid_branch "$GITHUB_DEFAULT_BRANCH"; then
|
|
1642
1776
|
GH_ERR="no defaultBranchRef.name in its output"
|
|
1643
1777
|
gh_fail "reading GitHub's default branch failed"
|
|
1644
1778
|
fi
|
|
1779
|
+
}
|
|
1780
|
+
|
|
1781
|
+
verb_warm() {
|
|
1782
|
+
local default_branch
|
|
1783
|
+
github_default_branch_var
|
|
1784
|
+
default_branch="$GITHUB_DEFAULT_BRANCH"
|
|
1645
1785
|
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
1786
|
echo "remote-run.sh: dispatched action=warm on $default_branch"
|
|
1647
1787
|
}
|
|
1648
1788
|
|
|
1649
1789
|
verb_stop() {
|
|
1650
|
-
local ids id failed=0 first_err="" registry stopped_at
|
|
1790
|
+
local ids id failed=0 first_err="" registry stopped_at marker_ref="$branch" fields=databaseId,displayTitle,status
|
|
1791
|
+
local gone="" gone_sha="" note
|
|
1651
1792
|
stopped_at=$(date +%s)
|
|
1652
|
-
|
|
1793
|
+
if [ "$branch_gone" -eq 1 ]; then
|
|
1794
|
+
github_default_branch_var
|
|
1795
|
+
marker_ref="$GITHUB_DEFAULT_BRANCH"
|
|
1796
|
+
gone=gone
|
|
1797
|
+
fields="$fields,headSha,createdAt"
|
|
1798
|
+
fi
|
|
1799
|
+
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
1800
|
echo "remote-run.sh: dispatched the action=stop marker for $branch"
|
|
1654
1801
|
|
|
1655
|
-
gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json
|
|
1802
|
+
gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json "$fields" --limit 100 || gh_fail "listing the runs of '$branch' failed"
|
|
1656
1803
|
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
1804
|
GH_ERR="its run list is not the expected JSON"
|
|
1658
1805
|
gh_fail "listing the runs of '$branch' failed"
|
|
1659
1806
|
}
|
|
1807
|
+
if [ -n "$gone" ]; then
|
|
1808
|
+
gone_sha=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" \
|
|
1809
|
+
'[.[] | select(.displayTitle == $t)] | max_by(.createdAt // "") | .headSha // empty' 2>/dev/null) || gone_sha=""
|
|
1810
|
+
fi
|
|
1660
1811
|
for id in $ids; do
|
|
1661
1812
|
if gh_call run cancel "$id"; then
|
|
1662
1813
|
echo "remote-run.sh: asked GitHub to cancel run $id of $branch"
|
|
@@ -1677,11 +1828,14 @@ verb_stop() {
|
|
|
1677
1828
|
|| echo "remote-run.sh: stopped on GitHub, but the local record of $branch could not be updated" >&2
|
|
1678
1829
|
fi
|
|
1679
1830
|
echo "remote-run.sh: stopped $branch"
|
|
1680
|
-
if [ -n "$
|
|
1681
|
-
|
|
1831
|
+
if [ -n "$report_note" ]; then
|
|
1832
|
+
note="$report_note"
|
|
1833
|
+
elif [ -n "$actor_arg" ]; then
|
|
1834
|
+
note="Stopped by @$actor_arg."
|
|
1682
1835
|
else
|
|
1683
|
-
|
|
1836
|
+
note="Stopped from a local \`remote-run.sh stop\`."
|
|
1684
1837
|
fi
|
|
1838
|
+
forge_report stopped "$branch" "$note" "$pr_arg" "$gone" "$gone_sha"
|
|
1685
1839
|
}
|
|
1686
1840
|
|
|
1687
1841
|
# list_runs — the branch's runs of the workflow into GH_OUT; exits 3 on failure.
|
|
@@ -2434,6 +2588,27 @@ remote_branch_stopped() {
|
|
|
2434
2588
|
[ "$verdict" = stopped ]
|
|
2435
2589
|
}
|
|
2436
2590
|
|
|
2591
|
+
# remote_branch_exists <branch> — 0 when origin lists refs/heads/<branch>, 1
|
|
2592
|
+
# when `ls-remote` answers 2 (no such head), 2 on any other failure, with
|
|
2593
|
+
# REMOTE_BRANCH_ERR holding its exit status and first stderr line.
|
|
2594
|
+
REMOTE_BRANCH_ERR=""
|
|
2595
|
+
remote_branch_exists() {
|
|
2596
|
+
local errfile status
|
|
2597
|
+
REMOTE_BRANCH_ERR=""
|
|
2598
|
+
errfile=$(mktemp) || { REMOTE_BRANCH_ERR="mktemp failed"; return 2; }
|
|
2599
|
+
git -C "$root" ls-remote --exit-code --heads origin "refs/heads/$1" >/dev/null 2>"$errfile"
|
|
2600
|
+
status=$?
|
|
2601
|
+
case "$status" in
|
|
2602
|
+
0) rm -f "$errfile"; return 0 ;;
|
|
2603
|
+
2) rm -f "$errfile"; return 1 ;;
|
|
2604
|
+
esac
|
|
2605
|
+
IFS= read -r REMOTE_BRANCH_ERR <"$errfile" || :
|
|
2606
|
+
[ -n "$REMOTE_BRANCH_ERR" ] || REMOTE_BRANCH_ERR="(no stderr)"
|
|
2607
|
+
REMOTE_BRANCH_ERR="git ls-remote exited $status: $REMOTE_BRANCH_ERR"
|
|
2608
|
+
rm -f "$errfile"
|
|
2609
|
+
return 2
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2437
2612
|
# redispatch <engine> <chain> — `dispatch <branch> --engine <engine> --resume
|
|
2438
2613
|
# pause --chain <chain>` for the global branch, in a subshell so dispatch's own
|
|
2439
2614
|
# exits stay its own. On failure REDISPATCH_ERR holds its first stderr line.
|
|
@@ -2495,6 +2670,11 @@ continue_redispatch() {
|
|
|
2495
2670
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); not re-dispatched"; return 0 ;;
|
|
2496
2671
|
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
2672
|
esac
|
|
2673
|
+
remote_branch_exists "$branch"
|
|
2674
|
+
case $? in
|
|
2675
|
+
1) echo "remote-run.sh: $branch no longer exists on origin; not re-dispatched"; return 0 ;;
|
|
2676
|
+
2) echo "remote-run.sh: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2677
|
+
esac
|
|
2498
2678
|
if ! max_chain_var; then
|
|
2499
2679
|
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
2680
|
return 0
|
|
@@ -2519,6 +2699,11 @@ continue_wait_poller() {
|
|
|
2519
2699
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); the resume poller is not enabled"; return 0 ;;
|
|
2520
2700
|
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
2701
|
esac
|
|
2702
|
+
remote_branch_exists "$branch"
|
|
2703
|
+
case $? in
|
|
2704
|
+
1) echo "remote-run.sh: $branch no longer exists on origin; the resume poller is not enabled"; return 0 ;;
|
|
2705
|
+
2) echo "remote-run.sh: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2706
|
+
esac
|
|
2522
2707
|
if gh_call workflow enable "$WORKFLOW_RESUME_FILE"; then
|
|
2523
2708
|
echo "remote-run.sh: enabled $WORKFLOW_RESUME_FILE for $branch"
|
|
2524
2709
|
else
|
|
@@ -2684,6 +2869,11 @@ poll_branch() {
|
|
|
2684
2869
|
0) echo "remote-run.sh: poll: $branch is stopped ($STOPPED_LINE); skipped"; return 1 ;;
|
|
2685
2870
|
2) echo "remote-run.sh: poll: the stop-marker check for $branch failed ($GH_ERR); skipped"; return 1 ;;
|
|
2686
2871
|
esac
|
|
2872
|
+
remote_branch_exists "$branch"
|
|
2873
|
+
case $? in
|
|
2874
|
+
1) poll_state_drop "$branch"; echo "remote-run.sh: poll: $branch no longer exists on origin; skipped"; return 1 ;;
|
|
2875
|
+
2) echo "remote-run.sh: poll: whether $branch exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
2876
|
+
esac
|
|
2687
2877
|
if [ "$state" != completed ]; then
|
|
2688
2878
|
# Never dispatched from here: the job's own `continue` step enables the
|
|
2689
2879
|
# poller after its upload, and a later tick sees the run completed.
|
|
@@ -3314,13 +3504,73 @@ trigger_bot_listed() {
|
|
|
3314
3504
|
return 1
|
|
3315
3505
|
}
|
|
3316
3506
|
|
|
3507
|
+
# run_actor_listed <login> — 0 when HARNESS_RUN_ACTORS admits <login>: split on
|
|
3508
|
+
# `,`, each entry trimmed, empty entries dropped, matched case-insensitively,
|
|
3509
|
+
# `*` admitting everyone, an empty <login> admitted by `*` alone. A list with no
|
|
3510
|
+
# entries is unset, and admits only `.repository.owner.login` of the event file
|
|
3511
|
+
# when `.repository.owner.type` is `User`; any other owner, or an event file or
|
|
3512
|
+
# field that cannot be read, admits nobody. Otherwise 1, with RUN_ACTORS_WHY
|
|
3513
|
+
# holding one sentence. Lowercased with `tr`, never a parameter expansion:
|
|
3514
|
+
# bash 3.2 has no case-changing one.
|
|
3515
|
+
RUN_ACTORS_WHY=""
|
|
3516
|
+
run_actor_listed() {
|
|
3517
|
+
local login="$1" want rest="${HARNESS_RUN_ACTORS-}," entry listed="" owner="" owner_type=""
|
|
3518
|
+
RUN_ACTORS_WHY=""
|
|
3519
|
+
want=$(printf '%s' "$login" | tr '[:upper:]' '[:lower:]')
|
|
3520
|
+
while [ -n "$rest" ]; do
|
|
3521
|
+
entry=${rest%%,*}
|
|
3522
|
+
rest=${rest#*,}
|
|
3523
|
+
entry=${entry#"${entry%%[![:space:]]*}"}
|
|
3524
|
+
entry=${entry%"${entry##*[![:space:]]}"}
|
|
3525
|
+
[ -n "$entry" ] || continue
|
|
3526
|
+
listed=1
|
|
3527
|
+
[ "$entry" = '*' ] && return 0
|
|
3528
|
+
[ -n "$want" ] && [ "$(printf '%s' "$entry" | tr '[:upper:]' '[:lower:]')" = "$want" ] && return 0
|
|
3529
|
+
done
|
|
3530
|
+
if [ -n "$listed" ]; then
|
|
3531
|
+
RUN_ACTORS_WHY="@$login is not on the repository variable HARNESS_RUN_ACTORS, the allow-list of who may start, command, answer or review a run."
|
|
3532
|
+
return 1
|
|
3533
|
+
fi
|
|
3534
|
+
if [ -n "${GITHUB_EVENT_PATH-}" ] && [ -r "$GITHUB_EVENT_PATH" ]; then
|
|
3535
|
+
event_field '.repository.owner.type // ""' && owner_type="$EVENT_VALUE"
|
|
3536
|
+
event_field '.repository.owner.login // ""' && owner="$EVENT_VALUE"
|
|
3537
|
+
fi
|
|
3538
|
+
if [ "$owner_type" = User ] && [ -n "$owner" ]; then
|
|
3539
|
+
[ -n "$want" ] && [ "$(printf '%s' "$owner" | tr '[:upper:]' '[:lower:]')" = "$want" ] && return 0
|
|
3540
|
+
RUN_ACTORS_WHY="@$login is not the repository owner, and the repository variable HARNESS_RUN_ACTORS is unset, which admits the owner, @$owner, alone."
|
|
3541
|
+
return 1
|
|
3542
|
+
fi
|
|
3543
|
+
RUN_ACTORS_WHY="the repository variable HARNESS_RUN_ACTORS is unset, and this repository has no single owner to admit (its owner is ${owner_type:-unreadable}), so it admits nobody until it names the logins allowed, or * for every writer."
|
|
3544
|
+
return 1
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
# rerun_actor_listed — 0 unless this job is a re-run (GITHUB_RUN_ATTEMPT above
|
|
3548
|
+
# 1) whose GITHUB_TRIGGERING_ACTOR is neither `github-actions[bot]` nor
|
|
3549
|
+
# admitted by run_actor_listed; then 1, with RUN_ACTORS_WHY naming the
|
|
3550
|
+
# re-runner. A re-run replays the original event, whose sender is the person
|
|
3551
|
+
# who first acted, so authorise_actor alone would check the wrong account.
|
|
3552
|
+
rerun_actor_listed() {
|
|
3553
|
+
local attempt="${GITHUB_RUN_ATTEMPT-}" who="${GITHUB_TRIGGERING_ACTOR-}"
|
|
3554
|
+
RUN_ACTORS_WHY=""
|
|
3555
|
+
case "$attempt" in ''|*[!0-9]*) return 0 ;; esac
|
|
3556
|
+
[ "$attempt" -gt 1 ] || return 0
|
|
3557
|
+
[ "$who" != 'github-actions[bot]' ] || return 0
|
|
3558
|
+
run_actor_listed "$who" && return 0
|
|
3559
|
+
RUN_ACTORS_WHY="this job is a re-run by @${who:-(no actor)}, and the re-runner is checked rather than the event's sender: $RUN_ACTORS_WHY"
|
|
3560
|
+
return 1
|
|
3561
|
+
}
|
|
3562
|
+
|
|
3317
3563
|
# authorise_actor <login> <type> — the one actor check, shared by `trigger` and
|
|
3318
3564
|
# `control`: 0 when authorised. Otherwise AUTH_WHY holds one sentence and the
|
|
3319
3565
|
# status names the arm: 1 `ghost`, empty or not a login shape; 2 a non-`User`
|
|
3320
3566
|
# not listed in HARNESS_TRIGGER_ALLOWED_BOTS, decided with no permission call,
|
|
3321
|
-
# because the permission API answers `none` or 404 for a bot
|
|
3322
|
-
#
|
|
3323
|
-
#
|
|
3567
|
+
# because the permission API answers `none` or 404 for a bot, and never
|
|
3568
|
+
# consulting the run-actor list; 3 a `User` whose permission is not `admin` or
|
|
3569
|
+
# `write` (AUTH_PERMISSION holds it); 4 that permission call failed (GH_ERR
|
|
3570
|
+
# holds why); 5 a writer `run_actor_listed` refuses. The list is checked after
|
|
3571
|
+
# the permission call so a non-writer's refusal keeps naming write access, and
|
|
3572
|
+
# 5 is kept for a writer the maintainer has not named. Prints nothing and
|
|
3573
|
+
# never posts.
|
|
3324
3574
|
AUTH_WHY=""
|
|
3325
3575
|
AUTH_PERMISSION=""
|
|
3326
3576
|
authorise_actor() {
|
|
@@ -3344,7 +3594,10 @@ authorise_actor() {
|
|
|
3344
3594
|
fi
|
|
3345
3595
|
AUTH_PERMISSION=$(printf '%s' "$GH_OUT" | jq -r '.permission // empty' 2>/dev/null) || AUTH_PERMISSION=""
|
|
3346
3596
|
case "$AUTH_PERMISSION" in
|
|
3347
|
-
admin|write)
|
|
3597
|
+
admin|write)
|
|
3598
|
+
run_actor_listed "$login" && return 0
|
|
3599
|
+
AUTH_WHY="$RUN_ACTORS_WHY"
|
|
3600
|
+
return 5 ;;
|
|
3348
3601
|
esac
|
|
3349
3602
|
AUTH_WHY="GitHub reports the permission of @$login as ${AUTH_PERMISSION:-nothing}, not write or admin."
|
|
3350
3603
|
return 3
|
|
@@ -3444,7 +3697,9 @@ verb_trigger() {
|
|
|
3444
3697
|
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
3445
3698
|
&& event_field '.client_payload.title // ""' && title="$EVENT_VALUE" \
|
|
3446
3699
|
&& event_field '.client_payload.body // ""' && body="$EVENT_VALUE" \
|
|
3447
|
-
&& event_field '.client_payload.source // ""' && source="$EVENT_VALUE"
|
|
3700
|
+
&& event_field '.client_payload.source // ""' && source="$EVENT_VALUE" \
|
|
3701
|
+
&& event_field '.sender.login // ""' && login="$EVENT_VALUE" \
|
|
3702
|
+
&& event_field '.sender.type // ""' && sender_type="$EVENT_VALUE"; } || {
|
|
3448
3703
|
echo "remote-run.sh: trigger: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
3449
3704
|
exit "$EXIT_USAGE"
|
|
3450
3705
|
}
|
|
@@ -3480,6 +3735,26 @@ verb_trigger() {
|
|
|
3480
3735
|
"Set both keys on the default branch, then $retry_then."
|
|
3481
3736
|
fi
|
|
3482
3737
|
|
|
3738
|
+
# A re-run replays the event's sender; the re-runner is held to the list.
|
|
3739
|
+
if ! rerun_actor_listed; then
|
|
3740
|
+
trigger_refuse "$RUN_ACTORS_WHY" \
|
|
3741
|
+
"Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can $retry_then."
|
|
3742
|
+
fi
|
|
3743
|
+
|
|
3744
|
+
# A dispatch's `User` sender is held to the allow-list with no permission
|
|
3745
|
+
# call; a sender with no type fails closed, since an empty login is admitted
|
|
3746
|
+
# by `*` alone; any other sender type is the token's, and passes.
|
|
3747
|
+
if [ "$trigger_source" = dispatch ]; then
|
|
3748
|
+
if [ -z "$sender_type" ] && ! run_actor_listed ""; then
|
|
3749
|
+
trigger_refuse "the dispatch's sender carries no account type, so it cannot be checked against the allow-list." \
|
|
3750
|
+
"Set the repository variable \`HARNESS_RUN_ACTORS\` to \`*\` to admit every token holder, then $retry_then."
|
|
3751
|
+
fi
|
|
3752
|
+
if [ "$sender_type" = User ] && ! run_actor_listed "$login"; then
|
|
3753
|
+
trigger_refuse "$RUN_ACTORS_WHY" \
|
|
3754
|
+
"Add \`$login\` to the comma-separated repository variable \`HARNESS_RUN_ACTORS\`, or set it to \`*\` to admit every writer, then $retry_then."
|
|
3755
|
+
fi
|
|
3756
|
+
fi
|
|
3757
|
+
|
|
3483
3758
|
if [ "$trigger_source" = issue ]; then
|
|
3484
3759
|
if [ "$state" != open ]; then
|
|
3485
3760
|
trigger_refuse "this issue is not open." "Reopen it, then re-apply the label \`$trigger_label\`."
|
|
@@ -3495,6 +3770,8 @@ verb_trigger() {
|
|
|
3495
3770
|
"Add \`$login\` to that comma-separated list to let it start runs, or have a collaborator with write access apply the label \`$trigger_label\`." ;;
|
|
3496
3771
|
3) trigger_refuse "could not confirm write access for @$login: GitHub reports their permission as \`${AUTH_PERMISSION:-nothing}\`." \
|
|
3497
3772
|
"Only a collaborator with write, maintain or admin access starts a run by labelling an issue; one of them can re-apply the label \`$trigger_label\`." ;;
|
|
3773
|
+
5) trigger_refuse "$AUTH_WHY" \
|
|
3774
|
+
"Add \`$login\` to the comma-separated repository variable \`HARNESS_RUN_ACTORS\`, or set it to \`*\` to admit every writer, then re-apply the label \`$trigger_label\`." ;;
|
|
3498
3775
|
*) trigger_refuse "could not confirm write access for @$login: the permission check failed ($GH_ERR)." \
|
|
3499
3776
|
"Re-apply the label \`$trigger_label\` to try again." ;;
|
|
3500
3777
|
esac
|
|
@@ -3667,7 +3944,7 @@ forge_marker() {
|
|
|
3667
3944
|
FORGE_ISSUE=""
|
|
3668
3945
|
FORGE_TRIGGER_LABEL=""
|
|
3669
3946
|
forge_issue_var() {
|
|
3670
|
-
local state_rel rel prompt
|
|
3947
|
+
local state_rel rel prompt
|
|
3671
3948
|
FORGE_ISSUE=""
|
|
3672
3949
|
FORGE_TRIGGER_LABEL=""
|
|
3673
3950
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
@@ -3677,6 +3954,41 @@ forge_issue_var() {
|
|
|
3677
3954
|
fi
|
|
3678
3955
|
rel=$(hr_task_prompt_rel "$state_rel" "$1")
|
|
3679
3956
|
prompt=$(git -C "$root" show "refs/remotes/origin/$1:$rel" 2>/dev/null) || return 0
|
|
3957
|
+
forge_provenance_parse "$prompt"
|
|
3958
|
+
}
|
|
3959
|
+
|
|
3960
|
+
# forge_issue_at_commit_var <branch> <sha> — forge_issue_var for a branch
|
|
3961
|
+
# GitHub no longer has: the task prompt is read through the contents API at
|
|
3962
|
+
# <sha>. A failed read is one line and leaves both empty.
|
|
3963
|
+
forge_issue_at_commit_var() {
|
|
3964
|
+
local state_rel path
|
|
3965
|
+
FORGE_ISSUE=""
|
|
3966
|
+
FORGE_TRIGGER_LABEL=""
|
|
3967
|
+
if ! [[ "${2-}" =~ ^[0-9a-f]{7,40}$ ]]; then
|
|
3968
|
+
echo "remote-run.sh: report: no \`harness run $1\` run names a commit; the issue of $1 is unknown" >&2
|
|
3969
|
+
return 1
|
|
3970
|
+
fi
|
|
3971
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3972
|
+
if [ -z "$state_rel" ]; then
|
|
3973
|
+
echo "remote-run.sh: cannot resolve the state directory under '$root'" >&2
|
|
3974
|
+
return 1
|
|
3975
|
+
fi
|
|
3976
|
+
GH_ERR="its path could not be encoded"
|
|
3977
|
+
path=$(jq -rn --arg p "$(hr_task_prompt_rel "$state_rel" "$1")" '$p | split("/") | map(@uri) | join("/")') || path=""
|
|
3978
|
+
if [ -z "$path" ] || ! gh_call api "repos/$FORGE_REPO/contents/$path?ref=$2" -H "Accept: application/vnd.github.raw"; then
|
|
3979
|
+
echo "remote-run.sh: report: reading the task prompt of $1 at $2 failed: $GH_ERR" >&2
|
|
3980
|
+
return 1
|
|
3981
|
+
fi
|
|
3982
|
+
forge_provenance_parse "$GH_OUT"
|
|
3983
|
+
}
|
|
3984
|
+
|
|
3985
|
+
# forge_provenance_parse <prompt> — FORGE_ISSUE and FORGE_TRIGGER_LABEL from
|
|
3986
|
+
# the last provenance line of <prompt>, matched against this repository's own
|
|
3987
|
+
# issue URL only.
|
|
3988
|
+
forge_provenance_parse() {
|
|
3989
|
+
local prompt="$1" line rest num prefix label
|
|
3990
|
+
FORGE_ISSUE=""
|
|
3991
|
+
FORGE_TRIGGER_LABEL=""
|
|
3680
3992
|
prefix="Started from $FORGE_SERVER/$FORGE_REPO/issues/"
|
|
3681
3993
|
while IFS= read -r line; do
|
|
3682
3994
|
case "$line" in
|
|
@@ -3801,10 +4113,20 @@ forge_utc() {
|
|
|
3801
4113
|
|
|
3802
4114
|
# forge_question_body <out_file> <branch> <n> <open_count> <clar_dir> [<note>] —
|
|
3803
4115
|
# write question <n>'s park comment, without its marker, into <out_file>: the
|
|
3804
|
-
# file's bytes
|
|
3805
|
-
#
|
|
4116
|
+
# file's bytes less every line naming `answer_<n>.md` (the file channel's own
|
|
4117
|
+
# answer line; another index's stays), cut at the last whole line within
|
|
4118
|
+
# QUESTION_COMMENT_MAX_BYTES when it is over it (measured in bytes; `${#…}`
|
|
4119
|
+
# counts characters), then the answer form and its copy block. Leaves
|
|
4120
|
+
# <out_file>.q and, when cut, <out_file>.cut for the caller to remove.
|
|
3806
4121
|
forge_question_body() {
|
|
3807
|
-
local out="$1" br="$2" n="$3" count="$4" qfile="$5/question_$3.md" note="${6-}" size cut=0 last
|
|
4122
|
+
local out="$1" br="$2" n="$3" count="$4" qfile="$5/question_$3.md" note="${6-}" size cut=0 last rc
|
|
4123
|
+
[ -r "$qfile" ] || return 1
|
|
4124
|
+
# -a and LC_ALL=C: a question is text whatever bytes it holds. Status 1 is
|
|
4125
|
+
# every line dropped, not a failure.
|
|
4126
|
+
rc=0
|
|
4127
|
+
LC_ALL=C grep -avF "answer_$n.md" "$qfile" >"$out.q" || rc=$?
|
|
4128
|
+
[ "$rc" -le 1 ] || return 1
|
|
4129
|
+
qfile="$out.q"
|
|
3808
4130
|
size=$(wc -c <"$qfile") || return 1
|
|
3809
4131
|
size=$((size))
|
|
3810
4132
|
{
|
|
@@ -3826,17 +4148,21 @@ forge_question_body() {
|
|
|
3826
4148
|
"$HR_REMOTE_CLARIFY_DIR" "$br" "$n" "$STATE_ARTIFACT_NAME"
|
|
3827
4149
|
printf '\nAnswer with a comment whose first line is `%s answer %s` and whose following lines are your answer.' "$COMMAND_HANDLE" "$n"
|
|
3828
4150
|
[ "$count" -ne 1 ] || printf ' This is the only open question, so `%s` may be left out: `%s answer`.' "$n" "$COMMAND_HANDLE"
|
|
3829
|
-
printf '\n'
|
|
4151
|
+
printf '\n\n```\n%s answer %s\n<your answer>\n```\n' "$COMMAND_HANDLE" "$n"
|
|
3830
4152
|
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
3831
4153
|
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
3832
4154
|
} >>"$out"
|
|
3833
4155
|
}
|
|
3834
4156
|
|
|
3835
|
-
# forge_report <event> <branch> [<note>] — one lifecycle
|
|
3836
|
-
# one per open question) and the state label, by the
|
|
3837
|
-
#
|
|
4157
|
+
# forge_report <event> <branch> [<note> [<pr> [gone [<sha>]]]] — one lifecycle
|
|
4158
|
+
# comment (on `parked`, one per open question) and the state label, by the
|
|
4159
|
+
# target rule above. Read on `stopped` only: <pr>, an explicit pull request
|
|
4160
|
+
# that is the target whatever its state; `gone`, the branch deleted on GitHub,
|
|
4161
|
+
# its issue read from the task prompt at <sha>. Every event but `stopped` is
|
|
4162
|
+
# withheld when the branch's newest `harness stop` run is newer than its newest
|
|
4163
|
+
# `harness run` run, read from a fresh listing. Always 0.
|
|
3838
4164
|
forge_report() {
|
|
3839
|
-
local event="$1" br="$2" note="${3-}" state reason="" resume_at="" when registry_file
|
|
4165
|
+
local event="$1" br="$2" note="${3-}" pr="${4-}" gone="${5-}" gone_sha="${6-}" state reason="" resume_at="" when registry_file
|
|
3840
4166
|
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
|
|
3841
4167
|
case "$event" in
|
|
3842
4168
|
parked|park_loop) state=parked ;;
|
|
@@ -3861,7 +4187,9 @@ forge_report() {
|
|
|
3861
4187
|
fi
|
|
3862
4188
|
forge_repo_var || return 0
|
|
3863
4189
|
|
|
3864
|
-
if [ "$event"
|
|
4190
|
+
if [ "$event" != stopped ]; then
|
|
4191
|
+
# Re-listed: `review` dispatches a run and then reports `round` in one invocation.
|
|
4192
|
+
ALL_RUNS_LISTED=0
|
|
3865
4193
|
remote_branch_stopped "$br"
|
|
3866
4194
|
stopped=$?
|
|
3867
4195
|
if [ "$stopped" -eq 0 ]; then
|
|
@@ -3869,13 +4197,25 @@ forge_report() {
|
|
|
3869
4197
|
return 0
|
|
3870
4198
|
fi
|
|
3871
4199
|
[ "$stopped" -eq 1 ] \
|
|
3872
|
-
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the
|
|
4200
|
+
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the $event" >&2
|
|
3873
4201
|
fi
|
|
3874
4202
|
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
4203
|
+
[ "$event" = stopped ] || { pr=""; gone=""; }
|
|
4204
|
+
if [ -n "$gone" ]; then
|
|
4205
|
+
forge_issue_at_commit_var "$br" "$gone_sha" || FORGE_ISSUE=""
|
|
4206
|
+
else
|
|
4207
|
+
forge_fetch_branch "$br"
|
|
4208
|
+
forge_issue_var "$br" || FORGE_ISSUE=""
|
|
4209
|
+
fi
|
|
4210
|
+
if [ -n "$pr" ]; then
|
|
4211
|
+
FORGE_PR="$pr"
|
|
4212
|
+
elif [ -n "$gone" ]; then
|
|
4213
|
+
# GitHub closes a pull request whose head is deleted, so none is open.
|
|
4214
|
+
FORGE_PR=""
|
|
4215
|
+
else
|
|
4216
|
+
forge_pr_var "$br" || FORGE_PR=""
|
|
4217
|
+
fi
|
|
4218
|
+
if [ -z "$pr" ] && [ -n "$FORGE_PR" ] && ! forge_recognised "$br"; then
|
|
3879
4219
|
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries no flow-progress ledger; it is not a target"
|
|
3880
4220
|
FORGE_PR=""
|
|
3881
4221
|
fi
|
|
@@ -3905,8 +4245,6 @@ forge_report() {
|
|
|
3905
4245
|
fi ;;
|
|
3906
4246
|
park_loop)
|
|
3907
4247
|
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
4248
|
resumed)
|
|
3911
4249
|
text="The harness run on \`$br\` resumed." ;;
|
|
3912
4250
|
failed)
|
|
@@ -3917,7 +4255,14 @@ forge_report() {
|
|
|
3917
4255
|
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
4256
|
fi ;;
|
|
3919
4257
|
stopped)
|
|
3920
|
-
|
|
4258
|
+
if [ -n "$gone" ]; then
|
|
4259
|
+
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."
|
|
4260
|
+
elif [ -n "$pr" ]; then
|
|
4261
|
+
# Only a close passes <pr>, and every command on a closed pull request is refused.
|
|
4262
|
+
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
|
+
else
|
|
4264
|
+
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."
|
|
4265
|
+
fi ;;
|
|
3921
4266
|
round)
|
|
3922
4267
|
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again." ;;
|
|
3923
4268
|
esac
|
|
@@ -3926,6 +4271,10 @@ forge_report() {
|
|
|
3926
4271
|
if [ "$event" = parked ]; then
|
|
3927
4272
|
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3928
4273
|
[ -z "$state_rel" ] || open_questions_in "$root/${state_rel%/}"
|
|
4274
|
+
if [ -z "$OPEN_QUESTIONS" ]; then
|
|
4275
|
+
echo "::error::remote-run.sh: report: $br was classified parked with no open question; nothing posted"
|
|
4276
|
+
return 0
|
|
4277
|
+
fi
|
|
3929
4278
|
fi
|
|
3930
4279
|
|
|
3931
4280
|
tmp="${RUNNER_TEMP-}"
|
|
@@ -3943,7 +4292,7 @@ forge_report() {
|
|
|
3943
4292
|
else
|
|
3944
4293
|
echo "remote-run.sh: report: cannot write question $n's comment for #$target; not posted" >&2
|
|
3945
4294
|
fi
|
|
3946
|
-
rm -f "$file" "$file.cut"
|
|
4295
|
+
rm -f "$file" "$file.cut" "$file.q"
|
|
3947
4296
|
else
|
|
3948
4297
|
echo "remote-run.sh: report: cannot create question $n's comment file for #$target; not posted" >&2
|
|
3949
4298
|
fi
|
|
@@ -4256,6 +4605,62 @@ control_state_var() {
|
|
|
4256
4605
|
return 0
|
|
4257
4606
|
}
|
|
4258
4607
|
|
|
4608
|
+
# control_branch_stopped <state> — 0 when <state> is unfinished (`running`,
|
|
4609
|
+
# `parked`, `park_loop`, `paused`) and `remote_branch_stopped` finds
|
|
4610
|
+
# CONTROL_BRANCH stopped; else 1, after one stderr line when the check failed.
|
|
4611
|
+
# The run list, not the bundle, records a stop.
|
|
4612
|
+
control_branch_stopped() {
|
|
4613
|
+
local status=0
|
|
4614
|
+
case "$1" in
|
|
4615
|
+
running|parked|park_loop|paused) ;;
|
|
4616
|
+
*) return 1 ;;
|
|
4617
|
+
esac
|
|
4618
|
+
remote_branch_stopped "$CONTROL_BRANCH" || status=$?
|
|
4619
|
+
case "$status" in
|
|
4620
|
+
0) return 0 ;;
|
|
4621
|
+
1) ;;
|
|
4622
|
+
*) echo "remote-run.sh: control: whether \`$CONTROL_BRANCH\` is stopped could not be read ($GH_ERR); the reply names the state as read" >&2 ;;
|
|
4623
|
+
esac
|
|
4624
|
+
return 1
|
|
4625
|
+
}
|
|
4626
|
+
|
|
4627
|
+
# control_state_word_var — called right after a successful control_state_var:
|
|
4628
|
+
# CS_STOPPED 1 when control_branch_stopped answers 0 for CS_STATE, else 0;
|
|
4629
|
+
# CS_WORD `stopped` then, else CS_STATE. Every reply names the state by
|
|
4630
|
+
# CS_WORD; every test of which states a verb accepts reads CS_STATE.
|
|
4631
|
+
CS_STOPPED=0; CS_WORD=""
|
|
4632
|
+
control_state_word_var() {
|
|
4633
|
+
CS_STOPPED=0
|
|
4634
|
+
CS_WORD="$CS_STATE"
|
|
4635
|
+
if control_branch_stopped "$CS_STATE"; then
|
|
4636
|
+
CS_STOPPED=1
|
|
4637
|
+
CS_WORD=stopped
|
|
4638
|
+
fi
|
|
4639
|
+
return 0
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
# control_stopped_refuse — the refusal of a verb on a stopped run (CS_STOPPED
|
|
4643
|
+
# 1): names it `stopped`, the state underneath where the way on depends on it,
|
|
4644
|
+
# and the way on that state takes.
|
|
4645
|
+
control_stopped_refuse() {
|
|
4646
|
+
local open=""
|
|
4647
|
+
case "$CS_STATE" in
|
|
4648
|
+
parked)
|
|
4649
|
+
[ -z "$CS_OPEN" ] || open=", open: $CS_OPEN"
|
|
4650
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\` (it was parked, waiting for an answer$open)" \
|
|
4651
|
+
"Comment \`$COMMAND_HANDLE answer <n>\` with the answer to question <n> on the lines below it; the answer resumes it." ;;
|
|
4652
|
+
park_loop)
|
|
4653
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\` (it was held by the park-loop guard)" \
|
|
4654
|
+
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
4655
|
+
running)
|
|
4656
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`; its cancelled job is still finishing" \
|
|
4657
|
+
"Comment \`$COMMAND_HANDLE resume\` once it has ended." ;;
|
|
4658
|
+
*)
|
|
4659
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`" \
|
|
4660
|
+
"Comment \`$COMMAND_HANDLE resume\` to continue it from its committed ledger." ;;
|
|
4661
|
+
esac
|
|
4662
|
+
}
|
|
4663
|
+
|
|
4259
4664
|
# control_check_branch <branch> — the refusals both paths share, in order: a
|
|
4260
4665
|
# branch not answered 1 by hr_branch_is_protected, then (after a fetch) one
|
|
4261
4666
|
# whose origin tip carries no flow-progress ledger. Each is a reply and exit 2.
|
|
@@ -4302,20 +4707,28 @@ control_branch_from_pr() {
|
|
|
4302
4707
|
control_check_branch "$head"
|
|
4303
4708
|
}
|
|
4304
4709
|
|
|
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
|
-
|
|
4710
|
+
# control_issue_branch_var <number> — ISSUE_BRANCH, the branch of the issue's
|
|
4711
|
+
# last genuine start comment: by `github-actions[bot]`, its first line opening
|
|
4712
|
+
# with the trigger's start sentence and a backticked <b>, and its last non-empty
|
|
4713
|
+
# line exactly `forge_marker started <b>`. A marker anywhere else is never
|
|
4714
|
+
# trusted. 1 when there is none, ISSUE_BRANCH_ERR then non-empty when the
|
|
4715
|
+
# comments could not be read. Posts nothing.
|
|
4716
|
+
ISSUE_BRANCH=""
|
|
4717
|
+
ISSUE_BRANCH_ERR=""
|
|
4718
|
+
control_issue_branch_var() {
|
|
4719
|
+
local count i login body last b found="" lead='Started a harness run on the branch `'
|
|
4720
|
+
ISSUE_BRANCH=""
|
|
4721
|
+
ISSUE_BRANCH_ERR=""
|
|
4311
4722
|
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$1/comments" --jq '.[] | {login: .user.login, body: .body}'; then
|
|
4312
|
-
|
|
4723
|
+
ISSUE_BRANCH_ERR="$GH_ERR"
|
|
4724
|
+
return 1
|
|
4313
4725
|
fi
|
|
4314
4726
|
count=$(printf '%s' "$GH_OUT" | jq -s 'length' 2>/dev/null) || count=""
|
|
4315
4727
|
case "$count" in
|
|
4316
4728
|
''|*[!0-9]*)
|
|
4317
4729
|
GH_ERR="its comment list is not the expected JSON"
|
|
4318
|
-
|
|
4730
|
+
ISSUE_BRANCH_ERR="$GH_ERR"
|
|
4731
|
+
return 1 ;;
|
|
4319
4732
|
esac
|
|
4320
4733
|
for ((i = 0; i < count; i++)); do
|
|
4321
4734
|
login=$(printf '%s' "$GH_OUT" | jq -s -r --argjson i "$i" '.[$i].login // ""' 2>/dev/null) || continue
|
|
@@ -4338,17 +4751,27 @@ control_branch_from_issue() {
|
|
|
4338
4751
|
[ "$last" = "$(forge_marker started "$b")" ] || continue
|
|
4339
4752
|
found="$b"
|
|
4340
4753
|
done
|
|
4341
|
-
|
|
4754
|
+
[ -n "$found" ] || return 1
|
|
4755
|
+
ISSUE_BRANCH="$found"
|
|
4756
|
+
}
|
|
4757
|
+
|
|
4758
|
+
# control_branch_from_issue <number> — control_issue_branch_var, its failures
|
|
4759
|
+
# refused, then control_check_branch's refusals.
|
|
4760
|
+
control_branch_from_issue() {
|
|
4761
|
+
if ! control_issue_branch_var "$1"; then
|
|
4762
|
+
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
4763
|
+
control_refuse "$EXIT_GH" "the comments of issue #$1 could not be read ($ISSUE_BRANCH_ERR)" "Comment again to retry."
|
|
4764
|
+
fi
|
|
4342
4765
|
control_refuse "$EXIT_REFUSED" "no harness run was started from this issue" \
|
|
4343
4766
|
"Comment on the pull request of the run's branch instead."
|
|
4344
4767
|
fi
|
|
4345
|
-
control_check_branch "$
|
|
4768
|
+
control_check_branch "$ISSUE_BRANCH"
|
|
4346
4769
|
}
|
|
4347
4770
|
|
|
4348
4771
|
# control_verb_handled <verb> — 0 when an arm below carries out <verb>.
|
|
4349
4772
|
control_verb_handled() {
|
|
4350
4773
|
case "$1" in
|
|
4351
|
-
answer|pause|stop|resume|clear) return 0 ;;
|
|
4774
|
+
answer|pause|stop|resume|clear|status) return 0 ;;
|
|
4352
4775
|
esac
|
|
4353
4776
|
return 1
|
|
4354
4777
|
}
|
|
@@ -4365,7 +4788,7 @@ control_resume_dispatch() {
|
|
|
4365
4788
|
route=$(hr_github_resume_route "$CONTROL_BRANCH" "<task, user_review or docs: the one the run was started with>")
|
|
4366
4789
|
route=${route#or from GitHub: }
|
|
4367
4790
|
[ "$#" -eq 0 ] || clear=", with park_loop_clear true as well"
|
|
4368
|
-
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (\`$
|
|
4791
|
+
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
4792
|
"Resume it with the **Run workflow** form instead: $route$clear."
|
|
4370
4793
|
fi
|
|
4371
4794
|
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
@@ -4390,11 +4813,15 @@ control_resume() {
|
|
|
4390
4813
|
local open
|
|
4391
4814
|
control_state_var "$CONTROL_BRANCH" \
|
|
4392
4815
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4816
|
+
control_state_word_var
|
|
4393
4817
|
case "$CS_STATE" in
|
|
4394
4818
|
paused)
|
|
4395
4819
|
# Every pause reason, `expired` and `killed` included, resumes from the
|
|
4396
4820
|
# committed ledger, as the local route does.
|
|
4397
4821
|
control_resume_dispatch "Resume requested by @$CONTROL_ACTOR: \`$CONTROL_BRANCH\` continues from its committed ledger." ;;
|
|
4822
|
+
esac
|
|
4823
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4824
|
+
case "$CS_STATE" in
|
|
4398
4825
|
park_loop)
|
|
4399
4826
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard" \
|
|
4400
4827
|
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
@@ -4406,20 +4833,24 @@ control_resume() {
|
|
|
4406
4833
|
running)
|
|
4407
4834
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is already \`running\`" "Nothing needs resuming." ;;
|
|
4408
4835
|
*)
|
|
4409
|
-
control_refuse "$EXIT_REFUSED" "only a paused run can be resumed, and the run on \`$CONTROL_BRANCH\` is \`${
|
|
4836
|
+
control_refuse "$EXIT_REFUSED" "only a paused run can be resumed, and the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4410
4837
|
"A finished run continues by a review requesting changes on its pull request, or by applying the trigger label to its issue again." ;;
|
|
4411
4838
|
esac
|
|
4412
4839
|
}
|
|
4413
4840
|
|
|
4414
4841
|
control_clear() {
|
|
4842
|
+
local subject="the run"
|
|
4415
4843
|
control_state_var "$CONTROL_BRANCH" \
|
|
4416
4844
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4845
|
+
control_state_word_var
|
|
4417
4846
|
if [ "$CS_STATE" != park_loop ]; then
|
|
4418
|
-
|
|
4847
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4848
|
+
control_refuse "$EXIT_REFUSED" "there is no park-loop hold to clear: the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4419
4849
|
"Only a run held by the park-loop guard is cleared."
|
|
4420
4850
|
fi
|
|
4851
|
+
[ "$CS_STOPPED" != 1 ] || subject="the stopped run"
|
|
4421
4852
|
# 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;
|
|
4853
|
+
control_resume_dispatch "Park-loop hold on \`$CONTROL_BRANCH\` cleared by @$CONTROL_ACTOR; $subject resumes from its committed ledger." \
|
|
4423
4854
|
--park-loop-clear
|
|
4424
4855
|
}
|
|
4425
4856
|
|
|
@@ -4427,8 +4858,11 @@ control_pause() {
|
|
|
4427
4858
|
local out
|
|
4428
4859
|
control_state_var "$CONTROL_BRANCH" \
|
|
4429
4860
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4861
|
+
control_state_word_var
|
|
4862
|
+
# A stopped `running` run is a cancel still finishing: nothing to pause.
|
|
4863
|
+
[ "$CS_STOPPED" != 1 ] || control_stopped_refuse
|
|
4430
4864
|
if [ "$CS_STATE" != running ]; then
|
|
4431
|
-
control_refuse "$EXIT_REFUSED" "only a running run can be paused, and the run on \`$CONTROL_BRANCH\` is \`$
|
|
4865
|
+
control_refuse "$EXIT_REFUSED" "only a running run can be paused, and the run on \`$CONTROL_BRANCH\` is \`$CS_WORD\`" \
|
|
4432
4866
|
"Nothing needs pausing."
|
|
4433
4867
|
fi
|
|
4434
4868
|
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
@@ -4463,12 +4897,102 @@ control_stop() {
|
|
|
4463
4897
|
esac
|
|
4464
4898
|
}
|
|
4465
4899
|
|
|
4900
|
+
# control_ledger_next_var — LEDGER_NEXT, the first `- [ ] ` line of
|
|
4901
|
+
# CONTROL_BRANCH's flow-progress ledger on origin's tip without that prefix,
|
|
4902
|
+
# and LEDGER_SECTION, the last `## ` heading above it; both empty when every
|
|
4903
|
+
# entry is ticked. 1 when the ledger cannot be read.
|
|
4904
|
+
LEDGER_NEXT=""; LEDGER_SECTION=""
|
|
4905
|
+
control_ledger_next_var() {
|
|
4906
|
+
local state_rel text line section=""
|
|
4907
|
+
LEDGER_NEXT=""; LEDGER_SECTION=""
|
|
4908
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
4909
|
+
[ -n "$state_rel" ] || return 1
|
|
4910
|
+
text=$(git -C "$root" show "refs/remotes/origin/$CONTROL_BRANCH:${state_rel%/}/flow_progress/${CONTROL_BRANCH}_progress.md" 2>/dev/null) \
|
|
4911
|
+
|| return 1
|
|
4912
|
+
while IFS= read -r line; do
|
|
4913
|
+
line=${line%$'\r'}
|
|
4914
|
+
case "$line" in
|
|
4915
|
+
'## '*) section=${line#'## '} ;;
|
|
4916
|
+
'- [ ] '*)
|
|
4917
|
+
LEDGER_NEXT=${line#'- [ ] '}
|
|
4918
|
+
LEDGER_SECTION="$section"
|
|
4919
|
+
return 0 ;;
|
|
4920
|
+
esac
|
|
4921
|
+
done <<<"$text"
|
|
4922
|
+
return 0
|
|
4923
|
+
}
|
|
4924
|
+
|
|
4925
|
+
# control_round_newer_than_ledger — whether origin's tip of CONTROL_BRANCH
|
|
4926
|
+
# carries a user-review round commit newer than the ledger's last change: the
|
|
4927
|
+
# round's job writes its own ledger only after that commit. 0 when it does; 1
|
|
4928
|
+
# otherwise, a failed git read included. Subjects are compared as whole lines.
|
|
4929
|
+
control_round_newer_than_ledger() {
|
|
4930
|
+
local state_rel ledger_sha subjects subject line
|
|
4931
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
4932
|
+
[ -n "$state_rel" ] || return 1
|
|
4933
|
+
ledger_sha=$(git -C "$root" log -1 --format=%H "refs/remotes/origin/$CONTROL_BRANCH" \
|
|
4934
|
+
-- "${state_rel%/}/flow_progress/${CONTROL_BRANCH}_progress.md" 2>/dev/null) || return 1
|
|
4935
|
+
[ -n "$ledger_sha" ] || return 1
|
|
4936
|
+
subjects=$(git -C "$root" log --format=%s "$ledger_sha..refs/remotes/origin/$CONTROL_BRANCH" 2>/dev/null) \
|
|
4937
|
+
|| return 1
|
|
4938
|
+
subject=$(hr_user_review_subject "$CONTROL_BRANCH")
|
|
4939
|
+
while IFS= read -r line; do
|
|
4940
|
+
[ "$line" != "$subject" ] || return 0
|
|
4941
|
+
done <<<"$subjects"
|
|
4942
|
+
return 1
|
|
4943
|
+
}
|
|
4944
|
+
|
|
4945
|
+
# control_status — the read-only reply: the state, the next ledger entry, the
|
|
4946
|
+
# open questions and the latest run. Sets no label and dispatches nothing.
|
|
4947
|
+
control_status() {
|
|
4948
|
+
local text v questions="" para=$'\n\n'
|
|
4949
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4950
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4951
|
+
control_state_word_var
|
|
4952
|
+
if [ "$CS_STATE" = none ]; then
|
|
4953
|
+
text="@$CONTROL_ACTOR: no harness run is listed for \`$CONTROL_BRANCH\`."
|
|
4954
|
+
else
|
|
4955
|
+
text="@$CONTROL_ACTOR: \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`${CS_REASON:+ (\`$CS_REASON\`)}"
|
|
4956
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
4957
|
+
case "$CS_STATE" in
|
|
4958
|
+
parked) text="$text; it was parked, waiting for an answer" ;;
|
|
4959
|
+
park_loop) text="$text; it was held by the park-loop guard" ;;
|
|
4960
|
+
running) text="$text; its cancelled job is still finishing" ;;
|
|
4961
|
+
esac
|
|
4962
|
+
fi
|
|
4963
|
+
# An expired bundle is reported as expired, never as absent.
|
|
4964
|
+
[ "$CS_REASON" != expired ] || text="$text: ${CS_DETAIL:-the state bundle of the run has expired}"
|
|
4965
|
+
text="$text."
|
|
4966
|
+
fi
|
|
4967
|
+
if control_ledger_next_var; then
|
|
4968
|
+
if [ -n "$LEDGER_NEXT" ]; then
|
|
4969
|
+
text="$text${para}Next in the flow-progress ledger: $LEDGER_NEXT${LEDGER_SECTION:+ (under \`$LEDGER_SECTION\`)}"
|
|
4970
|
+
elif control_round_newer_than_ledger; then
|
|
4971
|
+
text="$text${para}A user-review round has started on \`$CONTROL_BRANCH\`, and its flow-progress ledger is not written yet."
|
|
4972
|
+
elif [ "$CS_WORD" = running ]; then
|
|
4973
|
+
text="$text${para}The run is still running, and its flow-progress ledger has no open entry: it is finishing its last step, or a new stage has not written its ledger yet."
|
|
4974
|
+
else
|
|
4975
|
+
text="$text${para}Every entry of the flow-progress ledger is ticked."
|
|
4976
|
+
fi
|
|
4977
|
+
else
|
|
4978
|
+
text="$text${para}The flow-progress ledger could not be read, so its next entry is left out."
|
|
4979
|
+
fi
|
|
4980
|
+
if [ -n "$CS_OPEN" ]; then
|
|
4981
|
+
for v in $CS_OPEN; do
|
|
4982
|
+
questions="$questions${questions:+, }$v (\`$COMMAND_HANDLE answer $v\`)"
|
|
4983
|
+
done
|
|
4984
|
+
text="$text${para}Open questions: $questions, each answered by its own comment with the answer on the lines below the command."
|
|
4985
|
+
fi
|
|
4986
|
+
[ -z "$CS_URL" ] || text="$text${para}Latest run: $CS_URL"
|
|
4987
|
+
control_reply "$EXIT_OK" "$text"
|
|
4988
|
+
}
|
|
4989
|
+
|
|
4466
4990
|
# control_answer — one answer, one `resume: answer` dispatch with one entry. A
|
|
4467
4991
|
# park left partly answered is safe: run_job stops an `answer` job whose park
|
|
4468
4992
|
# is not fully answered before any session, and its bundle then carries the
|
|
4469
4993
|
# `answer_<n>.md` restore wrote, so the next answer's job finds the set complete.
|
|
4470
4994
|
control_answer() {
|
|
4471
|
-
local first short="" below="" text n="" v open_list="" rest="" cmds="" route form dir out status="$EXIT_OK"
|
|
4995
|
+
local first short="" below="" text n="" v open_list="" rest="" cmds="" route form dir out status="$EXIT_OK" named
|
|
4472
4996
|
first=${CONTROL_ARGS%%[$' \t']*}
|
|
4473
4997
|
if [[ "$first" =~ ^[1-9][0-9]*$ ]]; then
|
|
4474
4998
|
n="$first"
|
|
@@ -4494,6 +5018,14 @@ control_answer() {
|
|
|
4494
5018
|
|
|
4495
5019
|
control_state_var "$CONTROL_BRANCH" \
|
|
4496
5020
|
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
5021
|
+
control_state_word_var
|
|
5022
|
+
# On a stopped parked run the answer dispatch is its resume; any other
|
|
5023
|
+
# stopped state is refused naming `stopped`.
|
|
5024
|
+
if [ "$CS_STOPPED" = 1 ] && [ "$CS_STATE" != parked ]; then
|
|
5025
|
+
control_stopped_refuse
|
|
5026
|
+
fi
|
|
5027
|
+
named="\`$CS_WORD\`"
|
|
5028
|
+
[ "$CS_STOPPED" != 1 ] || named="$named (it was parked)"
|
|
4497
5029
|
case "$CS_STATE:$CS_REASON" in
|
|
4498
5030
|
park_loop:*)
|
|
4499
5031
|
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard, not waiting for an answer" \
|
|
@@ -4510,11 +5042,11 @@ control_answer() {
|
|
|
4510
5042
|
;;
|
|
4511
5043
|
parked:*) ;;
|
|
4512
5044
|
*)
|
|
4513
|
-
control_refuse "$EXIT_REFUSED" "only a parked run can be answered, and the run on \`$CONTROL_BRANCH\` is \`${
|
|
5045
|
+
control_refuse "$EXIT_REFUSED" "only a parked run can be answered, and the run on \`$CONTROL_BRANCH\` is \`${CS_WORD:-unknown}\`" \
|
|
4514
5046
|
"Nothing is waiting for an answer." ;;
|
|
4515
5047
|
esac
|
|
4516
5048
|
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
|
|
5049
|
+
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
5050
|
"Nothing is waiting for an answer."
|
|
4519
5051
|
fi
|
|
4520
5052
|
for v in $CS_OPEN; do
|
|
@@ -4540,7 +5072,7 @@ control_answer() {
|
|
|
4540
5072
|
route=${route#or from GitHub: }
|
|
4541
5073
|
form="resume answer and answers \`{\"$n\": \"<the answer>\"}\`"
|
|
4542
5074
|
route=${route/and resume pause/$form}
|
|
4543
|
-
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (
|
|
5075
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` ($named) records no engine, and the harness does not guess one" \
|
|
4544
5076
|
"Answer it with the **Run workflow** form instead: $route."
|
|
4545
5077
|
fi
|
|
4546
5078
|
|
|
@@ -4579,10 +5111,18 @@ control_answer() {
|
|
|
4579
5111
|
done
|
|
4580
5112
|
if [ -n "$rest" ]; then
|
|
4581
5113
|
# The label stays `parked` until the last answer: only that job resumes.
|
|
5114
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
5115
|
+
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."
|
|
5116
|
+
fi
|
|
4582
5117
|
control_reply "$EXIT_OK" "Answer to question $n received from @$CONTROL_ACTOR and sent; question(s) $rest still need an answer: $cmds."
|
|
4583
5118
|
fi
|
|
4584
|
-
|
|
4585
|
-
|
|
5119
|
+
if [ "$CS_STOPPED" = 1 ]; then
|
|
5120
|
+
control_post "Answer to question $n received from @$CONTROL_ACTOR; every open question is answered, so the stopped run on \`$CONTROL_BRANCH\` resumes." \
|
|
5121
|
+
|| status="$EXIT_GH"
|
|
5122
|
+
else
|
|
5123
|
+
control_post "Answer to question $n received from @$CONTROL_ACTOR; every open question is answered, so \`$CONTROL_BRANCH\` resumes." \
|
|
5124
|
+
|| status="$EXIT_GH"
|
|
5125
|
+
fi
|
|
4586
5126
|
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
4587
5127
|
forge_pr_var "$CONTROL_BRANCH" || FORGE_PR=""
|
|
4588
5128
|
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" running || :
|
|
@@ -4613,7 +5153,9 @@ control_review_story() {
|
|
|
4613
5153
|
# `branch`'s pull request <pr_number>, shared by `control` and `collect`: every
|
|
4614
5154
|
# submitted review (never `PENDING`) that requests changes or carries a
|
|
4615
5155
|
# non-blank body, and every inline comment, no earlier round consumed, by
|
|
4616
|
-
# every author `authorise_actor` accepts
|
|
5156
|
+
# every author `authorise_actor` accepts, the `HARNESS_RUN_ACTORS` allow-list
|
|
5157
|
+
# included; a refused author's items are dropped with one line, while a
|
|
5158
|
+
# failed permission call (status 4) fails the collection. What earlier rounds consumed is
|
|
4617
5159
|
# read from the marker lines of their files on origin's tip; with none marked,
|
|
4618
5160
|
# the boundary is the committer time of the newest round file. A comment
|
|
4619
5161
|
# belonging to a pending review is pending whatever its `created_at`: a draft
|
|
@@ -4884,18 +5426,33 @@ control_review_in_flight() {
|
|
|
4884
5426
|
state="\`$BS_STATE\`"
|
|
4885
5427
|
[ -z "$BS_REASON" ] || state="$state (\`$BS_REASON\`)"
|
|
4886
5428
|
fi
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
way=" The run
|
|
4895
|
-
|
|
4896
|
-
way="
|
|
4897
|
-
|
|
4898
|
-
|
|
5429
|
+
if control_branch_stopped "$BS_STATE"; then
|
|
5430
|
+
# A stopped run continues only by the command its underlying state takes.
|
|
5431
|
+
state="\`stopped\`"
|
|
5432
|
+
case "$BS_STATE" in
|
|
5433
|
+
parked)
|
|
5434
|
+
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." ;;
|
|
5435
|
+
park_loop)
|
|
5436
|
+
way=" The run was stopped while held by the park-loop guard: comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
5437
|
+
running)
|
|
5438
|
+
way=" Its cancelled job is still finishing: comment \`$COMMAND_HANDLE resume\` once it has ended." ;;
|
|
5439
|
+
*)
|
|
5440
|
+
way=" Comment \`$COMMAND_HANDLE resume\` to resume the run from its committed ledger." ;;
|
|
5441
|
+
esac
|
|
5442
|
+
else
|
|
5443
|
+
case "$BS_STATE" in
|
|
5444
|
+
parked)
|
|
5445
|
+
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." ;;
|
|
5446
|
+
park_loop)
|
|
5447
|
+
way=" The run is held by the park-loop guard: comment \`$COMMAND_HANDLE clear\` to release the hold." ;;
|
|
5448
|
+
paused)
|
|
5449
|
+
if [ "$BS_REASON" = usage ]; then
|
|
5450
|
+
way=" The run resumes by itself once the usage limit resets."
|
|
5451
|
+
else
|
|
5452
|
+
way=" Comment \`$COMMAND_HANDLE resume\` to resume the run from its committed ledger."
|
|
5453
|
+
fi ;;
|
|
5454
|
+
esac
|
|
5455
|
+
fi
|
|
4899
5456
|
branch="$CONTROL_BRANCH"
|
|
4900
5457
|
if round_markers_read "$REVIEW_ID" && [ -n "$RC_EVENT_ROUND" ]; then
|
|
4901
5458
|
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: your review is part of round $RC_EVENT_ROUND, which is $state on \`$CONTROL_BRANCH\`."
|
|
@@ -5054,14 +5611,194 @@ control_comment_intake() {
|
|
|
5054
5611
|
return 0
|
|
5055
5612
|
}
|
|
5056
5613
|
|
|
5614
|
+
# The close: CLOSE_KIND (`issue`, `pr_closed`, `pr_merged`, `deleted`) and
|
|
5615
|
+
# CLOSE_REF, the event's own branch (a pull request's head, a deleted ref;
|
|
5616
|
+
# empty for an issue). CONTROL_NUMBER is the item, empty for a deletion.
|
|
5617
|
+
CLOSE_KIND=""
|
|
5618
|
+
CLOSE_REF=""
|
|
5619
|
+
|
|
5620
|
+
control_event_unreadable() {
|
|
5621
|
+
echo "remote-run.sh: control: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
5622
|
+
exit "$EXIT_USAGE"
|
|
5623
|
+
}
|
|
5624
|
+
|
|
5625
|
+
# control_issues_intake — read an `issues` event; 1 when ignored, after one line.
|
|
5626
|
+
control_issues_intake() {
|
|
5627
|
+
local action
|
|
5628
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5629
|
+
&& event_field '.issue.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
5630
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5631
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5632
|
+
if [ "$action" != closed ]; then
|
|
5633
|
+
echo "remote-run.sh: control: ignored, an issue $action, not closed"
|
|
5634
|
+
return 1
|
|
5635
|
+
fi
|
|
5636
|
+
CLOSE_KIND=issue
|
|
5637
|
+
CONTROL_VERB=close
|
|
5638
|
+
return 0
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
# control_pull_request_intake — read a `pull_request` event; 1 when ignored,
|
|
5642
|
+
# after one line.
|
|
5643
|
+
control_pull_request_intake() {
|
|
5644
|
+
local action merged head_repo
|
|
5645
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5646
|
+
&& event_field '.pull_request.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
5647
|
+
&& event_field '.pull_request.merged // false' && merged="$EVENT_VALUE" \
|
|
5648
|
+
&& event_field '.pull_request.head.ref // ""' && CLOSE_REF="$EVENT_VALUE" \
|
|
5649
|
+
&& event_field '.pull_request.head.repo.full_name // ""' && head_repo="$EVENT_VALUE" \
|
|
5650
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5651
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5652
|
+
if [ "$action" != closed ]; then
|
|
5653
|
+
echo "remote-run.sh: control: ignored, a pull request $action, not closed"
|
|
5654
|
+
return 1
|
|
5655
|
+
fi
|
|
5656
|
+
if [ -z "$head_repo" ] || [ "$head_repo" != "${GITHUB_REPOSITORY-}" ]; then
|
|
5657
|
+
echo "remote-run.sh: control: ignored, a pull request from ${head_repo:-an unnamed repository}, not ${GITHUB_REPOSITORY:-this repository}"
|
|
5658
|
+
return 1
|
|
5659
|
+
fi
|
|
5660
|
+
CLOSE_KIND=pr_closed
|
|
5661
|
+
[ "$merged" != true ] || CLOSE_KIND=pr_merged
|
|
5662
|
+
CONTROL_VERB=close
|
|
5663
|
+
return 0
|
|
5664
|
+
}
|
|
5665
|
+
|
|
5666
|
+
# control_delete_intake — read a `delete` event; 1 when ignored, after one line.
|
|
5667
|
+
control_delete_intake() {
|
|
5668
|
+
local ref_type
|
|
5669
|
+
{ event_field '.ref // ""' && CLOSE_REF="$EVENT_VALUE" \
|
|
5670
|
+
&& event_field '.ref_type // ""' && ref_type="$EVENT_VALUE" \
|
|
5671
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5672
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || control_event_unreadable
|
|
5673
|
+
if [ "$ref_type" != branch ]; then
|
|
5674
|
+
echo "remote-run.sh: control: ignored, a deleted ${ref_type:-ref}, not a branch"
|
|
5675
|
+
return 1
|
|
5676
|
+
fi
|
|
5677
|
+
if ! valid_branch "$CLOSE_REF"; then
|
|
5678
|
+
echo "remote-run.sh: control: ignored, a deleted branch with no valid name"
|
|
5679
|
+
return 1
|
|
5680
|
+
fi
|
|
5681
|
+
CLOSE_KIND=deleted
|
|
5682
|
+
CONTROL_VERB=close
|
|
5683
|
+
return 0
|
|
5684
|
+
}
|
|
5685
|
+
|
|
5686
|
+
# control_close_ignore <reason> — the one line every close refusal is, and exit
|
|
5687
|
+
# 0: a close is never replied to, labelled or dispatched for.
|
|
5688
|
+
control_close_ignore() {
|
|
5689
|
+
echo "remote-run.sh: control: close ignored: $1"
|
|
5690
|
+
exit "$EXIT_OK"
|
|
5691
|
+
}
|
|
5692
|
+
|
|
5693
|
+
# control_close — THE CLOSE (the header): the quiet gates in order, the state
|
|
5694
|
+
# rule, then the `stop` child with the event's note.
|
|
5695
|
+
control_close() {
|
|
5696
|
+
local status=0 protected=0 b="" what note out
|
|
5697
|
+
local stop_flags=()
|
|
5698
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5699
|
+
control_close_ignore "the repository variable HARNESS_REMOTE_STOP is set"
|
|
5700
|
+
fi
|
|
5701
|
+
forge_on || control_close_ignore "the default branch's harness.config.json does not turn run control on"
|
|
5702
|
+
if [ "$CLOSE_KIND" = deleted ]; then
|
|
5703
|
+
# Deleting a branch already needs write access; only a bot is screened,
|
|
5704
|
+
# never HARNESS_RUN_ACTORS (the header, THE CLOSE gate 3).
|
|
5705
|
+
if [ "$CONTROL_SENDER_TYPE" = Bot ] && ! trigger_bot_listed "$CONTROL_ACTOR"; then
|
|
5706
|
+
control_close_ignore "@$CONTROL_ACTOR is a bot not listed in HARNESS_TRIGGER_ALLOWED_BOTS"
|
|
5707
|
+
fi
|
|
5708
|
+
else
|
|
5709
|
+
case "$CONTROL_NUMBER" in
|
|
5710
|
+
''|*[!0-9]*|0*)
|
|
5711
|
+
echo "remote-run.sh: control: the event carries no issue number" >&2
|
|
5712
|
+
exit "$EXIT_USAGE" ;;
|
|
5713
|
+
esac
|
|
5714
|
+
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
|
|
5715
|
+
case "$status" in
|
|
5716
|
+
0) ;;
|
|
5717
|
+
4)
|
|
5718
|
+
echo "::error::remote-run.sh: control: the close by @$CONTROL_ACTOR was not acted on: ${AUTH_WHY%.}"
|
|
5719
|
+
exit "$EXIT_GH" ;;
|
|
5720
|
+
*) control_close_ignore "@$CONTROL_ACTOR is not authorised: ${AUTH_WHY%.}" ;;
|
|
5721
|
+
esac
|
|
5722
|
+
fi
|
|
5723
|
+
|
|
5724
|
+
case "$CLOSE_KIND" in
|
|
5725
|
+
issue)
|
|
5726
|
+
if ! forge_repo_var; then
|
|
5727
|
+
echo "::error::remote-run.sh: control: the repository's name could not be read ($GH_ERR)"
|
|
5728
|
+
exit "$EXIT_GH"
|
|
5729
|
+
fi
|
|
5730
|
+
if ! control_issue_branch_var "$CONTROL_NUMBER"; then
|
|
5731
|
+
if [ -n "$ISSUE_BRANCH_ERR" ]; then
|
|
5732
|
+
echo "::error::remote-run.sh: control: the comments of issue #$CONTROL_NUMBER could not be read ($ISSUE_BRANCH_ERR)"
|
|
5733
|
+
exit "$EXIT_GH"
|
|
5734
|
+
fi
|
|
5735
|
+
control_close_ignore "no harness run was started from issue #$CONTROL_NUMBER"
|
|
5736
|
+
fi
|
|
5737
|
+
b="$ISSUE_BRANCH" ;;
|
|
5738
|
+
*) b="$CLOSE_REF" ;;
|
|
5739
|
+
esac
|
|
5740
|
+
if ! valid_branch "$b" || ! git check-ref-format --branch "$b" >/dev/null 2>&1; then
|
|
5741
|
+
control_close_ignore "\`$b\` is not a valid branch name"
|
|
5742
|
+
fi
|
|
5743
|
+
hr_branch_is_protected "$root" "$b" || protected=$?
|
|
5744
|
+
case "$protected" in
|
|
5745
|
+
1) ;;
|
|
5746
|
+
0) control_close_ignore "\`$b\` is a protected branch" ;;
|
|
5747
|
+
*) control_close_ignore "whether \`$b\` is protected could not be judged from harness.config.json" ;;
|
|
5748
|
+
esac
|
|
5749
|
+
CONTROL_BRANCH="$b"
|
|
5750
|
+
if [ "$CLOSE_KIND" = pr_closed ] || [ "$CLOSE_KIND" = pr_merged ]; then
|
|
5751
|
+
# GitHub closes a pull request whose head is deleted; the `delete` event's
|
|
5752
|
+
# own job stops that run from the default branch, so this one stays quiet.
|
|
5753
|
+
remote_branch_exists "$b"
|
|
5754
|
+
case $? in
|
|
5755
|
+
1) control_close_ignore "the branch \`$b\` of pull request #$CONTROL_NUMBER is gone from origin; the deletion's own job stops the run" ;;
|
|
5756
|
+
2) echo "remote-run.sh: control: whether \`$b\` exists on origin could not be checked ($REMOTE_BRANCH_ERR); proceeding" ;;
|
|
5757
|
+
esac
|
|
5758
|
+
fi
|
|
5759
|
+
|
|
5760
|
+
# 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.
|
|
5762
|
+
if ! control_state_var "$b"; then
|
|
5763
|
+
echo "::error::remote-run.sh: control: the state of the run on \`$b\` could not be read ($CS_ERR)"
|
|
5764
|
+
exit "$EXIT_GH"
|
|
5765
|
+
fi
|
|
5766
|
+
case "$CS_STATE" in
|
|
5767
|
+
running|parked|park_loop|paused) ;;
|
|
5768
|
+
*) control_close_ignore "left alone: the run on \`$b\` is \`${CS_STATE:-unknown}\`" ;;
|
|
5769
|
+
esac
|
|
5770
|
+
control_branch_stopped "$CS_STATE" && control_close_ignore "the run on \`$b\` is already stopped"
|
|
5771
|
+
|
|
5772
|
+
case "$CLOSE_KIND" in
|
|
5773
|
+
issue) what="closed issue #$CONTROL_NUMBER" ;;
|
|
5774
|
+
pr_closed) what="closed pull request #$CONTROL_NUMBER"; stop_flags=(--pr "$CONTROL_NUMBER") ;;
|
|
5775
|
+
pr_merged) what="merged pull request #$CONTROL_NUMBER"; stop_flags=(--pr "$CONTROL_NUMBER") ;;
|
|
5776
|
+
deleted) what="deleted the branch \`$b\`"; stop_flags=(--branch-gone) ;;
|
|
5777
|
+
esac
|
|
5778
|
+
note="Stopped because @$CONTROL_ACTOR $what."
|
|
5779
|
+
if [ "$CLOSE_KIND" != deleted ]; then
|
|
5780
|
+
note="$note Its workflow runs and their \`$STATE_ARTIFACT_NAME\` artifacts are kept."
|
|
5781
|
+
fi
|
|
5782
|
+
echo "remote-run.sh: control: close of $b: @$CONTROL_ACTOR $what"
|
|
5783
|
+
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
5784
|
+
control_child "$out" stop "$b" --actor "$CONTROL_ACTOR" --note "$note" ${stop_flags[@]+"${stop_flags[@]}"} --repo "$root"
|
|
5785
|
+
[ "$out" = /dev/null ] || { cat "$out"; rm -f "$out"; }
|
|
5786
|
+
if [ "$CHILD_STATUS" -ne 0 ]; then
|
|
5787
|
+
echo "::error::remote-run.sh: control: the stop of \`$b\` after @$CONTROL_ACTOR $what failed: $CHILD_LAST"
|
|
5788
|
+
exit "$EXIT_GH"
|
|
5789
|
+
fi
|
|
5790
|
+
exit "$EXIT_OK"
|
|
5791
|
+
}
|
|
5792
|
+
|
|
5057
5793
|
verb_control() {
|
|
5058
5794
|
local LC_ALL=C
|
|
5059
|
-
local review=0 forge="" target="" status verbs="" v
|
|
5795
|
+
local review=0 close=0 forge="" target="" status verbs="" v
|
|
5060
5796
|
case "${GITHUB_EVENT_NAME-}" in
|
|
5061
5797
|
issue_comment) ;;
|
|
5062
5798
|
pull_request_review) review=1 ;;
|
|
5799
|
+
issues|pull_request|delete) close=1 ;;
|
|
5063
5800
|
*)
|
|
5064
|
-
echo "remote-run.sh: control handles GITHUB_EVENT_NAME issue_comment or
|
|
5801
|
+
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
5802
|
exit "$EXIT_USAGE" ;;
|
|
5066
5803
|
esac
|
|
5067
5804
|
if [ -z "${GITHUB_EVENT_PATH-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ] || [ ! -r "$GITHUB_EVENT_PATH" ]; then
|
|
@@ -5070,18 +5807,22 @@ verb_control() {
|
|
|
5070
5807
|
fi
|
|
5071
5808
|
hr_have_jq || { echo "remote-run.sh: control needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
5072
5809
|
|
|
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" ;;
|
|
5810
|
+
case "$GITHUB_EVENT_NAME" in
|
|
5811
|
+
pull_request_review) control_review_intake || return 0 ;;
|
|
5812
|
+
issue_comment) control_comment_intake || return 0 ;;
|
|
5813
|
+
issues) control_issues_intake || return 0 ;;
|
|
5814
|
+
pull_request) control_pull_request_intake || return 0 ;;
|
|
5815
|
+
delete) control_delete_intake || return 0 ;;
|
|
5083
5816
|
esac
|
|
5084
5817
|
|
|
5818
|
+
if [ "$close" -eq 0 ]; then
|
|
5819
|
+
case "$CONTROL_NUMBER" in
|
|
5820
|
+
''|*[!0-9]*|0*)
|
|
5821
|
+
echo "remote-run.sh: control: the event carries no issue number" >&2
|
|
5822
|
+
exit "$EXIT_USAGE" ;;
|
|
5823
|
+
esac
|
|
5824
|
+
fi
|
|
5825
|
+
|
|
5085
5826
|
control_tmp="${RUNNER_TEMP-}"
|
|
5086
5827
|
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5087
5828
|
control_tmp=$(mktemp -d) || { echo "remote-run.sh: control: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
@@ -5089,6 +5830,9 @@ verb_control() {
|
|
|
5089
5830
|
fi
|
|
5090
5831
|
trap control_cleanup EXIT
|
|
5091
5832
|
|
|
5833
|
+
# Decided by the event name, never CONTROL_VERB: a comment can name `close`.
|
|
5834
|
+
[ "$close" -eq 0 ] || control_close
|
|
5835
|
+
|
|
5092
5836
|
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5093
5837
|
control_refuse "$EXIT_REFUSED" "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command" \
|
|
5094
5838
|
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
@@ -5101,11 +5845,16 @@ verb_control() {
|
|
|
5101
5845
|
"Set both keys on the default branch, then comment again."
|
|
5102
5846
|
fi
|
|
5103
5847
|
|
|
5848
|
+
if ! rerun_actor_listed; then
|
|
5849
|
+
control_refuse "$EXIT_REFUSED" "${RUN_ACTORS_WHY%.}" \
|
|
5850
|
+
"Only a person the repository variable \`HARNESS_RUN_ACTORS\` admits may re-run this job; one of them can comment again."
|
|
5851
|
+
fi
|
|
5852
|
+
|
|
5104
5853
|
status=0
|
|
5105
5854
|
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
|
|
5106
5855
|
if [ "$status" -ne 0 ]; then
|
|
5107
5856
|
control_refuse "$EXIT_REFUSED" "${AUTH_WHY%.}" \
|
|
5108
|
-
"Only a collaborator with write, maintain or admin access, or a bot listed in
|
|
5857
|
+
"Only a collaborator with write, maintain or admin access whom the repository variable \`HARNESS_RUN_ACTORS\` admits (when unset, the owner alone of a repository a personal account owns, and nobody in an organisation-owned one), or a bot listed in \`HARNESS_TRIGGER_ALLOWED_BOTS\`, commands a run."
|
|
5109
5858
|
fi
|
|
5110
5859
|
|
|
5111
5860
|
if [ "$review" -eq 0 ] && { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then
|
|
@@ -5133,6 +5882,7 @@ verb_control() {
|
|
|
5133
5882
|
stop) control_stop ;;
|
|
5134
5883
|
resume) control_resume ;;
|
|
5135
5884
|
clear) control_clear ;;
|
|
5885
|
+
status) control_status ;;
|
|
5136
5886
|
review) control_review ;;
|
|
5137
5887
|
esac
|
|
5138
5888
|
}
|