autonomous-sdlc-harness 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/init.js +17 -8
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +27 -5
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +54 -11
- package/dist/config/model.js.map +1 -1
- package/dist/core/pluginIdentity.js +2 -0
- package/dist/core/pluginIdentity.js.map +1 -1
- package/dist/core/writer.js +1 -0
- package/dist/core/writer.js.map +1 -1
- package/dist/doctor/checks.js +317 -40
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +21 -11
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/remote/githubActions.js +63 -4
- package/dist/remote/githubActions.js.map +1 -1
- package/dist/retrieval/pythonBackend.js +114 -0
- package/dist/retrieval/pythonBackend.js.map +1 -0
- package/dist/retrieval/setup.js +8 -0
- package/dist/retrieval/setup.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/github/workflows/harness-control.yml +184 -0
- package/templates/github/workflows/harness-resume.yml +7 -0
- package/templates/github/workflows/harness-run.yml +100 -1
- package/templates/github/workflows/harness-trigger.yml +5 -4
- package/templates/scripts/README.md +1 -1
- package/templates/scripts/autonomous-watcher.sh +29 -2
- package/templates/scripts/docs-search-server.sh +88 -17
- package/templates/scripts/lib/harness-run-lib.sh +205 -26
- package/templates/scripts/remote-run.sh +2617 -150
- package/templates/scripts/scratch-run.sh +54 -73
- package/templates/state-dir/README-root.md +1 -1
- package/templates/state-dir/scratch/README.md +4 -2
- package/templates/state-dir/user_reviews/README.md +2 -2
|
@@ -12,7 +12,7 @@
|
|
|
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> [--repo <root>]
|
|
15
|
+
# remote-run.sh stop <branch> [--actor <login>] [--repo <root>]
|
|
16
16
|
# remote-run.sh status <branch> [--repo <root>]
|
|
17
17
|
# remote-run.sh sync <branch> [--repo <root>]
|
|
18
18
|
# remote-run.sh fetch <branch> <out_dir> [--repo <root>]
|
|
@@ -23,42 +23,59 @@
|
|
|
23
23
|
# remote-run.sh pause-requested <branch> <since_epoch> [--repo <root>]
|
|
24
24
|
# remote-run.sh run-created-at <run_id> [--repo <root>]
|
|
25
25
|
# remote-run.sh start <branch> --prompt-file <file> [--repo <root>]
|
|
26
|
-
# remote-run.sh review <branch> --review-file <file> [--
|
|
26
|
+
# remote-run.sh review <branch> --review-file <file> [--allow-no-run]
|
|
27
|
+
# [--actor <login>] [--reviewers <login,login,...>]
|
|
28
|
+
# [--source <https-url>] [--repo <root>]
|
|
27
29
|
# remote-run.sh trigger [--repo <root>] (its own exit map: its paragraph)
|
|
28
30
|
# remote-run.sh list [--repo <root>]
|
|
31
|
+
# remote-run.sh discard <dir> [--repo <root>]
|
|
32
|
+
# remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]
|
|
33
|
+
# (always 0, 1 only on a usage error: its paragraph)
|
|
34
|
+
# remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]
|
|
35
|
+
# (always 0, 1 only on a usage error: its paragraph)
|
|
36
|
+
# remote-run.sh collect <branch> [--pr <n>] [--repo <root>]
|
|
37
|
+
# (always 0, 1 only on a usage error: its paragraph)
|
|
38
|
+
# remote-run.sh control [--repo <root>] (its own exit map: its paragraph)
|
|
29
39
|
# 0 sent (for stop: the action=stop marker was dispatched, and every
|
|
30
40
|
# queued, waiting or in-progress `harness run` run of that branch was
|
|
31
41
|
# asked to cancel, or there was none); for status and fetch: printed
|
|
32
42
|
# (for fetch, `state: none` included); for sync: the record is
|
|
33
43
|
# current (including "no run listed yet", which writes nothing); for
|
|
34
|
-
# restore: restored, or no previous bundle
|
|
35
|
-
# `::warning::` line) under
|
|
44
|
+
# restore: restored, or no previous bundle of the branch's current
|
|
45
|
+
# lineage (or an expired one, with a `::warning::` line) under
|
|
46
|
+
# --resume none|pause;
|
|
36
47
|
# for save: ALWAYS, whatever happened; for continue: whatever it
|
|
37
48
|
# decided — every outcome a person must act on is a notification; for
|
|
38
49
|
# poll: the tick finished; for pause-requested: such a run exists; for
|
|
39
50
|
# run-created-at: printed; for review: placed, pushed and dispatched;
|
|
40
|
-
# for list: printed
|
|
51
|
+
# for list: printed; for discard: <dir> removed, or it did not exist
|
|
41
52
|
# 1 usage error, or the library or the configuration could not be
|
|
42
53
|
# resolved; for fetch, <out_dir> is not an existing, empty directory;
|
|
54
|
+
# for discard, <dir>'s parent does not resolve or the removal failed;
|
|
43
55
|
# for sync and restore, a local copy or write failed; for
|
|
44
56
|
# pause-requested, also NO such run — a caller that reads 1 as "no
|
|
45
57
|
# pause" passes arguments it has already validated
|
|
46
58
|
# 2 refused, nothing sent or written: execution.target is not
|
|
47
59
|
# github-actions (sending verbs, fetch, review and list); for
|
|
48
60
|
# review, a protected branch, a review file that is not a readable
|
|
49
|
-
# regular file, or a
|
|
61
|
+
# regular file, or a branch its settledness test reads as not settled
|
|
62
|
+
# (its paragraph); for sync, the
|
|
50
63
|
# branch's local record does not carry `execution: github-actions`; for
|
|
51
64
|
# status, a local record that does not carry `execution:
|
|
52
65
|
# github-actions`, or no record and `execution.target` not
|
|
53
66
|
# `github-actions`; the record's mirror working copy is missing, or a
|
|
54
67
|
# downloaded bundle is unrecognised (sync, restore, and status with no
|
|
55
68
|
# local record); the inputs payload is over the limit; a named answer
|
|
56
|
-
# file is missing
|
|
57
|
-
#
|
|
69
|
+
# file is missing (a relative --answers-from resolves against the
|
|
70
|
+
# caller's directory). For restore under --resume answer, "nothing more":
|
|
71
|
+
# no previous bundle of the branch's current lineage, the previous
|
|
72
|
+
# bundle expired (the message names
|
|
58
73
|
# its expiry and the resume command), `HARNESS_INPUT_ANSWERS` not an object of
|
|
59
74
|
# positive-integer keys to strings, or an answer whose `question_<n>.md`
|
|
60
75
|
# is not at the top level of the previous bundle — nothing is restored
|
|
61
|
-
# and no answer is written
|
|
76
|
+
# and no answer is written. For discard, <dir> does not resolve
|
|
77
|
+
# strictly inside `<state_dir>/scratch/`, is a symlink, or exists and
|
|
78
|
+
# is not a directory; nothing removed
|
|
62
79
|
# 3 gh failed: not found, or a non-zero exit — the first line of gh's
|
|
63
80
|
# stderr is named. For poll: the listing or the disable failed. For
|
|
64
81
|
# pause-requested and run-created-at, also an answer that is not the
|
|
@@ -108,14 +125,30 @@
|
|
|
108
125
|
# no `answer_<n>.md` beside it, ascending
|
|
109
126
|
# bundle_dir: <out_dir> when a bundle was downloaded
|
|
110
127
|
#
|
|
128
|
+
# `discard` REMOVES THE DIRECTORY A COMMAND FETCHED INTO, so the command needs
|
|
129
|
+
# no recursive `rm` of its own. It removes <dir> only when the library's
|
|
130
|
+
# `hr_scratch_path_var` accepts it: strictly inside the checkout's
|
|
131
|
+
# `<state_dir>/scratch/`, not a symlink, and a directory when it exists
|
|
132
|
+
# (2 otherwise). A relative <dir>
|
|
133
|
+
# resolves against the caller's directory; the root is `--repo`, or else
|
|
134
|
+
# `hr_repo_root` of the working directory, as for `restore`. No `gh` call and
|
|
135
|
+
# no `execution.target` gate. A <dir> that does not exist is exit 0. It
|
|
136
|
+
# creates nothing and writes nothing else.
|
|
137
|
+
#
|
|
111
138
|
# `review` PLACES A USER REVIEW ROUND ON THE BRANCH TIP AND DISPATCHES IT, for
|
|
112
139
|
# `/autonomous-sdlc-harness:branch-user-review` on a run that executes on
|
|
113
140
|
# GitHub. In order, stopping at the first failure: refuse a protected branch
|
|
114
141
|
# and a review file that is not a readable regular file (2; a relative
|
|
115
|
-
# --review-file resolves against the caller's directory); refuse a branch
|
|
116
|
-
#
|
|
117
|
-
#
|
|
118
|
-
#
|
|
142
|
+
# --review-file resolves against the caller's directory); refuse a branch that
|
|
143
|
+
# `branch_settled_var`, the settledness test `control` and `collect` share, reads as not
|
|
144
|
+
# settled (2). Its newest `harness run <branch>` run decides: none listed is
|
|
145
|
+
# settled only under --allow-no-run; a `completed` one is read by
|
|
146
|
+
# `remote_state`; any other is in flight as `running` until its `RUN_JOB_NAME`
|
|
147
|
+
# job has completed, and then read by `remote_state` as a finished run. Settled
|
|
148
|
+
# is `completed` or `failed`; `running`, `parked`, `park_loop` and `paused` are
|
|
149
|
+
# refused, an expired bundle naming its expiry. The refusal stays for this
|
|
150
|
+
# verb because a local round's file exists only on the caller's machine, so no
|
|
151
|
+
# later collection could pick it up. The bundle it reads is
|
|
119
152
|
# downloaded to `sync`'s directory, the one write a refusal makes. The copy: the main
|
|
120
153
|
# checkout's remote record's mirror when its `worktree` exists and is on the
|
|
121
154
|
# branch, never removed; otherwise `create-worktree.sh --existing
|
|
@@ -129,8 +162,25 @@
|
|
|
129
162
|
# matched, else `<branch>_review_<max+1>.md`. It is placed, committed as
|
|
130
163
|
# `hr_user_review_subject`'s `chore: add user review for <branch>` and pushed
|
|
131
164
|
# (each failure 4); the cut copy is removed; then `dispatch --engine
|
|
132
|
-
# user_review --resume none --chain 0
|
|
133
|
-
#
|
|
165
|
+
# user_review --resume none --chain 0`, after which it holds until a `harness run
|
|
166
|
+
# <branch>` run whose `headSha` is the pushed commit is listed — `run_by_sha_var`,
|
|
167
|
+
# `trigger`'s bounded lookup — so a review job serialized behind it never reads
|
|
168
|
+
# the branch as settled before GitHub lists the new run; a lookup that runs out
|
|
169
|
+
# is one `::warning::` line and still exit 0. A remote record, when one exists, is
|
|
170
|
+
# set `running` / `user_review` in one write after the dispatch. Then the round
|
|
171
|
+
# is reported as `report round`. Under --reviewers (comma-separated logins) the
|
|
172
|
+
# note is `Round <round> from pull request #<pr> by @<a>, @<b>`, <pr> read from
|
|
173
|
+
# --source's `/pull/<n>` or else the branch's open pull request, with ` (<source>)`
|
|
174
|
+
# after it under --source. Otherwise it is `Round <round>`, plus ` from <source>`
|
|
175
|
+
# under --source, and ` by @<actor>` under --actor (a login, as for `stop`),
|
|
176
|
+
# else ` from a local session`.
|
|
177
|
+
# --allow-no-run EXISTS FOR A LOCALLY EXECUTED BRANCH REVIEWED ON GITHUB: such
|
|
178
|
+
# a branch has no `harness run <branch>` run, and its round runs through
|
|
179
|
+
# `WORKFLOW_RUN_FILE` because a GitHub-started round always does. Its local
|
|
180
|
+
# record carries no `execution: github-actions`, so it is neither read as the
|
|
181
|
+
# copy nor written; the round runs remotely, and the branch's local working
|
|
182
|
+
# copy falls behind `origin/<branch>` until the maintainer fast-forwards it.
|
|
183
|
+
# The flag widens only the none-listed refusal.
|
|
134
184
|
#
|
|
135
185
|
# `trigger` IS THE GITHUB EVENT ADAPTER, the one step of the trigger
|
|
136
186
|
# workflow's job: event -> (branch, task text) -> `start`. It handles
|
|
@@ -144,12 +194,20 @@
|
|
|
144
194
|
# GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID the `gh` target and
|
|
145
195
|
# the URLs its comments name
|
|
146
196
|
# HARNESS_REMOTE_STOP non-empty: every start is refused
|
|
147
|
-
# HARNESS_TRIGGER_LABEL the trigger label; `DEFAULT_TRIGGER_LABEL`
|
|
197
|
+
# HARNESS_TRIGGER_LABEL the trigger label; when empty, `DEFAULT_TRIGGER_LABEL`
|
|
198
|
+
# or `LEGACY_TRIGGER_LABEL`, per the paragraph below
|
|
148
199
|
# HARNESS_TRIGGER_ALLOWED_BOTS comma-separated bot logins allowed to start
|
|
149
200
|
# HARNESS_TRIGGER_LOOKUP_SECS seconds between run lookups; `5` when empty.
|
|
150
201
|
# A test seam
|
|
151
202
|
# RUNNER_TEMP where the prompt snapshot is written; a `mktemp -d`
|
|
152
203
|
# directory when empty
|
|
204
|
+
# THE LEGACY LABEL. The workflow `init` now writes always passes a non-empty
|
|
205
|
+
# `HARNESS_TRIGGER_LABEL`; only the previous release's workflow, which
|
|
206
|
+
# `init --upgrade-workflows` never re-renders, passes it empty, and its `if:`
|
|
207
|
+
# already ran the job for `LEGACY_TRIGGER_LABEL`. So when it is empty either
|
|
208
|
+
# `DEFAULT_TRIGGER_LABEL` or `LEGACY_TRIGGER_LABEL` is accepted, and the comment
|
|
209
|
+
# and the label removal name the one applied; a new workflow starts on
|
|
210
|
+
# `LEGACY_TRIGGER_LABEL` only when the variable names it.
|
|
153
211
|
# An `action` other than `labeled`, or another label, is one line and exit 0
|
|
154
212
|
# with no `gh` call. Otherwise refused, in this order, each refusal one issue
|
|
155
213
|
# comment naming the reason and the way on:
|
|
@@ -167,15 +225,23 @@
|
|
|
167
225
|
# 6. a `User` whose `collaborators/<login>/permission` is not `admin` or
|
|
168
226
|
# `write` — `maintain` reads as `write` and `triage` as `read` there; a
|
|
169
227
|
# failed call is "could not confirm write access", never a pass
|
|
228
|
+
# Refusals 4 to 6 are `authorise_actor`, the one actor check `control` reuses.
|
|
170
229
|
# Then it fetches `origin <defaultBranch>` (a failure tolerated), derives the
|
|
171
|
-
# branch with `hr_derive_branch <title> issue_<number
|
|
230
|
+
# branch with `hr_derive_branch <title> issue_<number>`, passing `gh` so a name
|
|
231
|
+
# with run-workflow history counts as taken (2 or 3 refused), writes
|
|
172
232
|
# the snapshot — `# <title>`, the body's bytes, `---` and a provenance sentence
|
|
173
233
|
# naming the issue, the labeller, the label and the time — and runs `start` as a
|
|
174
|
-
# child. After a start it looks up the `harness run <branch>` run
|
|
234
|
+
# child. After a start it looks up the `harness run <branch>` run whose
|
|
235
|
+
# `headSha` is the `origin/<branch>` commit `start` pushed, at most
|
|
175
236
|
# `TRIGGER_RUN_LOOKUP_TRIES` times, falling back to the branch's filtered run
|
|
176
|
-
# list, and comments the branch and that URL
|
|
177
|
-
#
|
|
178
|
-
#
|
|
237
|
+
# list, and comments the branch and that URL; the comment never names an older
|
|
238
|
+
# run of the branch. Every comment ends with the marker
|
|
239
|
+
# `<!-- sdlc-harness event=started branch=<branch> -->` on a start and
|
|
240
|
+
# `event=refused` otherwise (`branch=` empty before one is derived), and is
|
|
241
|
+
# followed by removing the label, so re-applying it is deliberate; then a start
|
|
242
|
+
# sets the state label `sdlc-harness: running`. That comment, that removal and
|
|
243
|
+
# that one label are the trigger's only writes to the issue; a refusal sets no
|
|
244
|
+
# label. A removal or a label set that fails is one `::warning::` line.
|
|
179
245
|
# A `repository_dispatch` reads `.action` (where GitHub puts the `event_type`)
|
|
180
246
|
# and `client_payload`'s `title`, `body` and `source`, the contract being
|
|
181
247
|
# {"event_type": TRIGGER_DISPATCH_EVENT_TYPE, "client_payload": {"title": …,
|
|
@@ -199,6 +265,253 @@
|
|
|
199
265
|
# its dispatch failed (commented with the manual Run-workflow way on)
|
|
200
266
|
# 4 `start` refused or failed its placement (commented)
|
|
201
267
|
#
|
|
268
|
+
# `report` TURNS A LIFECYCLE EVENT INTO ONE COMMENT AND ONE STATE LABEL, through
|
|
269
|
+
# the forge surface (its section states the functions). Job-side for its root,
|
|
270
|
+
# as `trigger` is, and outside the sending-verb gate: it does nothing, with one
|
|
271
|
+
# line, unless `hr_forge` is `github` and `hr_execution_target` is
|
|
272
|
+
# `github-actions`. The comment goes to the open same-repository pull request
|
|
273
|
+
# whose head is <branch> when origin's <branch> carries
|
|
274
|
+
# `<state_dir>/flow_progress/<branch>_progress.md`, else to the issue named by
|
|
275
|
+
# the last `Started from <server>/<repo>/issues/<n> by @` line of its committed
|
|
276
|
+
# task prompt, else nowhere. The label `STATE_LABEL_PREFIX<state>` replaces any
|
|
277
|
+
# other state label on that issue and that pull request, each when known; the
|
|
278
|
+
# label is a view, and the run list stays the authority. The state map:
|
|
279
|
+
# `parked` and `park_loop` -> parked, `paused` -> paused, `resumed` -> running,
|
|
280
|
+
# `failed` -> failed, `stopped` -> stopped, `round` (review's) -> running.
|
|
281
|
+
# `failed` posts nothing when
|
|
282
|
+
# `remote_branch_stopped` finds the branch stopped, so a cancelled job never
|
|
283
|
+
# overwrites `stopped`. `completed` (deliver's) and `launched` (the trigger's
|
|
284
|
+
# own comment) are one line each, as is any other event. The comment names the
|
|
285
|
+
# next GitHub action — never a slash command — then <note> byte for byte, then
|
|
286
|
+
# this run's URL when `GITHUB_RUN_ID` is set, then the marker line
|
|
287
|
+
# `<!-- sdlc-harness event=<event> branch=<branch> -->`. The pause reason and
|
|
288
|
+
# reset come from the registry record, read only when the registry file exists.
|
|
289
|
+
# `parked` instead posts one comment per open question — `open_questions_in`
|
|
290
|
+
# over `$root`'s state directory, ascending — carrying `question_<n>.md` whole,
|
|
291
|
+
# cut at its last whole line within `QUESTION_COMMENT_MAX_BYTES` and then naming
|
|
292
|
+
# the file in the `STATE_ARTIFACT_NAME` artifact; then the answer form, a
|
|
293
|
+
# comment whose first line is `COMMAND_HANDLE answer <n>` (`<n>` optional when
|
|
294
|
+
# one question is open) and whose following lines are the answer; the marker
|
|
295
|
+
# adds `question=<n>`. With no question open it posts the one notice. The label
|
|
296
|
+
# is set once per target, not per question. On a public repository a question
|
|
297
|
+
# comment and its answer are public, as the artifact already is
|
|
298
|
+
# (`docs/remote-execution.md` -> `## 11. Security`, *What a reader of the
|
|
299
|
+
# repository's Actions runs can see*).
|
|
300
|
+
# It never fails its caller: every problem is one line and exit 0.
|
|
301
|
+
#
|
|
302
|
+
# `deliver` HANDS A COMPLETED RUN TO REVIEW: the run workflow's step after the
|
|
303
|
+
# job's own `push-branch.sh`, which opens no pull request. Job-side for its
|
|
304
|
+
# root and self-gated, as `report` is (`forge_on`, one line when off). Anything
|
|
305
|
+
# but `status: completed` in <bundle_dir>/status.json, or no status.json, is
|
|
306
|
+
# one line and nothing sent. Otherwise, by the forge surface: an open
|
|
307
|
+
# same-repository pull request whose head is <branch> is reused and no second
|
|
308
|
+
# one is opened; else a draft is created against `defaultBranch`, titled from
|
|
309
|
+
# the task prompt's `# ` first line (cut to `PR_TITLE_MAX_CHARS`, else
|
|
310
|
+
# <branch>), whose body names the issue as `Started from #<n>.` — a plain
|
|
311
|
+
# mention, never a closing keyword — states what a review requesting changes
|
|
312
|
+
# and the `COMMAND_HANDLE` commands do, and ends with the `pull-request`
|
|
313
|
+
# marker. The create runs with `HARNESS_PR_TOKEN` as `GH_TOKEN` when that is
|
|
314
|
+
# set, so the adopter's CI runs without an approval click; every other call
|
|
315
|
+
# uses the job's token. A pull request so opened is authored by that token's
|
|
316
|
+
# owner, who therefore cannot request changes on it: use a machine account's
|
|
317
|
+
# token, another reviewer, or a local `branch-user-review` round. A create
|
|
318
|
+
# refused with `PR_CREATE_FORBIDDEN` is not retried, and the comment names the
|
|
319
|
+
# Actions setting and `HARNESS_GIT_TOKEN`; any other failure is retried once
|
|
320
|
+
# without `--draft`, and a second failure is named in the comment with the
|
|
321
|
+
# branch's compare URL. A pull-request lookup that fails opens nothing. Then
|
|
322
|
+
# one `completed` comment: on the issue naming the new pull request's URL; on
|
|
323
|
+
# the pull request when there is no issue, or when it existed before this run
|
|
324
|
+
# (saying the round finished); on the issue alone when none could be opened;
|
|
325
|
+
# nowhere when neither is known. It names reviewing and requesting changes as
|
|
326
|
+
# the next action, and with `phases.qa` true the local `branch-qa-test` still
|
|
327
|
+
# owed. Then `sdlc-harness: done` on the issue and the pull request, each when
|
|
328
|
+
# known. It writes at most one pull request, one comment and those labels, and
|
|
329
|
+
# never pushes. It never fails its caller: every problem is one line and exit 0.
|
|
330
|
+
#
|
|
331
|
+
# `collect` STARTS THE NEXT ROUND FROM THE REVIEWS COLLECTED DURING A RUN: the
|
|
332
|
+
# one step of the run workflow's `collect` job, which follows its `run` job, so
|
|
333
|
+
# a review `control` answered "collected" waits at most for the run in flight.
|
|
334
|
+
# Job-side for its root and self-gated, as `report` and `deliver` are
|
|
335
|
+
# (`forge_on`, one line when off). In order, each stop one line and exit 0:
|
|
336
|
+
# `HARNESS_REMOTE_STOP` set; `remote_branch_stopped` finding the branch stopped,
|
|
337
|
+
# or failing; no pull request, from --pr or else `forge_pr_var` (a failed lookup
|
|
338
|
+
# included); `branch_settled_var`, read as `control` reads it, finding the
|
|
339
|
+
# branch in flight — a newer run listed, or this run ending `parked`,
|
|
340
|
+
# `park_loop` or `paused` (a budget chain's included), whose own end collects
|
|
341
|
+
# next — or failing; `round_collect` finding no review requesting changes
|
|
342
|
+
# pending (inline comments alone start no round, as *Comment* starts none; they
|
|
343
|
+
# ride along in the next), or failing, a `::warning::` line. Otherwise `review
|
|
344
|
+
# <branch> --review-file <file> --allow-no-run --reviewers <logins> --source
|
|
345
|
+
# <pull request url>` runs as a child, as `control` runs it, placing,
|
|
346
|
+
# dispatching and reporting the round. A child that exits non-zero gets exactly
|
|
347
|
+
# one comment on the pull request, with the `reply` marker, naming its last
|
|
348
|
+
# stderr line and saying the reviews stay there and that submitting a review
|
|
349
|
+
# requesting changes retries; there is no automatic retry. It never fails its
|
|
350
|
+
# caller: every outcome but a usage error is exit 0.
|
|
351
|
+
#
|
|
352
|
+
# `control` IS THE COMMENT AND REVIEW ADAPTER, the twin of `trigger` and the one
|
|
353
|
+
# step of the `WORKFLOW_CONTROL_FILE` job: one GitHub event -> one action on
|
|
354
|
+
# exactly one branch, carried out by this script's own verbs run as children,
|
|
355
|
+
# so it composes no dispatch itself. Job-side for its root, as `trigger` is,
|
|
356
|
+
# and outside the sending-verb gate: it gates itself after reading the event,
|
|
357
|
+
# so a refusal can still be replied to. The workflow's `if:` only saves a
|
|
358
|
+
# runner; every rule below holds without it. It handles `GITHUB_EVENT_NAME`
|
|
359
|
+
# `issue_comment` and `pull_request_review` (THE REVIEW, below); any other
|
|
360
|
+
# name, or an event file it cannot read, exits 1.
|
|
361
|
+
# It reads `.action`, `.comment.body`, `.issue.number`,
|
|
362
|
+
# `.issue.pull_request.url`, `.sender.login` and `.sender.type`, each by `jq`
|
|
363
|
+
# into a variable (data, never shell source), plus `trigger`'s environment.
|
|
364
|
+
# Ignored, with one line and no `gh` call: an action other than `created`; a
|
|
365
|
+
# body carrying `COMMENT_MARKER` anywhere (the harness's own comment, whoever
|
|
366
|
+
# posted it); and a body whose first line — a trailing CR stripped, leading
|
|
367
|
+
# spaces and tabs skipped — does not open with a word equal to
|
|
368
|
+
# `COMMAND_HANDLE`, compared lowercase. So `pause`, `Let's @sdlc-harness
|
|
369
|
+
# pause` and `> @sdlc-harness pause` start nothing. The verb is the next word,
|
|
370
|
+
# lowercased, and `CONTROL_ARGS` the rest of that line. Then refused, in this
|
|
371
|
+
# order, each a reply and exit 2:
|
|
372
|
+
# 1. `HARNESS_REMOTE_STOP` is set
|
|
373
|
+
# 2. `forge_on` fails — before any authorisation, so a disabled coupling asks
|
|
374
|
+
# GitHub nothing about the commenter
|
|
375
|
+
# 3. `authorise_actor` fails: `AUTH_WHY`, and who may command a run
|
|
376
|
+
# 4. the verb is empty, not a `COMMAND_VERBS` word, or one no arm carries out
|
|
377
|
+
# yet: the reply lists every command and names `docs/github-run-control.md`
|
|
378
|
+
# THE BRANCH. On a pull request (`.issue.pull_request.url` set), its head, by
|
|
379
|
+
# `pr view`: a fork's pull request is refused, because this event carries the
|
|
380
|
+
# repository's secrets, and nothing from its head is checked out or run; one
|
|
381
|
+
# not `OPEN` is refused. On an issue, the branch of the LAST genuine start
|
|
382
|
+
# comment, read by a paginated comment listing: its author is
|
|
383
|
+
# `github-actions[bot]`, its first line opens with the trigger's sentence
|
|
384
|
+
# `Started a harness run on the branch ` and a backticked <b>, and its last
|
|
385
|
+
# non-empty line is byte for byte what `forge_marker started <b>` prints. A
|
|
386
|
+
# marker quoted mid-body, in a comment not opening with that sentence, or by
|
|
387
|
+
# anyone else, is never trusted; none is a refusal. Both paths then pass
|
|
388
|
+
# `control_check_branch`: a branch `hr_branch_is_protected` does not answer 1
|
|
389
|
+
# for is refused, then it is fetched, and one whose origin tip carries no
|
|
390
|
+
# flow-progress ledger (`forge_recognised`) is not a harness branch.
|
|
391
|
+
# THE ARMS. `pause`: the state by a `fetch` child (`control_state_var`); only
|
|
392
|
+
# `running` sends `pause <branch>` as a child and replies that the run yields at
|
|
393
|
+
# its next clean checkpoint; any other state is a refusal naming it. `stop`:
|
|
394
|
+
# state `none` is a refusal; otherwise `stop <branch> --actor <login>` as a
|
|
395
|
+
# child, which posts its own `stopped` comment to the run's target; on 0 a reply
|
|
396
|
+
# is ALWAYS posted where the command was typed too, so a command on the issue of
|
|
397
|
+
# a branch with a pull request is answered there; on 3 the reply says the stop
|
|
398
|
+
# was partial and to comment `stop` again. `resume` accepts only `paused`, any
|
|
399
|
+
# `pause_reason` (`expired` and `killed` included); `park_loop` is refused
|
|
400
|
+
# pointing at `clear`, `parked` pointing at `answer <n>` with the open indexes,
|
|
401
|
+
# `running`, `completed`, `failed` and `none` each naming the state. `clear`
|
|
402
|
+
# accepts only `park_loop`, the GitHub form of `branch-resume`'s confirmation;
|
|
403
|
+
# any other state is a refusal naming it. Each sends the local relay's dispatch,
|
|
404
|
+
# `dispatch <branch> --engine <the state's engine> --resume pause --chain 0`,
|
|
405
|
+
# `clear` with `park_loop_clear` set, and no other command sets it. An empty
|
|
406
|
+
# engine is refused, never guessed (the `engine` input defaults to `task`),
|
|
407
|
+
# naming the Run workflow form. On 0 a reply, then `running` on the run's issue
|
|
408
|
+
# and pull request; on 2 a refusal and exit 2. `answer`: the first line is
|
|
409
|
+
# `answer <n>` and the answer every line below it, a trailing CR stripped from
|
|
410
|
+
# each line and its bytes otherwise unchanged; text after <n> on the first line
|
|
411
|
+
# is the answer when nothing follows below, and with no positive-integer <n>
|
|
412
|
+
# all of that line's text is. <n> may be left out only when exactly one
|
|
413
|
+
# question is open — issue comments have no threads. Refused, each a reply and
|
|
414
|
+
# exit 2: an empty answer; `park_loop` (pointing at `clear`); `paused` /
|
|
415
|
+
# `expired`, quoting its detail and pointing at `resume`, never treated as no
|
|
416
|
+
# park; `running`, naming the run, so a second answer never queues behind a job
|
|
417
|
+
# that a newer pending run in the per-branch `concurrency` group could cancel;
|
|
418
|
+
# any state but `parked`; no open question; several open and no <n>; an <n> not
|
|
419
|
+
# open, listing the open set; an empty engine, naming the Run workflow form.
|
|
420
|
+
# Otherwise the answer is written by `printf` (data, never shell source) to
|
|
421
|
+
# `answer_<n>.md` in a fresh directory under `RUNNER_TEMP`, and sent as
|
|
422
|
+
# `dispatch <branch> --engine <engine> --resume answer --answers-from <dir>
|
|
423
|
+
# --indexes <n> --chain 0`: one answer, one dispatch with one entry. That is
|
|
424
|
+
# safe because an `answer` job whose park is not fully answered stops parked
|
|
425
|
+
# before any session (`autonomous-watcher.sh` -> `run_job`), its bundle then
|
|
426
|
+
# carrying the `answer_<n>.md` restore wrote, so the next answer's job finds the
|
|
427
|
+
# set complete. The payload limit is `dispatch`'s alone: its refusal is quoted,
|
|
428
|
+
# with shortening the answer or committing it to a file on the branch as the
|
|
429
|
+
# way on. On 0 with no other question open, a reply that the run resumes and
|
|
430
|
+
# `running` on its issue and pull request; with others open, a reply naming
|
|
431
|
+
# 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.
|
|
433
|
+
# THE REVIEW. A `pull_request_review` event reads `.action`, `.review.state`,
|
|
434
|
+
# `.review.body`, `.review.id`, `.review.html_url`, `.review.submitted_at`,
|
|
435
|
+
# `.pull_request.number`, `.pull_request.head.ref`,
|
|
436
|
+
# `.pull_request.head.repo.full_name`, `.sender.login` and `.sender.type`.
|
|
437
|
+
# Ignored, with one line and no `gh` call: an action other than `submitted`; a
|
|
438
|
+
# state other than `REVIEW_ROUND_STATE`, compared lowercase (the REST API
|
|
439
|
+
# reports it uppercase); a body carrying `COMMENT_MARKER`; and a head
|
|
440
|
+
# repository other than `GITHUB_REPOSITORY`, not even replied to, because a
|
|
441
|
+
# fork's review job holds a read-only token. So *Comment* and *Approve* start
|
|
442
|
+
# nothing, and draft status plays no part. Then gates 1-3 above, in order, each
|
|
443
|
+
# a reply on the pull request, and `control_check_branch` on the head; then a
|
|
444
|
+
# head whose origin tip carries no `<state>/story_plans/<head>_story_plan.md`
|
|
445
|
+
# is refused, because the round reads its story index. A REVIEW IS NEVER
|
|
446
|
+
# REFUSED FOR A RUN IN FLIGHT. The head is read by `branch_settled_var`, no run
|
|
447
|
+
# counting as settled, in a command substitution so a failed read is a reply
|
|
448
|
+
# (exit 3) rather than an exit with none. Not settled: a reply and exit 0,
|
|
449
|
+
# nothing pushed or dispatched — `your review is part of round <n>, which is
|
|
450
|
+
# <state>` when a marker on origin's tip records the event's review id, else
|
|
451
|
+
# `your review was collected`, naming the state and saying the next round
|
|
452
|
+
# starts by itself when that run finishes, plus the state's way on: `answer
|
|
453
|
+
# <n>` for `parked`, `clear` for `park_loop`, `resume` for any `paused` but
|
|
454
|
+
# `usage`, and for `usage` that it resumes after the reset. The review stays on
|
|
455
|
+
# the pull request, and a later round collects it. Settled: a round is CUMULATIVE,
|
|
456
|
+
# built by `round_collect` in a fresh file under `RUNNER_TEMP` from ONE
|
|
457
|
+
# paginated `pulls/<n>/reviews` and ONE paginated `pulls/<n>/comments` listing
|
|
458
|
+
# — never the per-review endpoint, which carries no `line` — the event's own
|
|
459
|
+
# review merged when the listing lacks it. What earlier rounds consumed is the
|
|
460
|
+
# union of the review and comment ids their marker lines record, read from
|
|
461
|
+
# origin's tip; the time boundary is the highest-numbered marked round's
|
|
462
|
+
# `collected_at` less `ROUND_OVERLAP_SECS`, or, when no round is marked, the
|
|
463
|
+
# committer time of the branch's newest `user_reviews/<head>_review[_<n>].md`
|
|
464
|
+
# (none when there is no round). Pending: a submitted review (never
|
|
465
|
+
# `PENDING`) whose id is unrecorded, whose body carries no `COMMENT_MARKER`,
|
|
466
|
+
# whose `submitted_at` is at or after the boundary, and which either has state
|
|
467
|
+
# `REVIEW_ROUND_STATE` or a non-blank body; and an
|
|
468
|
+
# inline comment, by any author and whatever its review's state, whose id is
|
|
469
|
+
# unrecorded, whose body carries no `COMMENT_MARKER`, and whose `created_at` is
|
|
470
|
+
# at or after the boundary or whose review is pending. Every distinct author
|
|
471
|
+
# of a pending item passes `authorise_actor`; a refused author's items are
|
|
472
|
+
# dropped with one line naming the login, `AUTH_WHY` and the count, and a
|
|
473
|
+
# failed permission call fails the collection. A round is placed only when at
|
|
474
|
+
# least one kept review has state `REVIEW_ROUND_STATE`; a *Comment* or
|
|
475
|
+
# *Approve* review rides along in the next round one requesting changes starts.
|
|
476
|
+
# The file: one `## Review by @<login>` section per pending review, oldest
|
|
477
|
+
# `submitted_at` first, holding the body verbatim (or `(The review carries no
|
|
478
|
+
# summary.)`) and a provenance line naming its state — `Requested changes on
|
|
479
|
+
# pull request #<n> (<url>) at <submitted_at>.`, `Commented on pull request
|
|
480
|
+
# …`, `Approved pull request …`, `Reviewed pull request … at <submitted_at>;
|
|
481
|
+
# the review has since been dismissed.`, or for any other state `Reviewed
|
|
482
|
+
# pull request … at <submitted_at> (state <state>).`; then, when any is
|
|
483
|
+
# kept, `## Inline comments`, oldest `created_at` first; then the marker line
|
|
484
|
+
# `<!-- sdlc-harness round collected_at=<utc> reviews=<id,…> comments=<id,…> -->`
|
|
485
|
+
# listing exactly the ids written. Each comment is a `### `<path>`, line
|
|
486
|
+
# <n>` heading (`original line <n> (outdated)` when `line` is null), `Made on
|
|
487
|
+
# commit `<original_commit_id or commit_id>`.`, `By @<login>: <url>`, its body
|
|
488
|
+
# verbatim, and its `diff_hunk` in a `diff` fence one backtick longer than the
|
|
489
|
+
# hunk's longest backtick run, at least three — the commit and hunk let the fix plan re-locate
|
|
490
|
+
# a line the fixes moved. With no review pending, nothing is placed: a reply
|
|
491
|
+
# names the round whose marker records the event's review, else says nothing
|
|
492
|
+
# is pending, and exit 0. Then `review <head> --review-file <file>
|
|
493
|
+
# --allow-no-run --reviewers <logins> --source <pull request url>` runs as a
|
|
494
|
+
# child, which fast-forwards, commits `chore: add user review for <head>`, pushes,
|
|
495
|
+
# dispatches `engine: user_review`, waits for its run to be listed and reports
|
|
496
|
+
# the round itself. Its 0 is exit 0 with nothing more posted; 2 means the branch
|
|
497
|
+
# became unsettled between the two reads, answered with the in-flight reply
|
|
498
|
+
# above (the state read again) and exit 0; 3 a reply quoting the re-send line,
|
|
499
|
+
# exit 3; 4 a reply that placement failed and nothing was dispatched, exit 4.
|
|
500
|
+
# A refusal reads `@<login>: `review` was not run: …`, and no reply to a review
|
|
501
|
+
# asks for it to be submitted again to be kept: a reply that started nothing
|
|
502
|
+
# says the reviews stay on the pull request for the next round, and that
|
|
503
|
+
# submitting a review requesting changes retries now.
|
|
504
|
+
# A child's failure is a reply
|
|
505
|
+
# naming its last stderr line, and exit 3. Every reply goes to the item the comment was
|
|
506
|
+
# typed on, opens `@<login>`, and carries the `reply` marker; a refusal reads
|
|
507
|
+
# `@<login>: `<verb>` was not run: <reason>. <way on>`.
|
|
508
|
+
# 0 handled (replied), or ignored
|
|
509
|
+
# 1 neither an `issue_comment` nor a `pull_request_review` event, or the event could not be read
|
|
510
|
+
# 2 refused (replied)
|
|
511
|
+
# 3 a `gh` step failed: the reply could not be posted (an `::error::`
|
|
512
|
+
# line), or the action failed and was replied to
|
|
513
|
+
# 4 a review's round could not be placed; nothing was dispatched (replied)
|
|
514
|
+
#
|
|
202
515
|
# `restore` AND `save` ARE THE JOB-SIDE VERBS: the run workflow calls them in
|
|
203
516
|
# its job, before and (under `always()`) after the harness step. Without
|
|
204
517
|
# `--repo` they act on the checkout of the working directory (`hr_repo_root`),
|
|
@@ -232,7 +545,21 @@
|
|
|
232
545
|
# with the exact bytes, after checking every entry first; and with
|
|
233
546
|
# `HARNESS_INPUT_PARK_LOOP_CLEAR` exactly `true` it sets `park_loop_cycles` to
|
|
234
547
|
# "0" in the restored `autonomous_logs/remote_status.json`. No bundle in any
|
|
235
|
-
# run is an ordinary first job (exit 0, one line) except under
|
|
548
|
+
# candidate run is an ordinary first job (exit 0, one line) except under
|
|
549
|
+
# --resume answer, whose refusal names the current lineage.
|
|
550
|
+
#
|
|
551
|
+
# THE CANDIDATES ARE BOUNDED TO THE BRANCH'S CURRENT LINEAGE, so a branch
|
|
552
|
+
# recreated under a reused name never restores an earlier, unrelated run's
|
|
553
|
+
# bundle. The listing reads each run's `headSha`, and `lineage_commits_var`
|
|
554
|
+
# lists `git rev-list refs/remotes/origin/<defaultBranch>..HEAD` in the job's
|
|
555
|
+
# checkout; a run whose `headSha` is not among those commits, or that carries
|
|
556
|
+
# none, is dropped before the walk, so the expired-bundle stop applies to
|
|
557
|
+
# lineage runs only. When any was dropped it prints `skipped <n> finished
|
|
558
|
+
# run(s) of <branch> from before its current lineage`. When the lineage cannot
|
|
559
|
+
# be listed — the configuration unreadable, `origin/<defaultBranch>` not
|
|
560
|
+
# present, or HEAD carrying no commit beyond it — every finished run is a
|
|
561
|
+
# candidate, as before the bound, and it prints `the lineage of <branch> is
|
|
562
|
+
# not bounded (<reason>); every finished run of it is a candidate`.
|
|
236
563
|
#
|
|
237
564
|
# `save` WRAPS `hr_remote_bundle_write` into <out_dir>, and with
|
|
238
565
|
# `GITHUB_STEP_SUMMARY` set appends a Markdown table of the bundle's `status`,
|
|
@@ -264,7 +591,8 @@
|
|
|
264
591
|
# so it names the repository rather than a runner path
|
|
265
592
|
# GITHUB_RUN_ID, GITHUB_SERVER_URL, GITHUB_REPOSITORY the run URL
|
|
266
593
|
# Notifications go through the sibling `autonomous-notify.sh`, as `paused` or
|
|
267
|
-
# `failed
|
|
594
|
+
# `failed`, and each is then reported as `report` reports that event, with a
|
|
595
|
+
# note of its own that names no slash command and no shell command. A re-dispatch is `dispatch <branch> --engine <status.json engine>
|
|
268
596
|
# --resume pause --chain <chain + 1>`, composed by `dispatch` itself.
|
|
269
597
|
#
|
|
270
598
|
# `chain` HAS ONE SOURCE: the bundle's `status.json`, whose `chain` is the
|
|
@@ -436,7 +764,7 @@
|
|
|
436
764
|
# is a wire: `pause` and `stop` send the branch as their `branch` input for
|
|
437
765
|
# exactly that reason.
|
|
438
766
|
#
|
|
439
|
-
# `stop` DOES
|
|
767
|
+
# `stop` DOES FOUR THINGS, IN THIS ORDER. (1) It ALWAYS dispatches action=stop
|
|
440
768
|
# on the branch — a jobless run titled `harness stop <branch>` that GitHub keeps
|
|
441
769
|
# as the stop marker `continue` and `poll` read. It is first because it is the
|
|
442
770
|
# only part that reaches a usage-paused run waiting on the resume poller, which
|
|
@@ -448,7 +776,12 @@
|
|
|
448
776
|
# after a failure. (3) Only when (1) and (2) all succeeded, and only when a
|
|
449
777
|
# local registry record exists, it writes `remote_stopped_at` and sets `status`
|
|
450
778
|
# to `failed` in one `hr_registry_set` call; a partial stop leaves the record alone
|
|
451
|
-
# and exits 3, so running `stop` again is the remedy.
|
|
779
|
+
# and exits 3, so running `stop` again is the remedy. (4) A complete stop is then
|
|
780
|
+
# reported as `report stopped` with the note `Stopped by @<actor>.` under
|
|
781
|
+
# `--actor` (a login, the trigger's shape plus an optional `[bot]`; anything else
|
|
782
|
+
# is a usage error), else one naming a local stop; a partial stop reports
|
|
783
|
+
# nothing. The cancelled job's own `failed` is then posted nowhere, because
|
|
784
|
+
# `report` finds the branch stopped.
|
|
452
785
|
#
|
|
453
786
|
# `warm` dispatches action=warm on GitHub's OWN default branch (`gh repo view
|
|
454
787
|
# --json defaultBranchRef`), which may differ from the configured
|
|
@@ -460,18 +793,28 @@
|
|
|
460
793
|
# root's `harness.config.json`.
|
|
461
794
|
#
|
|
462
795
|
# WHAT IT NEVER DOES. It never launches a local session, never writes the
|
|
463
|
-
# inbox, and never watches a run it sent
|
|
796
|
+
# inbox, and never watches a run it sent beyond the bounded lookup of its
|
|
797
|
+
# listing that `trigger` and `review` make. Only `start` and `review` push, and
|
|
464
798
|
# only through `create-worktree.sh` and `push-branch.sh`; `start`'s writes are
|
|
465
799
|
# the prompt committed on `origin/<branch>`, through a working copy and a local
|
|
466
800
|
# branch it removes before it returns; `review`'s are the round committed on
|
|
467
801
|
# `origin/<branch>`, through the record's mirror or a copy and a local branch
|
|
468
|
-
# it removes, the record's `status` / `engine`,
|
|
469
|
-
# directory `sync` uses. A user's chain-0 `dispatch --resume answer|pause`
|
|
802
|
+
# it removes, the record's `status` / `engine`, the bundle download
|
|
803
|
+
# directory `sync` uses, and `report`'s writes for the round. A user's chain-0 `dispatch --resume answer|pause`
|
|
470
804
|
# writes the record's `status`, `resumed_at` and `resume_kind` in one write
|
|
471
805
|
# when the main checkout's registry file exists and holds a record with
|
|
472
806
|
# `execution: github-actions`; any other `dispatch` writes nothing. `trigger` writes its snapshot and comment
|
|
473
807
|
# files under `RUNNER_TEMP`, one comment on the issue and the label removal, or
|
|
474
|
-
# for a dispatch event a block in the step summary.
|
|
808
|
+
# for a dispatch event a block in the step summary. `report` writes its comment
|
|
809
|
+
# file under `RUNNER_TEMP` (removed), one comment and the state labels on the
|
|
810
|
+
# issue and the pull request, and nothing local. `deliver` writes its body and
|
|
811
|
+
# comment files under `RUNNER_TEMP` (removed), at most one pull request, one
|
|
812
|
+
# comment and the state labels. `control` writes its reply file and its
|
|
813
|
+
# `fetch` directory under `RUNNER_TEMP` (removed) and one reply comment, plus
|
|
814
|
+
# what the child verb it runs writes. `collect` writes its round file and its
|
|
815
|
+
# settledness directory under `RUNNER_TEMP` (removed), at most one comment on
|
|
816
|
+
# the pull request, plus what its `review` child writes. `stop`, `continue` and `poll` also make
|
|
817
|
+
# `report`'s writes for each event they report. Every other verb's only writes are the
|
|
475
818
|
# registry record (`stop`, `sync`) and, for `sync`, the download directory
|
|
476
819
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
477
820
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
@@ -481,7 +824,8 @@
|
|
|
481
824
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
482
825
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
483
826
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
484
|
-
# and `current/`. For `fetch`, <out_dir> only. `
|
|
827
|
+
# and `current/`. For `fetch`, <out_dir> only. For `discard`, the removal of
|
|
828
|
+
# <dir> only. `pause-requested`,
|
|
485
829
|
# `run-created-at`, `list` and `status` write nothing; a no-record `status`
|
|
486
830
|
# downloads into a temporary directory it removes on exit.
|
|
487
831
|
#
|
|
@@ -494,8 +838,18 @@
|
|
|
494
838
|
# HARNESS_GH_CLI mirrors GH_CLI_VARIABLE (the binary run as `gh`)
|
|
495
839
|
# HARNESS_TRIGGER_LABEL mirrors TRIGGER_LABEL_VARIABLE
|
|
496
840
|
# DEFAULT_TRIGGER_LABEL mirrors DEFAULT_TRIGGER_LABEL
|
|
841
|
+
# LEGACY_TRIGGER_LABEL mirrors LEGACY_TRIGGER_LABEL
|
|
497
842
|
# HARNESS_TRIGGER_ALLOWED_BOTS mirrors TRIGGER_ALLOWED_BOTS_VARIABLE
|
|
498
843
|
# TRIGGER_DISPATCH_EVENT_TYPE mirrors TRIGGER_DISPATCH_EVENT_TYPE ('harness-task')
|
|
844
|
+
# WORKFLOW_CONTROL_FILE mirrors WORKFLOW_CONTROL_FILE
|
|
845
|
+
# COMMAND_HANDLE mirrors COMMAND_HANDLE
|
|
846
|
+
# COMMAND_VERBS mirrors COMMAND_VERBS, space-separated
|
|
847
|
+
# COMMENT_MARKER mirrors COMMENT_MARKER
|
|
848
|
+
# REVIEW_ROUND_STATE mirrors REVIEW_ROUND_STATE
|
|
849
|
+
# STATE_LABEL_PREFIX mirrors STATE_LABEL_PREFIX
|
|
850
|
+
# RUN_STATES mirrors RUN_STATES, space-separated, same order
|
|
851
|
+
# PR_CREATE_SETTING mirrors PR_CREATE_SETTING
|
|
852
|
+
# PR_CREATE_SETTING_PATH mirrors PR_CREATE_SETTING_PATH
|
|
499
853
|
#
|
|
500
854
|
# `set -u` WITHOUT `-e`: every refusal is reported with its own exit code rather
|
|
501
855
|
# than aborting mid-decision.
|
|
@@ -542,11 +896,24 @@
|
|
|
542
896
|
# bash scripts/remote-run.sh review feat_x --review-file /tmp/r.md
|
|
543
897
|
# -> 0; origin/feat_x gains `chore: add user review for feat_x`
|
|
544
898
|
# placing feat_x_review.md, then one `-f engine=user_review`
|
|
545
|
-
# dispatch; no copy or local feat_x is left
|
|
546
|
-
#
|
|
899
|
+
# dispatch; no copy or local feat_x is left; the stub lists no
|
|
900
|
+
# run of the pushed `headSha`, so one `::warning::` line follows
|
|
901
|
+
# (export HARNESS_TRIGGER_LOOKUP_SECS=0 to skip the waits)
|
|
902
|
+
# in flight the newest run `in_progress`, its jobs listing no completed
|
|
903
|
+
# `run` job -> 2, nothing pushed or sent
|
|
904
|
+
# no run no `harness run feat_x` run listed -> 2, nothing pushed; with
|
|
905
|
+
# --allow-no-run -> 0, placed and dispatched as `review` above
|
|
906
|
+
# reported report's setup, then review ... --actor alice --source
|
|
907
|
+
# https://github.com/o/r/pull/12#pullrequestreview-1 -> 0; one
|
|
908
|
+
# comment on 7 naming `Round <n>`, the source and `@alice`, then
|
|
909
|
+
# `sdlc-harness: running` on 7
|
|
547
910
|
# fetch t=$(mktemp -d); bash scripts/remote-run.sh fetch feat_x "$t"
|
|
548
911
|
# -> 0; prints `state: running` (or, with no run listed,
|
|
549
912
|
# `state: none`), every other key present
|
|
913
|
+
# discard mkdir -p sdlc-harness/scratch/branch-pause-feat_x, then
|
|
914
|
+
# bash scripts/remote-run.sh discard sdlc-harness/scratch/branch-pause-feat_x
|
|
915
|
+
# -> 0, the directory gone; discard sdlc-harness/autonomous_logs
|
|
916
|
+
# -> 2, nothing removed; no gh call either way
|
|
550
917
|
#
|
|
551
918
|
# no record with no registry, the "a bundle" stub below with question_1.md:
|
|
552
919
|
# bash scripts/remote-run.sh status feat_x -> 0; prints `state:
|
|
@@ -594,6 +961,13 @@
|
|
|
594
961
|
# first job a `run list` answer with no finished run: --resume none -> 0;
|
|
595
962
|
# --resume answer -> 2
|
|
596
963
|
# own run GITHUB_RUN_ID=<the bundle run's id> -> that run is skipped
|
|
964
|
+
# lineage the checkout on feat_x one commit beyond origin/main, and a `run
|
|
965
|
+
# list` answer holding only an older completed run whose `headSha`
|
|
966
|
+
# is another commit: --resume none -> 0, prints `skipped 1 finished
|
|
967
|
+
# run(s) of feat_x from before its current lineage` and `this is
|
|
968
|
+
# its first job`, no `run download`
|
|
969
|
+
# own lineage that same branch plus a newer completed run whose `headSha`
|
|
970
|
+
# is that commit -> that run's bundle is restored
|
|
597
971
|
# expired the artifact list answering {"artifacts":[{"name":"harness-state",
|
|
598
972
|
# "expired":true,"expires_at":"2026-01-02T00:00:00Z"}]}: --resume
|
|
599
973
|
# pause -> 0, a `::warning::` line, no `run download`, no older
|
|
@@ -648,7 +1022,7 @@
|
|
|
648
1022
|
# run-created-at 42 -> prints 1767225610, 0; a failing stub -> 3
|
|
649
1023
|
#
|
|
650
1024
|
# trigger needs start's setup plus `"forge": "github"`, an event file e.json
|
|
651
|
-
# {"action":"labeled","label":{"name":"harness"},"sender":{"login":"alice",
|
|
1025
|
+
# {"action":"labeled","label":{"name":"sdlc-harness"},"sender":{"login":"alice",
|
|
652
1026
|
# "type":"User"},"issue":{"number":7,"title":"Add comments","body":"x",
|
|
653
1027
|
# "html_url":"https://github.com/o/r/issues/7","state":"open"}}, and a stub
|
|
654
1028
|
# answering `api repos/o/r/collaborators/alice/permission` with
|
|
@@ -657,7 +1031,7 @@
|
|
|
657
1031
|
# trigger bash scripts/remote-run.sh trigger -> 0; origin/add_comments gains
|
|
658
1032
|
# the prompt commit, "$s.log" gains `workflow run harness-run.yml
|
|
659
1033
|
# --ref add_comments ...`, `issue comment 7 ...` naming the branch,
|
|
660
|
-
# then `issue edit 7 ... --remove-label harness`
|
|
1034
|
+
# then `issue edit 7 ... --remove-label sdlc-harness`
|
|
661
1035
|
# read the permission answer {"permission":"read"} -> 2, no `workflow
|
|
662
1036
|
# run`, one comment naming write access, the label removed
|
|
663
1037
|
# ignored e.json's label name `bug` -> 0, one line, "$s.log" unchanged
|
|
@@ -666,6 +1040,48 @@
|
|
|
666
1040
|
# "client_payload":{"title":"Add tags","body":"x","source":"jira"}}
|
|
667
1041
|
# -> 0; `workflow run ... --ref add_tags ...`, /tmp/s names
|
|
668
1042
|
# add_tags, no `issue` call; without "title" -> 2, no `workflow run`
|
|
1043
|
+
#
|
|
1044
|
+
# report needs trigger's setup, a branch feat_x pushed carrying its task
|
|
1045
|
+
# prompt (ending in a `Started from https://github.com/o/r/issues/7 by @alice`
|
|
1046
|
+
# line) and `flow_progress/feat_x_progress.md`, and a stub answering `pr
|
|
1047
|
+
# list` and `api repos/o/r/issues/7/labels` with []; GITHUB_REPOSITORY=o/r:
|
|
1048
|
+
# paused bash scripts/remote-run.sh report paused feat_x -> 0; "$s.log"
|
|
1049
|
+
# gains `api --method POST repos/o/r/issues/7/comments -F body=@…`
|
|
1050
|
+
# naming `@sdlc-harness resume`, then `api --method POST
|
|
1051
|
+
# repos/o/r/issues/7/labels -f labels[]=sdlc-harness: paused`
|
|
1052
|
+
# forge off `"forge": "none"`, then the same -> 0, one line, "$s.log" unchanged
|
|
1053
|
+
#
|
|
1054
|
+
# deliver needs report's setup, a stub answering the create with
|
|
1055
|
+
# https://github.com/o/r/pull/12, and <b> a bundle directory whose status.json
|
|
1056
|
+
# carries schema "1" and status completed:
|
|
1057
|
+
# deliver bash scripts/remote-run.sh deliver feat_x <b> -> 0; "$s.log" gains
|
|
1058
|
+
# a `--draft` create with `--base main --head feat_x`, then one
|
|
1059
|
+
# comment on issue 7 naming /pull/12, then `sdlc-harness: done` on
|
|
1060
|
+
# 7 and 12
|
|
1061
|
+
# not done status parked, or an empty <b> -> 0, one line, "$s.log" unchanged
|
|
1062
|
+
#
|
|
1063
|
+
# collect needs deliver's setup, a story index on feat_x, a stub answering
|
|
1064
|
+
# `pr list` with [{"number":12,"isCrossRepository":false}], `run list` with a
|
|
1065
|
+
# `completed` `harness run feat_x` run whose bundle says `completed`, `api
|
|
1066
|
+
# --paginate repos/o/r/pulls/12/reviews` with one `CHANGES_REQUESTED` review
|
|
1067
|
+
# by alice, and the permission call with {"permission":"write"}:
|
|
1068
|
+
# collect bash scripts/remote-run.sh collect feat_x -> 0; origin/feat_x
|
|
1069
|
+
# gains `chore: add user review for feat_x`, then one `-f
|
|
1070
|
+
# engine=user_review` dispatch; with no review listed -> 0, one
|
|
1071
|
+
# line, nothing pushed or dispatched
|
|
1072
|
+
#
|
|
1073
|
+
# control needs report's setup, a stub answering `pr view 12 ...` with
|
|
1074
|
+
# {"headRefName":"feat_x","isCrossRepository":false,"state":"OPEN"}, the
|
|
1075
|
+
# permission call with {"permission":"write"}, and `run list` with an
|
|
1076
|
+
# `in_progress` `harness run feat_x` run; an event file c.json
|
|
1077
|
+
# {"action":"created","comment":{"body":"@sdlc-harness pause"},"issue":{"number":12,
|
|
1078
|
+
# "pull_request":{"url":"x"}},"sender":{"login":"alice","type":"User"}};
|
|
1079
|
+
# export GITHUB_EVENT_NAME=issue_comment GITHUB_EVENT_PATH=c.json GITHUB_REPOSITORY=o/r:
|
|
1080
|
+
# pause bash scripts/remote-run.sh control -> 0; "$s.log" gains `workflow
|
|
1081
|
+
# run harness-run.yml --ref feat_x -f action=pause -f branch=feat_x`,
|
|
1082
|
+
# then one comment on 12 naming @alice
|
|
1083
|
+
# ignored c.json's body `Let's @sdlc-harness pause` -> 0, one line,
|
|
1084
|
+
# "$s.log" unchanged
|
|
669
1085
|
|
|
670
1086
|
set -u
|
|
671
1087
|
|
|
@@ -682,8 +1098,23 @@ WORKFLOW_RUN_FILE='harness-run.yml'
|
|
|
682
1098
|
WORKFLOW_RESUME_FILE='harness-resume.yml'
|
|
683
1099
|
STATE_ARTIFACT_NAME='harness-state'
|
|
684
1100
|
POLL_STATE_ARTIFACT_NAME='harness-poll-state'
|
|
685
|
-
DEFAULT_TRIGGER_LABEL='harness'
|
|
1101
|
+
DEFAULT_TRIGGER_LABEL='sdlc-harness'
|
|
1102
|
+
LEGACY_TRIGGER_LABEL='harness'
|
|
686
1103
|
TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
1104
|
+
WORKFLOW_CONTROL_FILE='harness-control.yml'
|
|
1105
|
+
COMMAND_HANDLE='@sdlc-harness'
|
|
1106
|
+
COMMAND_VERBS='answer pause resume stop clear'
|
|
1107
|
+
COMMENT_MARKER='<!-- sdlc-harness'
|
|
1108
|
+
REVIEW_ROUND_STATE='changes_requested'
|
|
1109
|
+
STATE_LABEL_PREFIX='sdlc-harness: '
|
|
1110
|
+
RUN_STATES='running parked paused done failed stopped'
|
|
1111
|
+
PR_CREATE_SETTING='Allow GitHub Actions to create and approve pull requests'
|
|
1112
|
+
PR_CREATE_SETTING_PATH='Settings -> Actions -> General -> Workflow permissions'
|
|
1113
|
+
# The most bytes of a question file one park comment carries. An issue comment
|
|
1114
|
+
# holds 262,144 bytes of UTF-8, and the refusal text's character count is not
|
|
1115
|
+
# to be trusted (docs/github-integration-research.md -> S6); the margin is the
|
|
1116
|
+
# framing lines and the marker.
|
|
1117
|
+
QUESTION_COMMENT_MAX_BYTES=250000
|
|
687
1118
|
GH="${HARNESS_GH_CLI:-gh}"
|
|
688
1119
|
|
|
689
1120
|
# How many runs `status` prints, and how many `run list` returns for status
|
|
@@ -708,6 +1139,13 @@ POLL_STATE_RUNS_LIMIT=10
|
|
|
708
1139
|
# between tries is `HARNESS_TRIGGER_LOOKUP_SECS`, a test seam.
|
|
709
1140
|
TRIGGER_RUN_LOOKUP_TRIES=6
|
|
710
1141
|
TRIGGER_LOOKUP_SECS_DEFAULT=5
|
|
1142
|
+
# The `run` job's name in WORKFLOW_RUN_FILE: its key, since it has no `name:`.
|
|
1143
|
+
# `branch_settled_var` reads that job's status; harness-run.yml's header
|
|
1144
|
+
# declares the mirror.
|
|
1145
|
+
RUN_JOB_NAME='run'
|
|
1146
|
+
# How far before the previous round's `collected_at` `round_collect` lists
|
|
1147
|
+
# from, absorbing runner-clock skew; the id check drops what it re-lists.
|
|
1148
|
+
ROUND_OVERLAP_SECS=300
|
|
711
1149
|
|
|
712
1150
|
# GitHub's documented limit on a `workflow_dispatch` inputs payload: "The
|
|
713
1151
|
# maximum payload for inputs is 65,535 characters."
|
|
@@ -727,7 +1165,7 @@ usage() {
|
|
|
727
1165
|
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
|
|
728
1166
|
echo " remote-run.sh pause <branch> [--repo <root>]" >&2
|
|
729
1167
|
echo " remote-run.sh warm [--repo <root>]" >&2
|
|
730
|
-
echo " remote-run.sh stop <branch> [--repo <root>]" >&2
|
|
1168
|
+
echo " remote-run.sh stop <branch> [--actor <login>] [--repo <root>]" >&2
|
|
731
1169
|
echo " remote-run.sh status <branch> [--repo <root>]" >&2
|
|
732
1170
|
echo " remote-run.sh sync <branch> [--repo <root>]" >&2
|
|
733
1171
|
echo " remote-run.sh fetch <branch> <out_dir> [--repo <root>]" >&2
|
|
@@ -738,9 +1176,14 @@ usage() {
|
|
|
738
1176
|
echo " remote-run.sh pause-requested <branch> <since_epoch> [--repo <root>]" >&2
|
|
739
1177
|
echo " remote-run.sh run-created-at <run_id> [--repo <root>]" >&2
|
|
740
1178
|
echo " remote-run.sh start <branch> --prompt-file <file> [--repo <root>]" >&2
|
|
741
|
-
echo " remote-run.sh review <branch> --review-file <file> [--repo <root>]" >&2
|
|
1179
|
+
echo " remote-run.sh review <branch> --review-file <file> [--allow-no-run] [--actor <login>] [--reviewers <login,login,...>] [--source <https-url>] [--repo <root>]" >&2
|
|
742
1180
|
echo " remote-run.sh trigger [--repo <root>]" >&2
|
|
743
1181
|
echo " remote-run.sh list [--repo <root>]" >&2
|
|
1182
|
+
echo " remote-run.sh discard <dir> [--repo <root>]" >&2
|
|
1183
|
+
echo " remote-run.sh report <event> <branch> [--note <text>] [--repo <root>]" >&2
|
|
1184
|
+
echo " remote-run.sh deliver <branch> <bundle_dir> [--repo <root>]" >&2
|
|
1185
|
+
echo " remote-run.sh collect <branch> [--pr <n>] [--repo <root>]" >&2
|
|
1186
|
+
echo " remote-run.sh control [--repo <root>]" >&2
|
|
744
1187
|
[ "${verb-}" != save ] || exit "$EXIT_OK"
|
|
745
1188
|
exit "$EXIT_USAGE"
|
|
746
1189
|
}
|
|
@@ -751,7 +1194,22 @@ usage() {
|
|
|
751
1194
|
GH_OUT=""
|
|
752
1195
|
GH_ERR=""
|
|
753
1196
|
gh_call() {
|
|
754
|
-
|
|
1197
|
+
gh_run 0 "" "$@"
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
# gh_call_token <token> <args...> — gh_call with GH_TOKEN set to <token> for
|
|
1201
|
+
# that one gh process only; every other call keeps the environment's token.
|
|
1202
|
+
gh_call_token() {
|
|
1203
|
+
local token="$1"
|
|
1204
|
+
shift
|
|
1205
|
+
gh_run 1 "$token" "$@"
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
# gh_run <0|1> <token> <args...> — gh_call's body; with 1, GH_TOKEN is a prefix
|
|
1209
|
+
# assignment on the external command, never on a function, so it cannot leak.
|
|
1210
|
+
gh_run() {
|
|
1211
|
+
local with_token="$1" token="$2" errfile status
|
|
1212
|
+
shift 2
|
|
755
1213
|
GH_OUT=""
|
|
756
1214
|
GH_ERR=""
|
|
757
1215
|
if ! command -v "$GH" >/dev/null 2>&1; then
|
|
@@ -759,7 +1217,11 @@ gh_call() {
|
|
|
759
1217
|
return 127
|
|
760
1218
|
fi
|
|
761
1219
|
errfile=$(mktemp) || { GH_ERR="mktemp failed"; return 1; }
|
|
762
|
-
|
|
1220
|
+
if [ "$with_token" = 1 ]; then
|
|
1221
|
+
GH_OUT=$(GH_TOKEN="$token" "$GH" "$@" 2>"$errfile")
|
|
1222
|
+
else
|
|
1223
|
+
GH_OUT=$("$GH" "$@" 2>"$errfile")
|
|
1224
|
+
fi
|
|
763
1225
|
status=$?
|
|
764
1226
|
if [ "$status" -ne 0 ]; then
|
|
765
1227
|
IFS= read -r GH_ERR <"$errfile" || :
|
|
@@ -791,7 +1253,7 @@ verb=""
|
|
|
791
1253
|
verb="$1"
|
|
792
1254
|
shift
|
|
793
1255
|
case "$verb" in
|
|
794
|
-
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list) ;;
|
|
1256
|
+
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list|discard|report|deliver|collect|control) ;;
|
|
795
1257
|
*) usage "unknown verb '$verb'" ;;
|
|
796
1258
|
esac
|
|
797
1259
|
|
|
@@ -811,6 +1273,15 @@ since_arg=""
|
|
|
811
1273
|
run_id_arg=""
|
|
812
1274
|
prompt_file=""
|
|
813
1275
|
review_file=""
|
|
1276
|
+
discard_dir=""
|
|
1277
|
+
discard_base=""
|
|
1278
|
+
report_event=""
|
|
1279
|
+
report_note=""
|
|
1280
|
+
actor_arg=""
|
|
1281
|
+
source_arg=""
|
|
1282
|
+
reviewers_arg=""
|
|
1283
|
+
allow_no_run=0
|
|
1284
|
+
pr_arg=""
|
|
814
1285
|
|
|
815
1286
|
while [ "$#" -gt 0 ]; do
|
|
816
1287
|
case "$1" in
|
|
@@ -842,21 +1313,52 @@ while [ "$#" -gt 0 ]; do
|
|
|
842
1313
|
[ "$verb" = review ] || usage "$1 is a review option"
|
|
843
1314
|
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
844
1315
|
review_file="$2"; shift 2 ;;
|
|
1316
|
+
--note)
|
|
1317
|
+
[ "$verb" = report ] || usage "$1 is a report option"
|
|
1318
|
+
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1319
|
+
report_note="$2"; shift 2 ;;
|
|
1320
|
+
--actor)
|
|
1321
|
+
[ "$verb" = stop ] || [ "$verb" = review ] || usage "$1 is a stop or review option"
|
|
1322
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
1323
|
+
actor_arg="$2"; shift 2 ;;
|
|
1324
|
+
--source)
|
|
1325
|
+
[ "$verb" = review ] || usage "$1 is a review option"
|
|
1326
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
1327
|
+
source_arg="$2"; shift 2 ;;
|
|
1328
|
+
--reviewers)
|
|
1329
|
+
[ "$verb" = review ] || usage "$1 is a review option"
|
|
1330
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
1331
|
+
reviewers_arg="$2"; shift 2 ;;
|
|
1332
|
+
--allow-no-run)
|
|
1333
|
+
[ "$verb" = review ] || usage "$1 is a review option"
|
|
1334
|
+
allow_no_run=1; shift ;;
|
|
1335
|
+
--pr)
|
|
1336
|
+
[ "$verb" = collect ] || usage "$1 is a collect option"
|
|
1337
|
+
[ "$#" -ge 2 ] || usage "$1 needs a value"
|
|
1338
|
+
case "$2" in
|
|
1339
|
+
''|*[!0-9]*|0*) usage "--pr needs a positive pull request number" ;;
|
|
1340
|
+
esac
|
|
1341
|
+
pr_arg="$2"; shift 2 ;;
|
|
845
1342
|
-*)
|
|
846
1343
|
usage "unknown option '$1'" ;;
|
|
847
1344
|
*)
|
|
848
1345
|
[ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != trigger ] \
|
|
849
|
-
&& [ "$verb" != list ] || usage "$verb takes no branch"
|
|
1346
|
+
&& [ "$verb" != list ] && [ "$verb" != control ] || usage "$verb takes no branch"
|
|
850
1347
|
if [ "$verb" = run-created-at ]; then
|
|
851
1348
|
[ -z "$run_id_arg" ] || usage "unexpected argument '$1'"
|
|
852
1349
|
run_id_arg="$1"
|
|
1350
|
+
elif [ "$verb" = discard ]; then
|
|
1351
|
+
[ -z "$discard_dir" ] || usage "unexpected argument '$1'"
|
|
1352
|
+
discard_dir="$1"
|
|
1353
|
+
elif [ "$verb" = report ] && [ -z "$report_event" ]; then
|
|
1354
|
+
report_event="$1"
|
|
853
1355
|
elif [ -z "$branch" ]; then
|
|
854
1356
|
branch="$1"
|
|
855
1357
|
elif [ "$verb" = pause-requested ] && [ -z "$since_arg" ]; then
|
|
856
1358
|
since_arg="$1"
|
|
857
1359
|
elif { [ "$verb" = save ] || [ "$verb" = fetch ]; } && [ -z "$out_dir" ]; then
|
|
858
1360
|
out_dir="$1"
|
|
859
|
-
elif [ "$verb" = continue ] && [ -z "$bundle_dir" ]; then
|
|
1361
|
+
elif { [ "$verb" = continue ] || [ "$verb" = deliver ]; } && [ -z "$bundle_dir" ]; then
|
|
860
1362
|
bundle_dir="$1"
|
|
861
1363
|
else
|
|
862
1364
|
usage "unexpected argument '$1'"
|
|
@@ -866,10 +1368,38 @@ while [ "$#" -gt 0 ]; do
|
|
|
866
1368
|
done
|
|
867
1369
|
|
|
868
1370
|
if [ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != run-created-at ] && [ "$verb" != trigger ] \
|
|
869
|
-
&& [ "$verb" != list ]; then
|
|
1371
|
+
&& [ "$verb" != list ] && [ "$verb" != discard ] && [ "$verb" != control ]; then
|
|
870
1372
|
valid_branch "$branch" || usage "$verb needs a <branch>"
|
|
871
1373
|
fi
|
|
872
1374
|
|
|
1375
|
+
if [ "$verb" = discard ] && [ -z "$discard_dir" ]; then
|
|
1376
|
+
usage "discard needs a <dir>"
|
|
1377
|
+
fi
|
|
1378
|
+
|
|
1379
|
+
# The event lands in the comment marker, so it is a word.
|
|
1380
|
+
if [ "$verb" = report ] && ! [[ "$report_event" =~ ^[a-z][a-z_]*$ ]]; then
|
|
1381
|
+
usage "report needs an <event> of lowercase letters and underscores"
|
|
1382
|
+
fi
|
|
1383
|
+
|
|
1384
|
+
# The actor lands in the stop or round comment, so it is a login: the trigger's shape.
|
|
1385
|
+
if [ -n "$actor_arg" ] && ! [[ "$actor_arg" =~ ^[A-Za-z0-9][A-Za-z0-9-]*(\[bot\])?$ ]]; then
|
|
1386
|
+
usage "--actor needs a GitHub login"
|
|
1387
|
+
fi
|
|
1388
|
+
|
|
1389
|
+
# The reviewers land in the round comment, so each is a login.
|
|
1390
|
+
if [ -n "$reviewers_arg" ]; then
|
|
1391
|
+
[[ ",$reviewers_arg," =~ ^(,[A-Za-z0-9][A-Za-z0-9-]*(\[bot\])?)+,$ ]] \
|
|
1392
|
+
|| usage "--reviewers needs comma-separated GitHub logins"
|
|
1393
|
+
fi
|
|
1394
|
+
|
|
1395
|
+
# The source lands in the round comment as a link.
|
|
1396
|
+
if [ -n "$source_arg" ]; then
|
|
1397
|
+
case "$source_arg" in
|
|
1398
|
+
https://*) ;;
|
|
1399
|
+
*) usage "--source needs an https:// URL" ;;
|
|
1400
|
+
esac
|
|
1401
|
+
fi
|
|
1402
|
+
|
|
873
1403
|
if [ "$verb" = pause-requested ]; then
|
|
874
1404
|
case "$since_arg" in
|
|
875
1405
|
''|*[!0-9]*) usage "pause-requested needs a <since_epoch> that is a non-negative integer" ;;
|
|
@@ -884,8 +1414,8 @@ if [ "$verb" = run-created-at ]; then
|
|
|
884
1414
|
esac
|
|
885
1415
|
fi
|
|
886
1416
|
|
|
887
|
-
if [ "$verb" = continue ] && [ -z "$bundle_dir" ]; then
|
|
888
|
-
usage "
|
|
1417
|
+
if { [ "$verb" = continue ] || [ "$verb" = deliver ]; } && [ -z "$bundle_dir" ]; then
|
|
1418
|
+
usage "$verb needs a <bundle_dir>"
|
|
889
1419
|
fi
|
|
890
1420
|
|
|
891
1421
|
if [ "$verb" = save ] && [ -z "$out_dir" ]; then
|
|
@@ -943,17 +1473,20 @@ fi
|
|
|
943
1473
|
# The repository and its configuration.
|
|
944
1474
|
# ---------------------------------------------------------------------------
|
|
945
1475
|
|
|
946
|
-
# setup_fail <message> — a configuration problem: exit 1, except for save
|
|
1476
|
+
# setup_fail <message> — a configuration problem: exit 1, except for save,
|
|
1477
|
+
# report, deliver and collect, which never fail the step that calls them.
|
|
947
1478
|
setup_fail() {
|
|
948
1479
|
echo "remote-run.sh: $1" >&2
|
|
949
|
-
[ "$verb" != save ] || exit "$EXIT_OK"
|
|
1480
|
+
[ "$verb" != save ] && [ "$verb" != report ] && [ "$verb" != deliver ] && [ "$verb" != collect ] || exit "$EXIT_OK"
|
|
950
1481
|
exit "$EXIT_USAGE"
|
|
951
1482
|
}
|
|
952
1483
|
|
|
953
1484
|
if [ -n "$repo_arg" ]; then
|
|
954
1485
|
root=$(hr_repo_root "$repo_arg") || setup_fail "'$repo_arg' is not a git repository"
|
|
955
1486
|
elif [ "$verb" = restore ] || [ "$verb" = save ] || [ "$verb" = continue ] || [ "$verb" = poll ] \
|
|
956
|
-
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ]
|
|
1487
|
+
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ] \
|
|
1488
|
+
|| [ "$verb" = discard ] || [ "$verb" = report ] || [ "$verb" = deliver ] || [ "$verb" = collect ] \
|
|
1489
|
+
|| [ "$verb" = control ]; then
|
|
957
1490
|
root=$(hr_repo_root "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
958
1491
|
else
|
|
959
1492
|
root=$(hr_main_repo "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
@@ -963,15 +1496,18 @@ hr_config_load "$root" || :
|
|
|
963
1496
|
registry=""
|
|
964
1497
|
status_no_record=0
|
|
965
1498
|
case "$verb" in
|
|
966
|
-
restore|save|continue|poll)
|
|
1499
|
+
restore|save|continue|poll|discard)
|
|
967
1500
|
hr_state_path "$root" >/dev/null || setup_fail "cannot resolve '$root/harness.config.json'"
|
|
968
1501
|
;;
|
|
969
1502
|
pause-requested|run-created-at)
|
|
970
1503
|
# Read verbs: no gate, and nothing of the configuration is read.
|
|
971
1504
|
;;
|
|
972
|
-
trigger)
|
|
1505
|
+
trigger|control)
|
|
973
1506
|
# Gates itself, after reading the event, so a refusal can still be commented.
|
|
974
1507
|
;;
|
|
1508
|
+
report|deliver|collect)
|
|
1509
|
+
# Gates itself (`forge_on`) and exits 0 on every outcome.
|
|
1510
|
+
;;
|
|
975
1511
|
status|sync)
|
|
976
1512
|
registry=$(hr_state_path "$root" autonomous_logs/registry.json) || {
|
|
977
1513
|
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
@@ -1020,12 +1556,18 @@ case "$review_file" in
|
|
|
1020
1556
|
''|/*) ;;
|
|
1021
1557
|
*) review_file="${PWD-.}/$review_file" ;;
|
|
1022
1558
|
esac
|
|
1559
|
+
case "$answers_from" in
|
|
1560
|
+
''|/*) ;;
|
|
1561
|
+
*) answers_from="${PWD-.}/$answers_from" ;;
|
|
1562
|
+
esac
|
|
1023
1563
|
if [ "$verb" = fetch ]; then
|
|
1024
1564
|
case "$out_dir" in
|
|
1025
1565
|
/*) ;;
|
|
1026
1566
|
*) out_dir="${PWD-.}/$out_dir" ;;
|
|
1027
1567
|
esac
|
|
1028
1568
|
fi
|
|
1569
|
+
# Kept apart from <dir>, so the library's character tests see it as typed.
|
|
1570
|
+
[ "$verb" != discard ] || discard_base="${PWD-.}"
|
|
1029
1571
|
|
|
1030
1572
|
cd "$root" || setup_fail "cannot enter '$root'"
|
|
1031
1573
|
|
|
@@ -1135,6 +1677,11 @@ verb_stop() {
|
|
|
1135
1677
|
|| echo "remote-run.sh: stopped on GitHub, but the local record of $branch could not be updated" >&2
|
|
1136
1678
|
fi
|
|
1137
1679
|
echo "remote-run.sh: stopped $branch"
|
|
1680
|
+
if [ -n "$actor_arg" ]; then
|
|
1681
|
+
forge_report stopped "$branch" "Stopped by @$actor_arg."
|
|
1682
|
+
else
|
|
1683
|
+
forge_report stopped "$branch" "Stopped from a local \`remote-run.sh stop\`."
|
|
1684
|
+
fi
|
|
1138
1685
|
}
|
|
1139
1686
|
|
|
1140
1687
|
# list_runs — the branch's runs of the workflow into GH_OUT; exits 3 on failure.
|
|
@@ -1332,7 +1879,9 @@ sync_expired() {
|
|
|
1332
1879
|
# into <download_dir> — `sync`'s per-run directory under the main checkout when
|
|
1333
1880
|
# empty — skipped when that directory already holds its status.json, and
|
|
1334
1881
|
# RS_BUNDLE is then 1. <applied_run_id>, when set, also counts as a bundle
|
|
1335
|
-
# existing for case 4.
|
|
1882
|
+
# existing for case 4. <finished> 1 reads the newest run as finished whatever
|
|
1883
|
+
# its `status`: `branch_settled_var` passes it once that run's `run` job has
|
|
1884
|
+
# completed. Exits 3 when gh fails, 2 for an unrecognised bundle.
|
|
1336
1885
|
RS_RUNS=""
|
|
1337
1886
|
RS_RUN_ID=""
|
|
1338
1887
|
RS_RUN_URL=""
|
|
@@ -1346,7 +1895,7 @@ RS_PARK_LOOP_CYCLES=""
|
|
|
1346
1895
|
RS_BUNDLE=0
|
|
1347
1896
|
RS_DOWNLOAD=""
|
|
1348
1897
|
remote_state() {
|
|
1349
|
-
local download="${1-}" applied="${2-}" newest status_file older bundle_exists=0
|
|
1898
|
+
local download="${1-}" applied="${2-}" finished="${3-0}" newest status_file older bundle_exists=0
|
|
1350
1899
|
RS_RUNS=""; RS_RUN_ID=""; RS_RUN_URL=""; RS_GH_STATUS=""; RS_STATE=""
|
|
1351
1900
|
RS_PAUSE_REASON=""; RS_DETAIL=""; RS_ENGINE=""; RS_USAGE_RESUME_AT=""
|
|
1352
1901
|
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""
|
|
@@ -1359,7 +1908,7 @@ remote_state() {
|
|
|
1359
1908
|
RS_RUN_ID=$(printf '%s' "$newest" | jq -r '.databaseId | tostring')
|
|
1360
1909
|
RS_GH_STATUS=$(printf '%s' "$newest" | jq -r '.status // ""')
|
|
1361
1910
|
RS_RUN_URL=$(printf '%s' "$newest" | jq -r '.url // ""')
|
|
1362
|
-
if [ "$RS_GH_STATUS" != completed ]; then
|
|
1911
|
+
if [ "$RS_GH_STATUS" != completed ] && [ "$finished" != 1 ]; then
|
|
1363
1912
|
RS_STATE=running
|
|
1364
1913
|
return 0
|
|
1365
1914
|
fi
|
|
@@ -1435,6 +1984,48 @@ remote_state() {
|
|
|
1435
1984
|
return 0
|
|
1436
1985
|
}
|
|
1437
1986
|
|
|
1987
|
+
# branch_settled_var <download_dir> <allow_no_run 0|1> — the one settledness
|
|
1988
|
+
# test `review`, `control` and `collect` share: whether a user-review round may be placed
|
|
1989
|
+
# on the branch now. Lists the runs itself, then derives RS_* through
|
|
1990
|
+
# `remote_state`. The newest `harness run <branch>` run decides:
|
|
1991
|
+
# none listed settled only under <allow_no_run> 1 (RS_STATE `none`)
|
|
1992
|
+
# `completed` `remote_state` as ever
|
|
1993
|
+
# anything else its jobs are read: while no job named RUN_JOB_NAME exists
|
|
1994
|
+
# (queued) or that job is not `completed`, in flight as
|
|
1995
|
+
# `running`; once it is, the run's bundle decides as for a
|
|
1996
|
+
# finished run, since only a later job of that run is left
|
|
1997
|
+
# SETTLED is 1 for RS_STATE `completed` or `failed` (and `none` as above), else
|
|
1998
|
+
# 0. Exits 3 when gh fails, 2 for an unrecognised bundle, as `remote_state` does.
|
|
1999
|
+
SETTLED=0
|
|
2000
|
+
branch_settled_var() {
|
|
2001
|
+
local download="${1-}" allow="${2-0}" listed newest id status job finished=0
|
|
2002
|
+
SETTLED=0
|
|
2003
|
+
list_runs
|
|
2004
|
+
# `remote_state` reads the listing from GH_OUT, which the jobs call replaces.
|
|
2005
|
+
listed="$GH_OUT"
|
|
2006
|
+
newest=$(titled_runs "harness run $branch" | jq -c '.[0] // empty') || newest=""
|
|
2007
|
+
if [ -n "$newest" ]; then
|
|
2008
|
+
status=$(printf '%s' "$newest" | jq -r '.status // ""')
|
|
2009
|
+
if [ "$status" != completed ]; then
|
|
2010
|
+
id=$(printf '%s' "$newest" | jq -r '.databaseId | tostring')
|
|
2011
|
+
gh_call api "repos/{owner}/{repo}/actions/runs/$id/jobs" || gh_fail "reading the jobs of run $id failed"
|
|
2012
|
+
job=$(printf '%s' "$GH_OUT" | jq -r --arg n "$RUN_JOB_NAME" \
|
|
2013
|
+
'[.jobs[]? | select(.name == $n) | .status // ""] | first // ""' 2>/dev/null) || {
|
|
2014
|
+
GH_ERR="its job list is not the expected JSON"
|
|
2015
|
+
gh_fail "reading the jobs of run $id failed"
|
|
2016
|
+
}
|
|
2017
|
+
[ "$job" != completed ] || finished=1
|
|
2018
|
+
fi
|
|
2019
|
+
fi
|
|
2020
|
+
GH_OUT="$listed"
|
|
2021
|
+
remote_state "$download" "" "$finished"
|
|
2022
|
+
case "$RS_STATE" in
|
|
2023
|
+
completed|failed) SETTLED=1 ;;
|
|
2024
|
+
none) [ "$allow" != 1 ] || SETTLED=1 ;;
|
|
2025
|
+
esac
|
|
2026
|
+
return 0
|
|
2027
|
+
}
|
|
2028
|
+
|
|
1438
2029
|
verb_sync() {
|
|
1439
2030
|
local worktree id url synced_id now download
|
|
1440
2031
|
local status reason detail
|
|
@@ -1542,26 +2133,71 @@ restore_refuse() {
|
|
|
1542
2133
|
exit "$EXIT_REFUSED"
|
|
1543
2134
|
}
|
|
1544
2135
|
|
|
2136
|
+
# lineage_commits_var <checkout> — the branch's current lineage, read from refs
|
|
2137
|
+
# alone: never a fetch, never gh, nothing on stdout. A run is of the current
|
|
2138
|
+
# lineage when its `headSha` is a commit reachable from HEAD and not from
|
|
2139
|
+
# `origin/<defaultBranch>`. The list is complete because `harness-run.yml`
|
|
2140
|
+
# checks out with `fetch-depth: 0`. It survives the branch's own history
|
|
2141
|
+
# edits because `refresh-branch.sh` merges and never rebases and
|
|
2142
|
+
# `push-branch.sh` never forces, so every own run's `headSha` stays an ancestor
|
|
2143
|
+
# of HEAD; a deleted, unmerged branch's commits are not ancestors of a branch
|
|
2144
|
+
# recreated under its name. An empty list leaves the lineage unbounded.
|
|
2145
|
+
# 0: LINEAGE_COMMITS holds the newline-separated full SHAs. 1: it is empty and
|
|
2146
|
+
# LINEAGE_WHY names the reason.
|
|
2147
|
+
LINEAGE_COMMITS=""
|
|
2148
|
+
LINEAGE_WHY=""
|
|
2149
|
+
lineage_commits_var() {
|
|
2150
|
+
local checkout="${1-}" default
|
|
2151
|
+
LINEAGE_COMMITS=""
|
|
2152
|
+
LINEAGE_WHY=""
|
|
2153
|
+
default=$(hr_default_branch "$checkout") && [ -n "$default" ] || {
|
|
2154
|
+
LINEAGE_WHY="the configuration could not be read"
|
|
2155
|
+
return 1
|
|
2156
|
+
}
|
|
2157
|
+
git -C "$checkout" rev-parse --verify --quiet "refs/remotes/origin/$default^{commit}" >/dev/null 2>&1 || {
|
|
2158
|
+
LINEAGE_WHY="origin/$default is not present"
|
|
2159
|
+
return 1
|
|
2160
|
+
}
|
|
2161
|
+
LINEAGE_COMMITS=$(git -C "$checkout" rev-list "refs/remotes/origin/$default..HEAD" 2>/dev/null) || LINEAGE_COMMITS=""
|
|
2162
|
+
[ -n "$LINEAGE_COMMITS" ] || {
|
|
2163
|
+
LINEAGE_WHY="HEAD carries no commit beyond origin/$default"
|
|
2164
|
+
return 1
|
|
2165
|
+
}
|
|
2166
|
+
return 0
|
|
2167
|
+
}
|
|
2168
|
+
|
|
1545
2169
|
# previous_bundle_run — PREV_RUN_ID is the newest finished `harness run
|
|
1546
2170
|
# <branch>` run, other than this job's own, carrying a state artifact, and
|
|
1547
|
-
# PREV_RUN_STATE is `present`, `expired` or empty when no run carries one.
|
|
1548
|
-
#
|
|
2171
|
+
# PREV_RUN_STATE is `present`, `expired` or empty when no run carries one. When
|
|
2172
|
+
# `lineage_commits_var` bounds the lineage, a run whose `headSha` is not in it
|
|
2173
|
+
# (or that has none) is dropped before the walk and counted in LINEAGE_SKIPPED.
|
|
2174
|
+
# A run with no artifact is walked past; an expired one stops the walk, because
|
|
1549
2175
|
# an older copy is staler state. Exits 3 when gh fails.
|
|
1550
2176
|
PREV_RUN_ID=""
|
|
1551
2177
|
PREV_RUN_STATE=""
|
|
2178
|
+
LINEAGE_SKIPPED=0
|
|
1552
2179
|
previous_bundle_run() {
|
|
1553
|
-
local ids id
|
|
2180
|
+
local ids id out bounded=0
|
|
1554
2181
|
PREV_RUN_ID=""
|
|
1555
2182
|
PREV_RUN_STATE=""
|
|
2183
|
+
LINEAGE_SKIPPED=0
|
|
2184
|
+
lineage_commits_var "$root" && bounded=1
|
|
1556
2185
|
gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" \
|
|
1557
|
-
--json databaseId,displayTitle,status,createdAt --limit "$RUN_LIST_LIMIT" \
|
|
2186
|
+
--json databaseId,displayTitle,status,createdAt,headSha --limit "$RUN_LIST_LIMIT" \
|
|
1558
2187
|
|| gh_fail "listing the runs of '$branch' failed"
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
2188
|
+
# jq 1.5: membership by `any(gen; cond)`, not `index` / `IN`.
|
|
2189
|
+
out=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" --arg self "${GITHUB_RUN_ID-}" \
|
|
2190
|
+
--arg bounded "$bounded" --arg lineage "$LINEAGE_COMMITS" '
|
|
2191
|
+
($lineage | split("\n")) as $l
|
|
2192
|
+
| [.[] | select(.displayTitle == $t and .status == "completed" and (.databaseId | tostring) != $self)] as $done
|
|
2193
|
+
| [$done[] | select($bounded != "1" or ((.headSha // "") as $h | any($l[]; . == $h)))] as $kept
|
|
2194
|
+
| ((($done | length) - ($kept | length)) | tostring),
|
|
2195
|
+
($kept | sort_by([.createdAt, .databaseId]) | reverse | .[].databaseId | tostring)' 2>/dev/null) || {
|
|
1562
2196
|
GH_ERR="its run list is not the expected JSON"
|
|
1563
2197
|
gh_fail "listing the runs of '$branch' failed"
|
|
1564
2198
|
}
|
|
2199
|
+
LINEAGE_SKIPPED=$(printf '%s\n' "$out" | head -n 1)
|
|
2200
|
+
ids=$(printf '%s\n' "$out" | tail -n +2)
|
|
1565
2201
|
for id in $ids; do
|
|
1566
2202
|
bundle_state "$id"
|
|
1567
2203
|
if [ "$BUNDLE_STATE" != none ]; then
|
|
@@ -1592,13 +2228,18 @@ verb_restore() {
|
|
|
1592
2228
|
previous_bundle_run
|
|
1593
2229
|
id="$PREV_RUN_ID"
|
|
1594
2230
|
hr_remote_names_var
|
|
2231
|
+
if [ -n "$LINEAGE_WHY" ]; then
|
|
2232
|
+
echo "remote-run.sh: the lineage of $branch is not bounded ($LINEAGE_WHY); every finished run of it is a candidate"
|
|
2233
|
+
elif [ "${LINEAGE_SKIPPED:-0}" -gt 0 ]; then
|
|
2234
|
+
echo "remote-run.sh: skipped $LINEAGE_SKIPPED finished run(s) of $branch from before its current lineage"
|
|
2235
|
+
fi
|
|
1595
2236
|
if [ "$PREV_RUN_STATE" = expired ]; then
|
|
1596
2237
|
[ "$resume" != answer ] \
|
|
1597
2238
|
|| restore_refuse "the state bundle of run $id expired on $BUNDLE_EXPIRES_AT, so its questions can no longer be answered here: resume from the committed ledger with $RESUME_HINT $branch, or re-drop the task; nothing written"
|
|
1598
2239
|
echo "::warning::remote-run.sh: the state bundle of run $id expired on $BUNDLE_EXPIRES_AT: the park-loop, auto-resume and stall counts, the clarification history and any planning drafts not yet committed that it carried are lost; this job continues from the committed ledger"
|
|
1599
2240
|
elif [ -z "$id" ]; then
|
|
1600
2241
|
[ "$resume" != answer ] \
|
|
1601
|
-
|| restore_refuse "--resume answer, but no finished run of $branch carries a state bundle; nothing written"
|
|
2242
|
+
|| restore_refuse "--resume answer, but no finished run of $branch's current lineage carries a state bundle; nothing written"
|
|
1602
2243
|
echo "remote-run.sh: no previous bundle for $branch; this is its first job"
|
|
1603
2244
|
else
|
|
1604
2245
|
download=$(hr_state_path "$root" "autonomous_logs/remote_download/$branch/$id") \
|
|
@@ -1726,7 +2367,11 @@ verb_save() {
|
|
|
1726
2367
|
return 0
|
|
1727
2368
|
}
|
|
1728
2369
|
|
|
1729
|
-
# notify <event> <branch> <detail> — one lifecycle notification
|
|
2370
|
+
# notify <event> <branch> <detail> <forge_note> — one lifecycle notification,
|
|
2371
|
+
# then `forge_report` of the same event; never fails. Every call site supplies
|
|
2372
|
+
# both texts: <detail> is the push notification's, slash commands included;
|
|
2373
|
+
# <forge_note> is the comment's, naming no slash command and no shell command,
|
|
2374
|
+
# and states only what happened, since `forge_report` adds the next action.
|
|
1730
2375
|
notify() {
|
|
1731
2376
|
if [ -n "${HARNESS_REMOTE_SLUG-}" ]; then
|
|
1732
2377
|
HARNESS_REPO_SLUG="$HARNESS_REMOTE_SLUG"
|
|
@@ -1735,6 +2380,8 @@ notify() {
|
|
|
1735
2380
|
bash "$script_dir/autonomous-notify.sh" "$1" "$2" "" "$3" \
|
|
1736
2381
|
|| echo "remote-run.sh: the $1 notification for $2 could not be sent" >&2
|
|
1737
2382
|
echo "remote-run.sh: notified $1 for $2: $3"
|
|
2383
|
+
# stdin closed: `poll` calls this inside a loop reading its run list.
|
|
2384
|
+
forge_report "$1" "$2" "$4" </dev/null
|
|
1738
2385
|
}
|
|
1739
2386
|
|
|
1740
2387
|
this_run_url() {
|
|
@@ -1833,46 +2480,49 @@ valid_engine() {
|
|
|
1833
2480
|
|
|
1834
2481
|
STOPPED_LINE="a 'harness stop' run is newer than its newest 'harness run' run"
|
|
1835
2482
|
RESUME_HINT="/autonomous-sdlc-harness:branch-resume"
|
|
2483
|
+
# A `paused` report on a usage pause says the run resumes by itself; a note
|
|
2484
|
+
# saying the automatic resume failed carries the action instead.
|
|
2485
|
+
USAGE_RESUME_NOTE="Comment \`$COMMAND_HANDLE resume\` after the limit resets to continue."
|
|
1836
2486
|
|
|
1837
2487
|
continue_redispatch() {
|
|
1838
2488
|
local status_file="$1" engine_value
|
|
1839
2489
|
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
1840
|
-
notify paused "$branch" "Not re-dispatched: remote stop is set. Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "")."
|
|
2490
|
+
notify paused "$branch" "Not re-dispatched: remote stop is set. Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "")." "Not continued: the repository variable \`HARNESS_REMOTE_STOP\` is set; clear it, then resume."
|
|
1841
2491
|
return 0
|
|
1842
2492
|
fi
|
|
1843
2493
|
remote_branch_stopped "$branch"
|
|
1844
2494
|
case $? in
|
|
1845
2495
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); not re-dispatched"; return 0 ;;
|
|
1846
|
-
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" "")."; return 0 ;;
|
|
2496
|
+
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 ;;
|
|
1847
2497
|
esac
|
|
1848
2498
|
if ! max_chain_var; then
|
|
1849
|
-
notify failed "$branch" "Not re-dispatched: HARNESS_MAX_CHAIN '$MAX_CHAIN' is not a non-negative integer."
|
|
2499
|
+
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."
|
|
1850
2500
|
return 0
|
|
1851
2501
|
fi
|
|
1852
2502
|
next_chain_var "$status_file"
|
|
1853
2503
|
case $? in
|
|
1854
|
-
1) notify failed "$branch" "Not re-dispatched: chain unreadable in status.json."; return 0 ;;
|
|
1855
|
-
2) notify failed "$branch" "Not re-dispatched: chain limit reached ($NEXT_CHAIN over HARNESS_MAX_CHAIN $MAX_CHAIN)."; return 0 ;;
|
|
2504
|
+
1) notify failed "$branch" "Not re-dispatched: chain unreadable in status.json." "Not continued: the chain count in the run's status could not be read."; return 0 ;;
|
|
2505
|
+
2) notify failed "$branch" "Not re-dispatched: chain limit reached ($NEXT_CHAIN over HARNESS_MAX_CHAIN $MAX_CHAIN)." "Not continued: the chain limit was reached ($NEXT_CHAIN over \`HARNESS_MAX_CHAIN\` $MAX_CHAIN)."; return 0 ;;
|
|
1856
2506
|
esac
|
|
1857
2507
|
engine_value=$(hr_remote_status_get "$status_file" engine) || engine_value=""
|
|
1858
2508
|
if ! valid_engine "$engine_value"; then
|
|
1859
|
-
notify failed "$branch" "Not re-dispatched: engine '$engine_value' in status.json is not task, user_review or docs."
|
|
2509
|
+
notify failed "$branch" "Not re-dispatched: engine '$engine_value' in status.json is not task, user_review or docs." "Not continued: the engine '$engine_value' in the run's status is not task, user_review or docs."
|
|
1860
2510
|
return 0
|
|
1861
2511
|
fi
|
|
1862
2512
|
redispatch "$engine_value" "$NEXT_CHAIN" \
|
|
1863
|
-
|| notify paused "$branch" "Re-dispatch failed ($REDISPATCH_ERR). Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "$engine_value")."
|
|
2513
|
+
|| notify paused "$branch" "Re-dispatch failed ($REDISPATCH_ERR). Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "$engine_value")." "Not continued: dispatching the next job failed ($REDISPATCH_ERR)."
|
|
1864
2514
|
}
|
|
1865
2515
|
|
|
1866
2516
|
continue_wait_poller() {
|
|
1867
2517
|
remote_branch_stopped "$branch"
|
|
1868
2518
|
case $? in
|
|
1869
2519
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); the resume poller is not enabled"; return 0 ;;
|
|
1870
|
-
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" "")."; return 0 ;;
|
|
2520
|
+
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 ;;
|
|
1871
2521
|
esac
|
|
1872
2522
|
if gh_call workflow enable "$WORKFLOW_RESUME_FILE"; then
|
|
1873
2523
|
echo "remote-run.sh: enabled $WORKFLOW_RESUME_FILE for $branch"
|
|
1874
2524
|
else
|
|
1875
|
-
notify paused "$branch" "Auto-resume is unavailable: enabling $WORKFLOW_RESUME_FILE failed ($GH_ERR). Run $RESUME_HINT $branch after the usage reset; $(hr_github_resume_route "$branch" "")."
|
|
2525
|
+
notify paused "$branch" "Auto-resume is unavailable: enabling $WORKFLOW_RESUME_FILE failed ($GH_ERR). Run $RESUME_HINT $branch after the usage reset; $(hr_github_resume_route "$branch" "")." "The automatic resume after the usage limit could not be scheduled ($GH_ERR). $USAGE_RESUME_NOTE"
|
|
1876
2526
|
fi
|
|
1877
2527
|
}
|
|
1878
2528
|
|
|
@@ -1881,7 +2531,7 @@ verb_continue() {
|
|
|
1881
2531
|
hr_remote_names_var
|
|
1882
2532
|
status_file="$bundle_dir/$HR_REMOTE_STATUS_FILE"
|
|
1883
2533
|
if [ ! -f "$status_file" ]; then
|
|
1884
|
-
notify failed "$branch" "The job stopped before the harness run started: $(this_run_url)"
|
|
2534
|
+
notify failed "$branch" "The job stopped before the harness run started: $(this_run_url)" "The job stopped before the run started: $(this_run_url)."
|
|
1885
2535
|
return 0
|
|
1886
2536
|
fi
|
|
1887
2537
|
decision=$(hr_remote_status_get "$status_file" decision) || decision=""
|
|
@@ -1889,7 +2539,7 @@ verb_continue() {
|
|
|
1889
2539
|
continue) continue_redispatch "$status_file" ;;
|
|
1890
2540
|
wait-poller) continue_wait_poller ;;
|
|
1891
2541
|
stop) echo "remote-run.sh: decision stop for $branch; nothing to do" ;;
|
|
1892
|
-
*) notify failed "$branch" "Not re-dispatched: status.json carries no recognised decision: $(this_run_url)" ;;
|
|
2542
|
+
*) notify failed "$branch" "Not re-dispatched: status.json carries no recognised decision: $(this_run_url)" "Not continued: the run's status carries no recognised decision." ;;
|
|
1893
2543
|
esac
|
|
1894
2544
|
return 0
|
|
1895
2545
|
}
|
|
@@ -2070,12 +2720,12 @@ poll_branch() {
|
|
|
2070
2720
|
fi
|
|
2071
2721
|
next_chain_var "$POLL_STATUS_FILE"
|
|
2072
2722
|
case $? in
|
|
2073
|
-
1) [ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: chain unreadable in status.json."; return 1 ;;
|
|
2074
|
-
2) [ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: chain limit reached ($NEXT_CHAIN over HARNESS_MAX_CHAIN $MAX_CHAIN)."; return 1 ;;
|
|
2723
|
+
1) [ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: chain unreadable in status.json." "Not resumed after the usage limit: the chain count in the run's status could not be read."; return 1 ;;
|
|
2724
|
+
2) [ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: chain limit reached ($NEXT_CHAIN over HARNESS_MAX_CHAIN $MAX_CHAIN)." "Not resumed after the usage limit: the chain limit was reached ($NEXT_CHAIN over \`HARNESS_MAX_CHAIN\` $MAX_CHAIN)."; return 1 ;;
|
|
2075
2725
|
esac
|
|
2076
2726
|
engine_value=$(hr_remote_status_get "$POLL_STATUS_FILE" engine) || engine_value=""
|
|
2077
2727
|
if ! valid_engine "$engine_value"; then
|
|
2078
|
-
[ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: engine '$engine_value' in status.json is not task, user_review or docs."
|
|
2728
|
+
[ "$may_dispatch" -eq 0 ] || notify failed "$branch" "Not resumed by the poller: engine '$engine_value' in status.json is not task, user_review or docs." "Not resumed after the usage limit: the engine '$engine_value' in the run's status is not task, user_review or docs."
|
|
2079
2729
|
return 1
|
|
2080
2730
|
fi
|
|
2081
2731
|
if [ "$may_dispatch" -eq 0 ]; then
|
|
@@ -2096,7 +2746,7 @@ poll_branch() {
|
|
|
2096
2746
|
if [ "$failures" -ge "$((10#$POLL_MAX_FAILURES))" ] \
|
|
2097
2747
|
|| [ "$now" -gt "$((10#$at + 10#$POLL_GIVE_UP_MINUTES * 60))" ]; then
|
|
2098
2748
|
poll_state_put "$branch" "$id" "$failures" 1
|
|
2099
|
-
notify paused "$branch" "The resume poller could not re-dispatch $branch ($REDISPATCH_ERR) after $failures attempts; automatic resume has stopped. Run $RESUME_HINT $branch; $(hr_github_resume_route "$branch" "$engine_value")."
|
|
2749
|
+
notify paused "$branch" "The resume poller could not re-dispatch $branch ($REDISPATCH_ERR) after $failures attempts; automatic resume has stopped. Run $RESUME_HINT $branch; $(hr_github_resume_route "$branch" "$engine_value")." "Not resumed after the usage limit: dispatching the next job failed $failures times ($REDISPATCH_ERR), and the automatic resume has stopped."
|
|
2100
2750
|
return 1
|
|
2101
2751
|
fi
|
|
2102
2752
|
poll_state_put "$branch" "$id" "$failures" ""
|
|
@@ -2139,7 +2789,7 @@ EOF
|
|
|
2139
2789
|
# poll_recheck — after the disable: one fresh listing, evaluated without
|
|
2140
2790
|
# dispatching; re-enables the poller when a branch became waiting meanwhile.
|
|
2141
2791
|
poll_recheck() {
|
|
2142
|
-
local b
|
|
2792
|
+
local b enable_err
|
|
2143
2793
|
ALL_RUNS_LISTED=0
|
|
2144
2794
|
if ! list_all_runs || ! poll_pass 0; then
|
|
2145
2795
|
echo "remote-run.sh: poll: the re-check after disabling $WORKFLOW_RESUME_FILE could not list the runs ($GH_ERR); it stays disabled"
|
|
@@ -2152,8 +2802,10 @@ poll_recheck() {
|
|
|
2152
2802
|
done
|
|
2153
2803
|
return 0
|
|
2154
2804
|
fi
|
|
2805
|
+
# Captured once: each notify's report overwrites GH_ERR.
|
|
2806
|
+
enable_err="$GH_ERR"
|
|
2155
2807
|
for b in $POLL_WAITING; do
|
|
2156
|
-
notify paused "$b" "Auto-resume is unavailable: re-enabling $WORKFLOW_RESUME_FILE failed ($
|
|
2808
|
+
notify paused "$b" "Auto-resume is unavailable: re-enabling $WORKFLOW_RESUME_FILE failed ($enable_err). Run $RESUME_HINT $b after the usage reset; $(hr_github_resume_route "$b" "")." "The automatic resume after the usage limit could not be scheduled ($enable_err). $USAGE_RESUME_NOTE"
|
|
2157
2809
|
done
|
|
2158
2810
|
}
|
|
2159
2811
|
|
|
@@ -2352,7 +3004,7 @@ remote_record_exists() {
|
|
|
2352
3004
|
}
|
|
2353
3005
|
|
|
2354
3006
|
verb_review() {
|
|
2355
|
-
local protected=0 status=0 reg="" record_wt="" use_mirror=0 state_rel names name round max=0 next rel
|
|
3007
|
+
local protected=0 status=0 reg="" record_wt="" use_mirror=0 state_rel names name round max=0 next rel note pr pushed
|
|
2356
3008
|
hr_branch_is_protected "$root" "$branch" || protected=$?
|
|
2357
3009
|
case "$protected" in
|
|
2358
3010
|
0)
|
|
@@ -2367,25 +3019,24 @@ verb_review() {
|
|
|
2367
3019
|
exit "$EXIT_REFUSED"
|
|
2368
3020
|
fi
|
|
2369
3021
|
|
|
2370
|
-
#
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
|
|
2377
|
-
|
|
2378
|
-
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
|
|
2384
|
-
|
|
2385
|
-
|
|
2386
|
-
|
|
2387
|
-
|
|
2388
|
-
esac
|
|
3022
|
+
# An unsettled branch takes no review here: a local round's file exists only
|
|
3023
|
+
# on the caller's machine, so no later collection could pick it up.
|
|
3024
|
+
branch_settled_var "" "$allow_no_run"
|
|
3025
|
+
if [ "$SETTLED" -ne 1 ]; then
|
|
3026
|
+
case "$RS_STATE" in
|
|
3027
|
+
none)
|
|
3028
|
+
echo "remote-run.sh: refused, nothing written: no \`harness run $branch\` run on GitHub" >&2 ;;
|
|
3029
|
+
paused)
|
|
3030
|
+
if [ "$RS_PAUSE_REASON" = expired ]; then
|
|
3031
|
+
echo "remote-run.sh: refused, nothing written: $RS_DETAIL" >&2
|
|
3032
|
+
else
|
|
3033
|
+
echo "remote-run.sh: refused, nothing written: $branch is paused${RS_PAUSE_REASON:+ ($RS_PAUSE_REASON)} on GitHub; a review waits until its run is completed or failed" >&2
|
|
3034
|
+
fi ;;
|
|
3035
|
+
*)
|
|
3036
|
+
echo "remote-run.sh: refused, nothing written: $branch is $RS_STATE on GitHub; a review waits until its run is completed or failed" >&2 ;;
|
|
3037
|
+
esac
|
|
3038
|
+
exit "$EXIT_REFUSED"
|
|
3039
|
+
fi
|
|
2389
3040
|
|
|
2390
3041
|
# The copy: the remote record's mirror when it is on the branch, never
|
|
2391
3042
|
# removed; else a copy this verb cuts and removes on every exit.
|
|
@@ -2446,6 +3097,7 @@ NAMES
|
|
|
2446
3097
|
[ "$status" -eq 0 ] || review_fail "committing '$rel'"
|
|
2447
3098
|
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 \
|
|
2448
3099
|
|| review_fail "pushing $branch (origin/$branch is not HEAD)"
|
|
3100
|
+
pushed=$(git -C "$worktree" rev-parse HEAD 2>/dev/null) || pushed=""
|
|
2449
3101
|
if [ "$use_mirror" -eq 0 ]; then
|
|
2450
3102
|
start_remove_copy
|
|
2451
3103
|
trap - EXIT
|
|
@@ -2458,10 +3110,41 @@ NAMES
|
|
|
2458
3110
|
dispatch_fail_note="; the review is already pushed to origin/$branch, so re-send with: remote-run.sh dispatch $branch --engine user_review"
|
|
2459
3111
|
verb_dispatch
|
|
2460
3112
|
|
|
3113
|
+
# Held until GitHub lists the dispatched run: the next job serialized behind
|
|
3114
|
+
# this one would otherwise read the branch as settled and place a second
|
|
3115
|
+
# round, whose dispatch cancels this one's pending run in the run workflow's
|
|
3116
|
+
# concurrency group.
|
|
3117
|
+
if run_by_sha_var "$pushed"; then
|
|
3118
|
+
echo "remote-run.sh: the dispatched run of $branch is listed: $RUN_BY_SHA_URL"
|
|
3119
|
+
else
|
|
3120
|
+
echo "::warning::remote-run.sh: no \`harness run $branch\` run of ${pushed:-the pushed commit} was listed after $TRIGGER_RUN_LOOKUP_TRIES lookups; the round is dispatched, but a review job reading $branch now may read it as settled"
|
|
3121
|
+
fi
|
|
3122
|
+
|
|
2461
3123
|
if remote_record_exists "$reg"; then
|
|
2462
3124
|
hr_registry_set "$reg" "$branch" status running engine user_review \
|
|
2463
3125
|
|| echo "remote-run.sh: dispatched, but the local record of $branch could not be updated" >&2
|
|
2464
3126
|
fi
|
|
3127
|
+
|
|
3128
|
+
if [ -n "$reviewers_arg" ]; then
|
|
3129
|
+
pr=""
|
|
3130
|
+
if [[ "$source_arg" =~ /pull/([0-9]+)([/?#]|$) ]]; then
|
|
3131
|
+
pr="${BASH_REMATCH[1]}"
|
|
3132
|
+
elif forge_on && forge_repo_var && forge_pr_var "$branch"; then
|
|
3133
|
+
pr="$FORGE_PR"
|
|
3134
|
+
fi
|
|
3135
|
+
note="Round $round from pull request${pr:+ #$pr}"
|
|
3136
|
+
[ -z "$source_arg" ] || note="$note ($source_arg)"
|
|
3137
|
+
note="$note by @${reviewers_arg//,/, @}"
|
|
3138
|
+
else
|
|
3139
|
+
note="Round $round"
|
|
3140
|
+
[ -z "$source_arg" ] || note="$note from $source_arg"
|
|
3141
|
+
if [ -n "$actor_arg" ]; then
|
|
3142
|
+
note="$note by @$actor_arg"
|
|
3143
|
+
else
|
|
3144
|
+
note="$note from a local session"
|
|
3145
|
+
fi
|
|
3146
|
+
fi
|
|
3147
|
+
forge_report round "$branch" "$note"
|
|
2465
3148
|
}
|
|
2466
3149
|
|
|
2467
3150
|
# unrecorded_runs — UNRECORDED: one `<branch>\t<url>` line per branch whose
|
|
@@ -2565,13 +3248,16 @@ event_field() {
|
|
|
2565
3248
|
EVENT_VALUE=${out%x}
|
|
2566
3249
|
}
|
|
2567
3250
|
|
|
2568
|
-
# trigger_finish <exit> <comment> — post <comment> on the issue
|
|
2569
|
-
#
|
|
2570
|
-
#
|
|
2571
|
-
#
|
|
2572
|
-
# cannot be
|
|
3251
|
+
# trigger_finish <exit> <comment> <event> — post <comment> on the issue with the
|
|
3252
|
+
# marker for <event> (`started` or `refused`; `branch=` is empty before a branch
|
|
3253
|
+
# is derived), remove the trigger label, on `started` set the state label
|
|
3254
|
+
# `running`, and exit <exit>. A comment that cannot be posted makes the exit 3;
|
|
3255
|
+
# a label that cannot be removed or set is a warning only. For a dispatch event,
|
|
3256
|
+
# print <comment> with no marker and append it to GITHUB_STEP_SUMMARY when set;
|
|
3257
|
+
# a summary that cannot be appended is a warning only, since stdout already
|
|
3258
|
+
# carries it.
|
|
2573
3259
|
trigger_finish() {
|
|
2574
|
-
local code="$1" body="$2" file
|
|
3260
|
+
local code="$1" body="$2" event="$3" file
|
|
2575
3261
|
if [ "$trigger_source" = dispatch ]; then
|
|
2576
3262
|
printf '%s\n' "$body"
|
|
2577
3263
|
if [ -n "${GITHUB_STEP_SUMMARY-}" ]; then
|
|
@@ -2589,7 +3275,7 @@ _Posted by the trigger job ${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REP
|
|
|
2589
3275
|
echo "::error::remote-run.sh: trigger: cannot create the comment file for issue #$issue_number under '$trigger_tmp'"
|
|
2590
3276
|
exit "$EXIT_GH"
|
|
2591
3277
|
fi
|
|
2592
|
-
printf '%s\n' "$body" >"$file"
|
|
3278
|
+
{ printf '%s\n\n' "$body"; forge_marker "$event" "$branch"; } >"$file"
|
|
2593
3279
|
if ! gh_call issue comment "$issue_number" --repo "${GITHUB_REPOSITORY-}" --body-file "$file"; then
|
|
2594
3280
|
echo "::error::remote-run.sh: trigger: the comment on issue #$issue_number could not be posted: $GH_ERR"
|
|
2595
3281
|
code="$EXIT_GH"
|
|
@@ -2598,6 +3284,11 @@ _Posted by the trigger job ${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REP
|
|
|
2598
3284
|
if ! gh_call issue edit "$issue_number" --repo "${GITHUB_REPOSITORY-}" --remove-label "$trigger_label"; then
|
|
2599
3285
|
echo "::warning::remote-run.sh: trigger: removing the label '$trigger_label' from issue #$issue_number failed: $GH_ERR"
|
|
2600
3286
|
fi
|
|
3287
|
+
if [ "$event" = started ]; then
|
|
3288
|
+
if ! { forge_repo_var && forge_set_state "$issue_number" running; }; then
|
|
3289
|
+
echo "::warning::remote-run.sh: trigger: setting the label '${STATE_LABEL_PREFIX}running' on issue #$issue_number failed: $GH_ERR"
|
|
3290
|
+
fi
|
|
3291
|
+
fi
|
|
2601
3292
|
exit "$code"
|
|
2602
3293
|
}
|
|
2603
3294
|
|
|
@@ -2606,7 +3297,7 @@ trigger_refuse() {
|
|
|
2606
3297
|
echo "remote-run.sh: trigger: refused, nothing sent: $1" >&2
|
|
2607
3298
|
trigger_finish "$EXIT_REFUSED" "No run started: $1
|
|
2608
3299
|
|
|
2609
|
-
$2"
|
|
3300
|
+
$2" refused
|
|
2610
3301
|
}
|
|
2611
3302
|
|
|
2612
3303
|
# trigger_bot_listed <login> — 0 when <login> is an exact entry of
|
|
@@ -2623,27 +3314,78 @@ trigger_bot_listed() {
|
|
|
2623
3314
|
return 1
|
|
2624
3315
|
}
|
|
2625
3316
|
|
|
2626
|
-
#
|
|
2627
|
-
#
|
|
2628
|
-
#
|
|
2629
|
-
|
|
2630
|
-
|
|
3317
|
+
# authorise_actor <login> <type> — the one actor check, shared by `trigger` and
|
|
3318
|
+
# `control`: 0 when authorised. Otherwise AUTH_WHY holds one sentence and the
|
|
3319
|
+
# status names the arm: 1 `ghost`, empty or not a login shape; 2 a non-`User`
|
|
3320
|
+
# not listed in HARNESS_TRIGGER_ALLOWED_BOTS, decided with no permission call,
|
|
3321
|
+
# because the permission API answers `none` or 404 for a bot; 3 a `User` whose
|
|
3322
|
+
# permission is not `admin` or `write` (AUTH_PERMISSION holds it); 4 that
|
|
3323
|
+
# permission call failed (GH_ERR holds why). Prints nothing and never posts.
|
|
3324
|
+
AUTH_WHY=""
|
|
3325
|
+
AUTH_PERMISSION=""
|
|
3326
|
+
authorise_actor() {
|
|
3327
|
+
local login="$1" type="$2"
|
|
3328
|
+
AUTH_WHY=""
|
|
3329
|
+
AUTH_PERMISSION=""
|
|
3330
|
+
if [ "$login" = ghost ] || [ -z "$login" ] \
|
|
3331
|
+
|| ! { [[ "$login" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]] \
|
|
3332
|
+
|| { [ "$type" = Bot ] && [[ "$login" =~ ^[A-Za-z0-9][A-Za-z0-9-]*\[bot\]$ ]]; }; }; then
|
|
3333
|
+
AUTH_WHY="the actor is an account GitHub does not name, or not a login."
|
|
3334
|
+
return 1
|
|
3335
|
+
fi
|
|
3336
|
+
if [ "$type" != User ]; then
|
|
3337
|
+
trigger_bot_listed "$login" && return 0
|
|
3338
|
+
AUTH_WHY="@$login is not a person, and is not listed in HARNESS_TRIGGER_ALLOWED_BOTS."
|
|
3339
|
+
return 2
|
|
3340
|
+
fi
|
|
3341
|
+
if ! gh_call api "repos/${GITHUB_REPOSITORY-}/collaborators/$login/permission"; then
|
|
3342
|
+
AUTH_WHY="the permission check for @$login failed ($GH_ERR)."
|
|
3343
|
+
return 4
|
|
3344
|
+
fi
|
|
3345
|
+
AUTH_PERMISSION=$(printf '%s' "$GH_OUT" | jq -r '.permission // empty' 2>/dev/null) || AUTH_PERMISSION=""
|
|
3346
|
+
case "$AUTH_PERMISSION" in
|
|
3347
|
+
admin|write) return 0 ;;
|
|
3348
|
+
esac
|
|
3349
|
+
AUTH_WHY="GitHub reports the permission of @$login as ${AUTH_PERMISSION:-nothing}, not write or admin."
|
|
3350
|
+
return 3
|
|
3351
|
+
}
|
|
3352
|
+
|
|
3353
|
+
# run_by_sha_var <sha> — RUN_BY_SHA_URL: the URL of the `harness run
|
|
3354
|
+
# <branch>` run whose `headSha` is <sha>, a commit just pushed and dispatched,
|
|
3355
|
+
# looked up at most TRIGGER_RUN_LOOKUP_TRIES times, `HARNESS_TRIGGER_LOOKUP_SECS`
|
|
3356
|
+
# apart; 1 when none was listed within that bound, and at once when <sha> is
|
|
3357
|
+
# empty. Never exits. Matched on `headSha` rather than a `createdAt` bound: the
|
|
3358
|
+
# SHA identifies this dispatch exactly and reads no runner clock, where a time
|
|
3359
|
+
# bound still admits an unrelated run created in the same second.
|
|
3360
|
+
RUN_BY_SHA_URL=""
|
|
3361
|
+
run_by_sha_var() {
|
|
3362
|
+
local sha="${1-}" try=1 secs url=""
|
|
3363
|
+
RUN_BY_SHA_URL=""
|
|
2631
3364
|
secs="${HARNESS_TRIGGER_LOOKUP_SECS-}"
|
|
2632
3365
|
case "$secs" in
|
|
2633
3366
|
''|*[!0-9]*) secs="$TRIGGER_LOOKUP_SECS_DEFAULT" ;;
|
|
2634
3367
|
esac
|
|
2635
|
-
while
|
|
2636
|
-
if gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json url,displayTitle --limit 5; then
|
|
2637
|
-
url=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" \
|
|
2638
|
-
'[.[]? | select(.displayTitle == $t) | .url | strings] | first // empty' 2>/dev/null) || url=""
|
|
3368
|
+
while [ -n "$sha" ]; do
|
|
3369
|
+
if gh_call run list --workflow "$WORKFLOW_RUN_FILE" --branch "$branch" --json url,displayTitle,headSha --limit 5; then
|
|
3370
|
+
url=$(printf '%s' "$GH_OUT" | jq -r --arg t "harness run $branch" --arg s "$sha" \
|
|
3371
|
+
'[.[]? | select(.displayTitle == $t and .headSha == $s) | .url | strings] | first // empty' 2>/dev/null) || url=""
|
|
2639
3372
|
[ -z "$url" ] || break
|
|
2640
3373
|
else
|
|
2641
|
-
echo "remote-run.sh:
|
|
3374
|
+
echo "remote-run.sh: $verb: looking up the run of $branch failed: $GH_ERR" >&2
|
|
2642
3375
|
fi
|
|
2643
3376
|
[ "$try" -lt "$TRIGGER_RUN_LOOKUP_TRIES" ] || break
|
|
2644
3377
|
try=$((try + 1))
|
|
2645
3378
|
sleep "$secs"
|
|
2646
3379
|
done
|
|
3380
|
+
RUN_BY_SHA_URL="$url"
|
|
3381
|
+
[ -n "$url" ]
|
|
3382
|
+
}
|
|
3383
|
+
|
|
3384
|
+
# trigger_run_url <sha> — `run_by_sha_var`'s URL for the commit `start` just
|
|
3385
|
+
# pushed; the branch's filtered run list when none appears. Never fails.
|
|
3386
|
+
trigger_run_url() {
|
|
3387
|
+
local url=""
|
|
3388
|
+
! run_by_sha_var "${1-}" || url="$RUN_BY_SHA_URL"
|
|
2647
3389
|
# A derived name is `[a-z0-9_]` only, so it needs no encoding in the query.
|
|
2648
3390
|
[ -n "$url" ] || url="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY-}/actions/workflows/${WORKFLOW_RUN_FILE}?query=branch%3A$branch"
|
|
2649
3391
|
printf '%s\n' "$url"
|
|
@@ -2652,7 +3394,7 @@ trigger_run_url() {
|
|
|
2652
3394
|
verb_trigger() {
|
|
2653
3395
|
local LC_ALL=C
|
|
2654
3396
|
local action label title body html_url state login sender_type source=""
|
|
2655
|
-
local forge="" target="" default name_file status
|
|
3397
|
+
local forge="" target="" default name_file status prompt errfile last url sha
|
|
2656
3398
|
local fallback retry_then retry_again task_what
|
|
2657
3399
|
case "${GITHUB_EVENT_NAME-}" in
|
|
2658
3400
|
issues) trigger_source=issue ;;
|
|
@@ -2681,6 +3423,9 @@ verb_trigger() {
|
|
|
2681
3423
|
exit "$EXIT_USAGE"
|
|
2682
3424
|
}
|
|
2683
3425
|
trigger_label="${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}"
|
|
3426
|
+
if [ -z "${HARNESS_TRIGGER_LABEL-}" ] && [ "$label" = "$LEGACY_TRIGGER_LABEL" ]; then
|
|
3427
|
+
trigger_label="$LEGACY_TRIGGER_LABEL"
|
|
3428
|
+
fi
|
|
2684
3429
|
if [ "$action" != labeled ] || [ "$label" != "$trigger_label" ]; then
|
|
2685
3430
|
echo "remote-run.sh: trigger: ignored, not the label '$trigger_label' being applied"
|
|
2686
3431
|
return 0
|
|
@@ -2740,31 +3485,19 @@ verb_trigger() {
|
|
|
2740
3485
|
trigger_refuse "this issue is not open." "Reopen it, then re-apply the label \`$trigger_label\`."
|
|
2741
3486
|
fi
|
|
2742
3487
|
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
2748
|
-
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
if gh_call api "repos/${GITHUB_REPOSITORY-}/collaborators/$login/permission"; then
|
|
2757
|
-
permission=$(printf '%s' "$GH_OUT" | jq -r '.permission // empty' 2>/dev/null) || permission=""
|
|
2758
|
-
case "$permission" in
|
|
2759
|
-
admin|write) ;;
|
|
2760
|
-
*) trigger_refuse "could not confirm write access for @$login: GitHub reports their permission as \`${permission:-nothing}\`." \
|
|
2761
|
-
"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\`." ;;
|
|
2762
|
-
esac
|
|
2763
|
-
else
|
|
2764
|
-
trigger_refuse "could not confirm write access for @$login: the permission check failed ($GH_ERR)." \
|
|
2765
|
-
"Re-apply the label \`$trigger_label\` to try again."
|
|
2766
|
-
fi
|
|
2767
|
-
fi
|
|
3488
|
+
status=0
|
|
3489
|
+
authorise_actor "$login" "$sender_type" || status=$?
|
|
3490
|
+
case "$status" in
|
|
3491
|
+
0) ;;
|
|
3492
|
+
1) trigger_refuse "the label was applied by an account GitHub does not name (a deleted account shows as \`ghost\`)." \
|
|
3493
|
+
"A collaborator with write access can re-apply the label \`$trigger_label\`." ;;
|
|
3494
|
+
2) trigger_refuse "@$login is not a person, and is not listed in the repository variable \`HARNESS_TRIGGER_ALLOWED_BOTS\`." \
|
|
3495
|
+
"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
|
+
3) trigger_refuse "could not confirm write access for @$login: GitHub reports their permission as \`${AUTH_PERMISSION:-nothing}\`." \
|
|
3497
|
+
"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\`." ;;
|
|
3498
|
+
*) trigger_refuse "could not confirm write access for @$login: the permission check failed ($GH_ERR)." \
|
|
3499
|
+
"Re-apply the label \`$trigger_label\` to try again." ;;
|
|
3500
|
+
esac
|
|
2768
3501
|
fi
|
|
2769
3502
|
|
|
2770
3503
|
# The name check reads origin/<defaultBranch>; a failed fetch leaves it to say so.
|
|
@@ -2775,7 +3508,7 @@ verb_trigger() {
|
|
|
2775
3508
|
name_file=$(mktemp "$trigger_tmp/harness-trigger-branch.XXXXXX") || trigger_refuse \
|
|
2776
3509
|
"the branch name could not be derived (mktemp failed)." "$retry_again"
|
|
2777
3510
|
status=0
|
|
2778
|
-
hr_derive_branch "$root" "$title" "$fallback" >"$name_file" || status=$?
|
|
3511
|
+
hr_derive_branch "$root" "$title" "$fallback" "" "$GH" >"$name_file" || status=$?
|
|
2779
3512
|
branch=""
|
|
2780
3513
|
IFS= read -r branch <"$name_file" || :
|
|
2781
3514
|
rm -f "$name_file"
|
|
@@ -2792,7 +3525,7 @@ verb_trigger() {
|
|
|
2792
3525
|
esac
|
|
2793
3526
|
|
|
2794
3527
|
prompt=$(mktemp "$trigger_tmp/harness-trigger-prompt.XXXXXX") || trigger_finish "$EXIT_PLACEMENT" \
|
|
2795
|
-
"No run started: the task prompt for \`$branch\` could not be written. $retry_again"
|
|
3528
|
+
"No run started: the task prompt for \`$branch\` could not be written. $retry_again" refused
|
|
2796
3529
|
if [ "$trigger_source" = issue ]; then
|
|
2797
3530
|
printf '# %s\n\n%s\n\n---\n\nStarted from %s by @%s, who applied the label `%s` at %s. This is the issue'"'"'s text at that moment; later edits to the issue do not reach this run.\n' \
|
|
2798
3531
|
"$title" "$body" "$html_url" "$login" "$trigger_label" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$prompt"
|
|
@@ -2821,7 +3554,7 @@ verb_trigger() {
|
|
|
2821
3554
|
$last
|
|
2822
3555
|
\`\`\`
|
|
2823
3556
|
|
|
2824
|
-
Start it by hand: **Actions → \`$WORKFLOW_RUN_FILE\` → Run workflow**, with *Use workflow from* set to \`$branch\`, \`action\` \`run\` and \`branch\` \`$branch\`." ;;
|
|
3557
|
+
Start it by hand: **Actions → \`$WORKFLOW_RUN_FILE\` → Run workflow**, with *Use workflow from* set to \`$branch\`, \`action\` \`run\` and \`branch\` \`$branch\`." refused ;;
|
|
2825
3558
|
*)
|
|
2826
3559
|
echo "remote-run.sh: trigger: the start of $branch failed (exit $status)" >&2
|
|
2827
3560
|
trigger_finish "$EXIT_PLACEMENT" "No run started: placing $task_what on the branch \`$branch\` failed:
|
|
@@ -2830,20 +3563,1749 @@ Start it by hand: **Actions → \`$WORKFLOW_RUN_FILE\` → Run workflow**, with
|
|
|
2830
3563
|
$last
|
|
2831
3564
|
\`\`\`
|
|
2832
3565
|
|
|
2833
|
-
$retry_again" ;;
|
|
3566
|
+
$retry_again" refused ;;
|
|
2834
3567
|
esac
|
|
2835
3568
|
|
|
2836
|
-
|
|
3569
|
+
# The ref `start`'s `hr_push_landed` confirmed equal to the pushed `HEAD`;
|
|
3570
|
+
# `start_remove_copy` deletes only the local branch.
|
|
3571
|
+
sha=$(git -C "$root" rev-parse --verify --quiet "refs/remotes/origin/$branch^{commit}") || sha=""
|
|
3572
|
+
url=$(trigger_run_url "$sha")
|
|
2837
3573
|
if [ "$trigger_source" = issue ]; then
|
|
2838
3574
|
echo "remote-run.sh: trigger: started $branch from issue #$issue_number: $url"
|
|
2839
3575
|
trigger_finish "$EXIT_OK" "Started a harness run on the branch \`$branch\`: $url
|
|
2840
3576
|
|
|
2841
|
-
The task is this issue's title and body as they were when the label \`$trigger_label\` was applied; later edits to the issue do not reach this run. Re-applying the label starts another run, on the next indexed branch."
|
|
3577
|
+
The task is this issue's title and body as they were when the label \`$trigger_label\` was applied; later edits to the issue do not reach this run. Re-applying the label starts another run, on the next indexed branch." started
|
|
2842
3578
|
fi
|
|
2843
3579
|
echo "remote-run.sh: trigger: started $branch from a repository_dispatch: $url"
|
|
2844
3580
|
trigger_finish "$EXIT_OK" "Started a harness run on the branch \`$branch\`: $url
|
|
2845
3581
|
|
|
2846
|
-
The task is the dispatch's \`client_payload\` title and body. Sending the same dispatch again starts another run, on the next indexed branch."
|
|
3582
|
+
The task is the dispatch's \`client_payload\` title and body. Sending the same dispatch again starts another run, on the next indexed branch." started
|
|
3583
|
+
}
|
|
3584
|
+
|
|
3585
|
+
# ---------------------------------------------------------------------------
|
|
3586
|
+
# THE FORGE SURFACE — the one place this script reads or writes an issue or a
|
|
3587
|
+
# pull request for a run. Every function sets globals rather than printing,
|
|
3588
|
+
# never exits, and reports a failure as one `remote-run.sh: …` line on stderr.
|
|
3589
|
+
#
|
|
3590
|
+
# THE TARGET RULE. A comment goes to the open same-repository pull request
|
|
3591
|
+
# whose head is the branch when `forge_recognised` holds for that branch, else
|
|
3592
|
+
# to the issue the run was started from (`FORGE_ISSUE`), else nowhere. The
|
|
3593
|
+
# state label goes on that issue and on that pull request, each when known.
|
|
3594
|
+
#
|
|
3595
|
+
# THE LABEL IS A VIEW, NEVER AN AUTHORITY. The run list is the authority
|
|
3596
|
+
# (`remote_state`); the harness overwrites any state label set by hand, and
|
|
3597
|
+
# nothing reads one back to decide anything.
|
|
3598
|
+
# ---------------------------------------------------------------------------
|
|
3599
|
+
|
|
3600
|
+
# forge_on — 0 when `forge` is `github` and `execution.target` is
|
|
3601
|
+
# `github-actions`; the shell mirror of `forgeTriggerApplies`.
|
|
3602
|
+
forge_on() {
|
|
3603
|
+
[ "$(hr_forge "$root" 2>/dev/null)" = github ] \
|
|
3604
|
+
&& [ "$(hr_execution_target "$root" 2>/dev/null)" = github-actions ]
|
|
3605
|
+
}
|
|
3606
|
+
|
|
3607
|
+
# forge_repo_var — FORGE_REPO (owner/name) and FORGE_SERVER, from the runner's
|
|
3608
|
+
# environment when it names the repository, else one `gh repo view`; a success
|
|
3609
|
+
# is kept for the invocation.
|
|
3610
|
+
FORGE_REPO=""
|
|
3611
|
+
FORGE_SERVER=""
|
|
3612
|
+
FORGE_REPO_KNOWN=0
|
|
3613
|
+
forge_repo_var() {
|
|
3614
|
+
local repo
|
|
3615
|
+
[ "$FORGE_REPO_KNOWN" -eq 0 ] || return 0
|
|
3616
|
+
if [ -n "${GITHUB_REPOSITORY-}" ]; then
|
|
3617
|
+
repo="$GITHUB_REPOSITORY"
|
|
3618
|
+
else
|
|
3619
|
+
if ! gh_call repo view --json nameWithOwner; then
|
|
3620
|
+
echo "remote-run.sh: reading the repository's name failed: $GH_ERR" >&2
|
|
3621
|
+
return 1
|
|
3622
|
+
fi
|
|
3623
|
+
repo=$(printf '%s' "$GH_OUT" | jq -r '.nameWithOwner // empty' 2>/dev/null) || repo=""
|
|
3624
|
+
fi
|
|
3625
|
+
# Interpolated into every API path below, so its shape is checked once here.
|
|
3626
|
+
if ! [[ "$repo" =~ ^[A-Za-z0-9._-]+/[A-Za-z0-9._-]+$ ]]; then
|
|
3627
|
+
GH_ERR="the repository name '$repo' is not owner/name"
|
|
3628
|
+
echo "remote-run.sh: reading the repository's name failed: $GH_ERR" >&2
|
|
3629
|
+
return 1
|
|
3630
|
+
fi
|
|
3631
|
+
FORGE_REPO="$repo"
|
|
3632
|
+
FORGE_SERVER="${GITHUB_SERVER_URL:-https://github.com}"
|
|
3633
|
+
FORGE_SERVER="${FORGE_SERVER%/}"
|
|
3634
|
+
FORGE_REPO_KNOWN=1
|
|
3635
|
+
}
|
|
3636
|
+
|
|
3637
|
+
# forge_fetch_branch <branch> — update refs/remotes/origin/<branch>, at most
|
|
3638
|
+
# once per branch per invocation. The refspec is explicit because a single-branch
|
|
3639
|
+
# checkout's bare `fetch origin <branch>` updates only FETCH_HEAD. A failure is
|
|
3640
|
+
# one line and tolerated.
|
|
3641
|
+
FORGE_FETCHED=" "
|
|
3642
|
+
forge_fetch_branch() {
|
|
3643
|
+
local err
|
|
3644
|
+
case "$FORGE_FETCHED" in *" $1 "*) return 0 ;; esac
|
|
3645
|
+
FORGE_FETCHED="$FORGE_FETCHED$1 "
|
|
3646
|
+
if ! err=$(git -C "$root" fetch --quiet origin "+refs/heads/$1:refs/remotes/origin/$1" 2>&1 >/dev/null); then
|
|
3647
|
+
echo "remote-run.sh: fetching origin $1 failed: ${err%%$'\n'*}" >&2
|
|
3648
|
+
fi
|
|
3649
|
+
return 0
|
|
3650
|
+
}
|
|
3651
|
+
|
|
3652
|
+
# forge_marker <event> <branch> [<question>] — the one producer of a comment's
|
|
3653
|
+
# marker line, built from COMMENT_MARKER.
|
|
3654
|
+
forge_marker() {
|
|
3655
|
+
if [ -n "${3-}" ]; then
|
|
3656
|
+
printf '%s event=%s branch=%s question=%s -->\n' "$COMMENT_MARKER" "$1" "$2" "$3"
|
|
3657
|
+
else
|
|
3658
|
+
printf '%s event=%s branch=%s -->\n' "$COMMENT_MARKER" "$1" "$2"
|
|
3659
|
+
fi
|
|
3660
|
+
}
|
|
3661
|
+
|
|
3662
|
+
# forge_issue_var <branch> — FORGE_ISSUE from the last provenance line
|
|
3663
|
+
# `verb_trigger` writes into the branch's committed task prompt, matched against
|
|
3664
|
+
# this repository's own issue URL only; empty when there is none. Also sets
|
|
3665
|
+
# FORGE_TRIGGER_LABEL, the label that same provenance line names; empty when
|
|
3666
|
+
# it names none.
|
|
3667
|
+
FORGE_ISSUE=""
|
|
3668
|
+
FORGE_TRIGGER_LABEL=""
|
|
3669
|
+
forge_issue_var() {
|
|
3670
|
+
local state_rel rel prompt line rest num prefix label
|
|
3671
|
+
FORGE_ISSUE=""
|
|
3672
|
+
FORGE_TRIGGER_LABEL=""
|
|
3673
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3674
|
+
if [ -z "$state_rel" ]; then
|
|
3675
|
+
echo "remote-run.sh: cannot resolve the state directory under '$root'" >&2
|
|
3676
|
+
return 1
|
|
3677
|
+
fi
|
|
3678
|
+
rel=$(hr_task_prompt_rel "$state_rel" "$1")
|
|
3679
|
+
prompt=$(git -C "$root" show "refs/remotes/origin/$1:$rel" 2>/dev/null) || return 0
|
|
3680
|
+
prefix="Started from $FORGE_SERVER/$FORGE_REPO/issues/"
|
|
3681
|
+
while IFS= read -r line; do
|
|
3682
|
+
case "$line" in
|
|
3683
|
+
"$prefix"*) ;;
|
|
3684
|
+
*) continue ;;
|
|
3685
|
+
esac
|
|
3686
|
+
rest=${line#"$prefix"}
|
|
3687
|
+
num=${rest%%[!0-9]*}
|
|
3688
|
+
[ -n "$num" ] || continue
|
|
3689
|
+
case "${rest#"$num"}" in
|
|
3690
|
+
' by @'*)
|
|
3691
|
+
FORGE_ISSUE="$num"
|
|
3692
|
+
FORGE_TRIGGER_LABEL=""
|
|
3693
|
+
case "$line" in
|
|
3694
|
+
*'who applied the label `'*'`'*)
|
|
3695
|
+
label=${line#*'who applied the label `'}
|
|
3696
|
+
label=${label%%'`'*}
|
|
3697
|
+
[ -z "$label" ] || FORGE_TRIGGER_LABEL="$label" ;;
|
|
3698
|
+
esac ;;
|
|
3699
|
+
esac
|
|
3700
|
+
done <<<"$prompt"
|
|
3701
|
+
return 0
|
|
3702
|
+
}
|
|
3703
|
+
|
|
3704
|
+
# forge_recognised <branch> — the harness-branch test, read from committed
|
|
3705
|
+
# state: 0 when origin's copy of the branch carries its flow-progress ledger.
|
|
3706
|
+
forge_recognised() {
|
|
3707
|
+
local state_rel
|
|
3708
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 1
|
|
3709
|
+
[ -n "$state_rel" ] || return 1
|
|
3710
|
+
git -C "$root" cat-file -e "refs/remotes/origin/$1:${state_rel%/}/flow_progress/$1_progress.md" 2>/dev/null
|
|
3711
|
+
}
|
|
3712
|
+
|
|
3713
|
+
# forge_pr_var <branch> — FORGE_PR, the open pull request whose head is
|
|
3714
|
+
# <branch> in this repository (a fork's same-named head is skipped), or empty.
|
|
3715
|
+
FORGE_PR=""
|
|
3716
|
+
forge_pr_var() {
|
|
3717
|
+
FORGE_PR=""
|
|
3718
|
+
if ! gh_call pr list --repo "$FORGE_REPO" --head "$1" --state open --json number,isCrossRepository --limit 10; then
|
|
3719
|
+
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
3720
|
+
return 1
|
|
3721
|
+
fi
|
|
3722
|
+
if ! FORGE_PR=$(printf '%s' "$GH_OUT" | jq -r \
|
|
3723
|
+
'if type == "array" then [.[] | select(.isCrossRepository == false) | .number | numbers] | first // empty else error end' 2>/dev/null); then
|
|
3724
|
+
FORGE_PR=""
|
|
3725
|
+
GH_ERR="its pr list is not the expected JSON"
|
|
3726
|
+
echo "remote-run.sh: listing the open pull requests of $1 failed: $GH_ERR" >&2
|
|
3727
|
+
return 1
|
|
3728
|
+
fi
|
|
3729
|
+
return 0
|
|
3730
|
+
}
|
|
3731
|
+
|
|
3732
|
+
# forge_comment <number> <event> <branch> <body_file> [<question>] — append the
|
|
3733
|
+
# marker to <body_file> and post it on issue or pull request <number>.
|
|
3734
|
+
forge_comment() {
|
|
3735
|
+
local number="$1" event="$2" branch="$3" file="$4" question="${5-}" status
|
|
3736
|
+
if ! { printf '\n'; forge_marker "$event" "$branch" "$question"; } >>"$file"; then
|
|
3737
|
+
echo "remote-run.sh: cannot append the marker to '$file'" >&2
|
|
3738
|
+
return 1
|
|
3739
|
+
fi
|
|
3740
|
+
gh_call api --method POST "repos/$FORGE_REPO/issues/$number/comments" -F "body=@$file"
|
|
3741
|
+
status=$?
|
|
3742
|
+
[ "$status" -eq 0 ] || echo "remote-run.sh: the $event comment on #$number could not be posted: $GH_ERR" >&2
|
|
3743
|
+
return "$status"
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
# forge_set_state <number> <state> — leave `STATE_LABEL_PREFIX<state>` as the
|
|
3747
|
+
# one state label on <number>. A failed add creates the label and retries once,
|
|
3748
|
+
# never more.
|
|
3749
|
+
forge_set_state() {
|
|
3750
|
+
local number="$1" state="$2" label names name encoded color try
|
|
3751
|
+
case " $RUN_STATES " in
|
|
3752
|
+
*" $state "*) ;;
|
|
3753
|
+
*) echo "remote-run.sh: '$state' is not one of: $RUN_STATES" >&2; return 1 ;;
|
|
3754
|
+
esac
|
|
3755
|
+
label="$STATE_LABEL_PREFIX$state"
|
|
3756
|
+
if ! gh_call api "repos/$FORGE_REPO/issues/$number/labels"; then
|
|
3757
|
+
echo "remote-run.sh: reading the labels of #$number failed: $GH_ERR" >&2
|
|
3758
|
+
return 1
|
|
3759
|
+
fi
|
|
3760
|
+
if ! names=$(printf '%s' "$GH_OUT" | jq -r --arg p "$STATE_LABEL_PREFIX" --arg t "$label" \
|
|
3761
|
+
'if type == "array" then .[] | .name | strings | select(startswith($p) and . != $t) else error end' 2>/dev/null); then
|
|
3762
|
+
GH_ERR="its label list is not the expected JSON"
|
|
3763
|
+
echo "remote-run.sh: reading the labels of #$number failed: $GH_ERR" >&2
|
|
3764
|
+
return 1
|
|
3765
|
+
fi
|
|
3766
|
+
while IFS= read -r name; do
|
|
3767
|
+
[ -n "$name" ] || continue
|
|
3768
|
+
encoded=$(jq -rn --arg s "$name" '$s|@uri')
|
|
3769
|
+
gh_call api --method DELETE "repos/$FORGE_REPO/issues/$number/labels/$encoded" \
|
|
3770
|
+
|| echo "remote-run.sh: removing the label '$name' from #$number failed: $GH_ERR" >&2
|
|
3771
|
+
done <<<"$names"
|
|
3772
|
+
for try in 1 2; do
|
|
3773
|
+
gh_call api --method POST "repos/$FORGE_REPO/issues/$number/labels" -f "labels[]=$label" && return 0
|
|
3774
|
+
if [ "$try" -eq 2 ]; then
|
|
3775
|
+
echo "remote-run.sh: adding the label '$label' to #$number failed: $GH_ERR" >&2
|
|
3776
|
+
return 1
|
|
3777
|
+
fi
|
|
3778
|
+
case "$state" in
|
|
3779
|
+
running) color=1d76db ;;
|
|
3780
|
+
parked) color=fbca04 ;;
|
|
3781
|
+
paused) color=c5def5 ;;
|
|
3782
|
+
done) color=0e8a16 ;;
|
|
3783
|
+
failed) color=b60205 ;;
|
|
3784
|
+
*) color=6a737d ;;
|
|
3785
|
+
esac
|
|
3786
|
+
gh_call api --method POST "repos/$FORGE_REPO/labels" -f "name=$label" -f "color=$color" \
|
|
3787
|
+
-f "description=Set by the harness from its run list; a hand-applied state label is overwritten." \
|
|
3788
|
+
|| echo "remote-run.sh: creating the label '$label' failed: $GH_ERR" >&2
|
|
3789
|
+
done
|
|
3790
|
+
}
|
|
3791
|
+
|
|
3792
|
+
# forge_utc <epoch> — print <epoch> as a UTC time, or nothing. `date -r` takes an
|
|
3793
|
+
# epoch on BSD and a reference file on GNU, hence the `-d @` fallback.
|
|
3794
|
+
forge_utc() {
|
|
3795
|
+
local out
|
|
3796
|
+
case "${1-}" in ''|*[!0-9]*) return 0 ;; esac
|
|
3797
|
+
out=$(date -u -r "$1" '+%Y-%m-%d %H:%M UTC' 2>/dev/null) || out=""
|
|
3798
|
+
[ -n "$out" ] || out=$(date -u -d "@$1" '+%Y-%m-%d %H:%M UTC' 2>/dev/null) || out=""
|
|
3799
|
+
printf '%s' "$out"
|
|
3800
|
+
}
|
|
3801
|
+
|
|
3802
|
+
# forge_question_body <out_file> <branch> <n> <open_count> <clar_dir> [<note>] —
|
|
3803
|
+
# write question <n>'s park comment, without its marker, into <out_file>: the
|
|
3804
|
+
# file's bytes, cut at the last whole line within QUESTION_COMMENT_MAX_BYTES
|
|
3805
|
+
# when it is over it (measured in bytes; `${#…}` counts characters).
|
|
3806
|
+
forge_question_body() {
|
|
3807
|
+
local out="$1" br="$2" n="$3" count="$4" qfile="$5/question_$3.md" note="${6-}" size cut=0 last
|
|
3808
|
+
size=$(wc -c <"$qfile") || return 1
|
|
3809
|
+
size=$((size))
|
|
3810
|
+
{
|
|
3811
|
+
printf 'The run on `%s` is waiting for an answer to question %s.\n\n' "$br" "$n"
|
|
3812
|
+
if [ "$size" -le "$QUESTION_COMMENT_MAX_BYTES" ]; then
|
|
3813
|
+
cat "$qfile"
|
|
3814
|
+
else
|
|
3815
|
+
cut=1
|
|
3816
|
+
head -c "$QUESTION_COMMENT_MAX_BYTES" "$qfile" >"$out.cut"
|
|
3817
|
+
last=$(tail -c 1 "$out.cut")
|
|
3818
|
+
# A non-empty last byte is a partial line; LC_ALL=C keeps sed from
|
|
3819
|
+
# refusing a multi-byte character the byte cut split.
|
|
3820
|
+
if [ -n "$last" ]; then LC_ALL=C sed '$d' "$out.cut"; else cat "$out.cut"; fi
|
|
3821
|
+
fi
|
|
3822
|
+
} >"$out" || return 1
|
|
3823
|
+
{
|
|
3824
|
+
[ -z "$(tail -c 1 "$out")" ] || printf '\n'
|
|
3825
|
+
[ "$cut" -eq 0 ] || printf '\nThis question was cut to fit a comment. The whole file is `%s/%s/question_%s.md` in the run'"'"'s `%s` artifact.\n' \
|
|
3826
|
+
"$HR_REMOTE_CLARIFY_DIR" "$br" "$n" "$STATE_ARTIFACT_NAME"
|
|
3827
|
+
printf '\nAnswer with a comment whose first line is `%s answer %s` and whose following lines are your answer.' "$COMMAND_HANDLE" "$n"
|
|
3828
|
+
[ "$count" -ne 1 ] || printf ' This is the only open question, so `%s` may be left out: `%s answer`.' "$n" "$COMMAND_HANDLE"
|
|
3829
|
+
printf '\n'
|
|
3830
|
+
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
3831
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
3832
|
+
} >>"$out"
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
# forge_report <event> <branch> [<note>] — one lifecycle comment (on `parked`,
|
|
3836
|
+
# one per open question) and the state label, by the target rule above.
|
|
3837
|
+
# Always 0.
|
|
3838
|
+
forge_report() {
|
|
3839
|
+
local event="$1" br="$2" note="${3-}" state reason="" resume_at="" when registry_file
|
|
3840
|
+
local target kind text tmp made_tmp="" file trigger_label stopped state_rel="" count n
|
|
3841
|
+
case "$event" in
|
|
3842
|
+
parked|park_loop) state=parked ;;
|
|
3843
|
+
paused) state=paused ;;
|
|
3844
|
+
resumed) state=running ;;
|
|
3845
|
+
failed) state=failed ;;
|
|
3846
|
+
stopped) state=stopped ;;
|
|
3847
|
+
round) state=running ;;
|
|
3848
|
+
completed)
|
|
3849
|
+
echo "remote-run.sh: report: completed is posted by deliver; nothing posted"
|
|
3850
|
+
return 0 ;;
|
|
3851
|
+
launched)
|
|
3852
|
+
echo "remote-run.sh: report: launched is the trigger's own comment; nothing posted"
|
|
3853
|
+
return 0 ;;
|
|
3854
|
+
*)
|
|
3855
|
+
echo "remote-run.sh: report: '$event' is not a reported event; nothing posted"
|
|
3856
|
+
return 0 ;;
|
|
3857
|
+
esac
|
|
3858
|
+
if ! forge_on; then
|
|
3859
|
+
echo "remote-run.sh: report: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
3860
|
+
return 0
|
|
3861
|
+
fi
|
|
3862
|
+
forge_repo_var || return 0
|
|
3863
|
+
|
|
3864
|
+
if [ "$event" = failed ]; then
|
|
3865
|
+
remote_branch_stopped "$br"
|
|
3866
|
+
stopped=$?
|
|
3867
|
+
if [ "$stopped" -eq 0 ]; then
|
|
3868
|
+
echo "remote-run.sh: report: $br was stopped, and the stop already reported the run; nothing posted"
|
|
3869
|
+
return 0
|
|
3870
|
+
fi
|
|
3871
|
+
[ "$stopped" -eq 1 ] \
|
|
3872
|
+
|| echo "remote-run.sh: report: whether $br was stopped is unknown ($GH_ERR); reporting the failure" >&2
|
|
3873
|
+
fi
|
|
3874
|
+
|
|
3875
|
+
forge_fetch_branch "$br"
|
|
3876
|
+
forge_issue_var "$br" || FORGE_ISSUE=""
|
|
3877
|
+
forge_pr_var "$br" || FORGE_PR=""
|
|
3878
|
+
if [ -n "$FORGE_PR" ] && ! forge_recognised "$br"; then
|
|
3879
|
+
echo "remote-run.sh: report: pull request #$FORGE_PR's head carries no flow-progress ledger; it is not a target"
|
|
3880
|
+
FORGE_PR=""
|
|
3881
|
+
fi
|
|
3882
|
+
if [ -n "$FORGE_PR" ]; then
|
|
3883
|
+
target="$FORGE_PR"; kind=pr
|
|
3884
|
+
elif [ -n "$FORGE_ISSUE" ]; then
|
|
3885
|
+
target="$FORGE_ISSUE"; kind=issue
|
|
3886
|
+
else
|
|
3887
|
+
echo "remote-run.sh: report: $br has no open pull request and no issue it was started from; nothing posted"
|
|
3888
|
+
return 0
|
|
3889
|
+
fi
|
|
3890
|
+
|
|
3891
|
+
# Tested with -f first: hr_registry_get creates an absent registry.
|
|
3892
|
+
registry_file=$(hr_state_path "$root" autonomous_logs/registry.json 2>/dev/null) || registry_file=""
|
|
3893
|
+
if [ -n "$registry_file" ] && [ -f "$registry_file" ]; then
|
|
3894
|
+
reason=$(hr_registry_get "$registry_file" "$br" pause_reason)
|
|
3895
|
+
resume_at=$(hr_registry_get "$registry_file" "$br" usage_resume_at)
|
|
3896
|
+
fi
|
|
3897
|
+
|
|
3898
|
+
case "$event" in
|
|
3899
|
+
paused)
|
|
3900
|
+
if [ "$reason" = usage ]; then
|
|
3901
|
+
when=$(forge_utc "$resume_at")
|
|
3902
|
+
text="The harness run on \`$br\` paused: it reached its usage limit. It resumes by itself after the limit resets${when:+, at $when}."
|
|
3903
|
+
else
|
|
3904
|
+
text="The harness run on \`$br\` paused${reason:+ (reason: \`$reason\`)}. Comment \`${COMMAND_HANDLE} resume\` to continue."
|
|
3905
|
+
fi ;;
|
|
3906
|
+
park_loop)
|
|
3907
|
+
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
|
+
resumed)
|
|
3911
|
+
text="The harness run on \`$br\` resumed." ;;
|
|
3912
|
+
failed)
|
|
3913
|
+
if [ "$kind" = pr ]; then
|
|
3914
|
+
text="The harness run on \`$br\` failed. Its log is \`run.log\` in the run's \`$STATE_ARTIFACT_NAME\` artifact. To start again, submit a review on this pull request requesting changes."
|
|
3915
|
+
else
|
|
3916
|
+
trigger_label="${FORGE_TRIGGER_LABEL:-${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}}"
|
|
3917
|
+
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
|
+
fi ;;
|
|
3919
|
+
stopped)
|
|
3920
|
+
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." ;;
|
|
3921
|
+
round)
|
|
3922
|
+
text="A user-review round started on \`$br\`; a \`completed\` comment follows when the branch is ready for review again." ;;
|
|
3923
|
+
esac
|
|
3924
|
+
|
|
3925
|
+
OPEN_QUESTIONS=""
|
|
3926
|
+
if [ "$event" = parked ]; then
|
|
3927
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
3928
|
+
[ -z "$state_rel" ] || open_questions_in "$root/${state_rel%/}"
|
|
3929
|
+
fi
|
|
3930
|
+
|
|
3931
|
+
tmp="${RUNNER_TEMP-}"
|
|
3932
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
3933
|
+
tmp=$(mktemp -d) || tmp=""
|
|
3934
|
+
made_tmp="$tmp"
|
|
3935
|
+
fi
|
|
3936
|
+
if [ -n "$OPEN_QUESTIONS" ]; then
|
|
3937
|
+
count=$(printf '%s\n' $OPEN_QUESTIONS | wc -l)
|
|
3938
|
+
count=$((count))
|
|
3939
|
+
for n in $OPEN_QUESTIONS; do
|
|
3940
|
+
if [ -n "$tmp" ] && file=$(mktemp "$tmp/harness-report-comment.XXXXXX"); then
|
|
3941
|
+
if forge_question_body "$file" "$br" "$n" "$count" "$root/${state_rel%/}/$HR_REMOTE_CLARIFY_DIR/$br" "$note"; then
|
|
3942
|
+
forge_comment "$target" "$event" "$br" "$file" "$n" || :
|
|
3943
|
+
else
|
|
3944
|
+
echo "remote-run.sh: report: cannot write question $n's comment for #$target; not posted" >&2
|
|
3945
|
+
fi
|
|
3946
|
+
rm -f "$file" "$file.cut"
|
|
3947
|
+
else
|
|
3948
|
+
echo "remote-run.sh: report: cannot create question $n's comment file for #$target; not posted" >&2
|
|
3949
|
+
fi
|
|
3950
|
+
done
|
|
3951
|
+
elif [ -n "$tmp" ] && file=$(mktemp "$tmp/harness-report-comment.XXXXXX"); then
|
|
3952
|
+
{
|
|
3953
|
+
printf '%s\n' "$text"
|
|
3954
|
+
[ -z "$note" ] || printf '\n%s\n' "$note"
|
|
3955
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
3956
|
+
} >"$file"
|
|
3957
|
+
forge_comment "$target" "$event" "$br" "$file" || :
|
|
3958
|
+
rm -f "$file"
|
|
3959
|
+
else
|
|
3960
|
+
echo "remote-run.sh: report: cannot create the comment file for #$target; no comment posted" >&2
|
|
3961
|
+
fi
|
|
3962
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
3963
|
+
|
|
3964
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" "$state" || :
|
|
3965
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" "$state" || :
|
|
3966
|
+
echo "remote-run.sh: report: $event on $br reported on #$target"
|
|
3967
|
+
return 0
|
|
3968
|
+
}
|
|
3969
|
+
|
|
3970
|
+
verb_report() {
|
|
3971
|
+
forge_report "$report_event" "$branch" "$report_note"
|
|
3972
|
+
exit "$EXIT_OK"
|
|
3973
|
+
}
|
|
3974
|
+
|
|
3975
|
+
# GitHub's refusal of a pull request created with the job's own token while the
|
|
3976
|
+
# repository's Actions setting is off (docs/github-integration-research.md -> C3).
|
|
3977
|
+
PR_CREATE_FORBIDDEN='GitHub Actions is not permitted to create or approve pull requests'
|
|
3978
|
+
# GitHub's limit on a pull request title.
|
|
3979
|
+
PR_TITLE_MAX_CHARS=256
|
|
3980
|
+
|
|
3981
|
+
# deliver_title_var <branch> — DELIVER_TITLE: the committed task prompt's first
|
|
3982
|
+
# line without its `# ` when it is a heading, cut to PR_TITLE_MAX_CHARS, else <branch>.
|
|
3983
|
+
DELIVER_TITLE=""
|
|
3984
|
+
deliver_title_var() {
|
|
3985
|
+
local state_rel first=""
|
|
3986
|
+
DELIVER_TITLE="$1"
|
|
3987
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || return 0
|
|
3988
|
+
[ -n "$state_rel" ] || return 0
|
|
3989
|
+
IFS= read -r first < <(git -C "$root" show "refs/remotes/origin/$1:$(hr_task_prompt_rel "$state_rel" "$1")" 2>/dev/null) || :
|
|
3990
|
+
case "$first" in
|
|
3991
|
+
'# '?*) DELIVER_TITLE="${first#'# '}"; DELIVER_TITLE="${DELIVER_TITLE:0:$PR_TITLE_MAX_CHARS}" ;;
|
|
3992
|
+
esac
|
|
3993
|
+
return 0
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
# deliver_pr_body <file> <branch> — the pull request's body: what it is, the
|
|
3997
|
+
# issue as a plain mention (never a closing keyword: the flow does not own the
|
|
3998
|
+
# issue's lifecycle), what a reviewer can do here, and the marker.
|
|
3999
|
+
deliver_pr_body() {
|
|
4000
|
+
local v verbs=""
|
|
4001
|
+
for v in $COMMAND_VERBS; do
|
|
4002
|
+
[ "$v" != answer ] || v="answer <n>"
|
|
4003
|
+
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
4004
|
+
done
|
|
4005
|
+
{
|
|
4006
|
+
printf 'This pull request carries the harness run on `%s`, ready for your review. The harness never merges it.\n' "$2"
|
|
4007
|
+
[ -z "$FORGE_ISSUE" ] || printf '\nStarted from #%s.\n' "$FORGE_ISSUE"
|
|
4008
|
+
printf '\nA review that requests changes starts a user-review round on this branch.\n'
|
|
4009
|
+
printf 'Comment %s to act on the run; each says when it applies (docs/github-run-control.md in the harness documentation).\n' "$verbs"
|
|
4010
|
+
printf '\n'
|
|
4011
|
+
forge_marker pull-request "$2"
|
|
4012
|
+
} >"$1"
|
|
4013
|
+
}
|
|
4014
|
+
|
|
4015
|
+
# deliver_create <base> <branch> <body_file> [--draft] — one `gh pr create`,
|
|
4016
|
+
# with HARNESS_PR_TOKEN when it is set, else the job's own token.
|
|
4017
|
+
deliver_create() {
|
|
4018
|
+
local base="$1" br="$2" body="$3"
|
|
4019
|
+
shift 3
|
|
4020
|
+
set -- pr create --repo "$FORGE_REPO" --base "$base" --head "$br" "$@" --title "$DELIVER_TITLE" --body-file "$body"
|
|
4021
|
+
if [ -n "${HARNESS_PR_TOKEN-}" ]; then
|
|
4022
|
+
gh_call_token "$HARNESS_PR_TOKEN" "$@"
|
|
4023
|
+
else
|
|
4024
|
+
gh_call "$@"
|
|
4025
|
+
fi
|
|
4026
|
+
}
|
|
4027
|
+
|
|
4028
|
+
# verb_deliver — after a `completed` bundle: find or open the branch's pull
|
|
4029
|
+
# request, post the one `completed` comment, and set `done`. Always exit 0.
|
|
4030
|
+
verb_deliver() {
|
|
4031
|
+
local status base="" pr_url="" opened=0 lookup_failed=0 create_err="" forbidden=0
|
|
4032
|
+
local tmp made_tmp="" body="" file target text compare
|
|
4033
|
+
if ! forge_on; then
|
|
4034
|
+
echo "remote-run.sh: deliver: the forge coupling is off (forge github and execution.target github-actions); nothing posted"
|
|
4035
|
+
exit "$EXIT_OK"
|
|
4036
|
+
fi
|
|
4037
|
+
hr_remote_names_var
|
|
4038
|
+
status=$(hr_remote_status_get "$bundle_dir/$HR_REMOTE_STATUS_FILE" status 2>/dev/null) || status=""
|
|
4039
|
+
if [ "$status" != completed ]; then
|
|
4040
|
+
echo "remote-run.sh: deliver: the bundle's status is '${status:-unreadable}', not completed; nothing posted"
|
|
4041
|
+
exit "$EXIT_OK"
|
|
4042
|
+
fi
|
|
4043
|
+
forge_repo_var || exit "$EXIT_OK"
|
|
4044
|
+
|
|
4045
|
+
forge_fetch_branch "$branch"
|
|
4046
|
+
forge_issue_var "$branch" || FORGE_ISSUE=""
|
|
4047
|
+
forge_pr_var "$branch" || { FORGE_PR=""; lookup_failed=1; create_err="whether a pull request is already open could not be read ($GH_ERR)"; }
|
|
4048
|
+
|
|
4049
|
+
tmp="${RUNNER_TEMP-}"
|
|
4050
|
+
if [ -z "$tmp" ] || [ ! -d "$tmp" ]; then
|
|
4051
|
+
tmp=$(mktemp -d) || tmp=""
|
|
4052
|
+
made_tmp="$tmp"
|
|
4053
|
+
fi
|
|
4054
|
+
|
|
4055
|
+
if [ -n "$FORGE_PR" ]; then
|
|
4056
|
+
pr_url="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
4057
|
+
echo "remote-run.sh: deliver: $branch already has pull request #$FORGE_PR; none opened"
|
|
4058
|
+
elif [ "$lookup_failed" -eq 0 ]; then
|
|
4059
|
+
base=$(hr_default_branch "$root" 2>/dev/null) || base=""
|
|
4060
|
+
deliver_title_var "$branch"
|
|
4061
|
+
if [ -z "$base" ]; then
|
|
4062
|
+
create_err="the configured defaultBranch could not be read"
|
|
4063
|
+
elif [ -z "$tmp" ] || ! body=$(mktemp "$tmp/harness-deliver-body.XXXXXX") || ! deliver_pr_body "$body" "$branch"; then
|
|
4064
|
+
create_err="its body file could not be written"
|
|
4065
|
+
elif deliver_create "$base" "$branch" "$body" --draft; then
|
|
4066
|
+
opened=1
|
|
4067
|
+
else
|
|
4068
|
+
create_err="$GH_ERR"
|
|
4069
|
+
case "$GH_ERR" in
|
|
4070
|
+
*"$PR_CREATE_FORBIDDEN"*) forbidden=1 ;;
|
|
4071
|
+
*)
|
|
4072
|
+
# Drafts depend on the account's plan (C3, not measured): one retry as ready.
|
|
4073
|
+
echo "remote-run.sh: deliver: opening a draft pull request failed ($GH_ERR); retrying once without --draft" >&2
|
|
4074
|
+
if deliver_create "$base" "$branch" "$body"; then
|
|
4075
|
+
opened=1
|
|
4076
|
+
else
|
|
4077
|
+
create_err="$GH_ERR"
|
|
4078
|
+
fi ;;
|
|
4079
|
+
esac
|
|
4080
|
+
fi
|
|
4081
|
+
[ -z "$body" ] || rm -f "$body"
|
|
4082
|
+
if [ "$opened" -eq 1 ]; then
|
|
4083
|
+
FORGE_PR=$(printf '%s\n' "$GH_OUT" | sed -n 's|.*/pull/\([0-9][0-9]*\).*|\1|p' | tail -n 1)
|
|
4084
|
+
if [ -n "$FORGE_PR" ]; then
|
|
4085
|
+
pr_url="$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR"
|
|
4086
|
+
echo "remote-run.sh: deliver: opened pull request #$FORGE_PR for $branch"
|
|
4087
|
+
else
|
|
4088
|
+
opened=0
|
|
4089
|
+
create_err="gh printed no pull request URL"
|
|
4090
|
+
fi
|
|
4091
|
+
fi
|
|
4092
|
+
fi
|
|
4093
|
+
|
|
4094
|
+
compare="$FORGE_SERVER/$FORGE_REPO/compare/${base:-<default branch>}...$branch?expand=1"
|
|
4095
|
+
if [ -z "$pr_url" ]; then
|
|
4096
|
+
target="$FORGE_ISSUE"
|
|
4097
|
+
if [ "$forbidden" -eq 1 ]; then
|
|
4098
|
+
text="The harness run on \`$branch\` completed, but its pull request could not be opened: GitHub Actions is not permitted to create pull requests in this repository. Turn on *$PR_CREATE_SETTING* under $PR_CREATE_SETTING_PATH, or set the \`HARNESS_GIT_TOKEN\` secret, for the next run. For this one, open the pull request from the branch: $compare"
|
|
4099
|
+
else
|
|
4100
|
+
text="The harness run on \`$branch\` completed, but its pull request could not be opened: $create_err. Open it by hand from the branch: $compare"
|
|
4101
|
+
fi
|
|
4102
|
+
elif [ "$opened" -eq 0 ]; then
|
|
4103
|
+
target="$FORGE_PR"
|
|
4104
|
+
text="The harness round on \`$branch\` finished. Review this pull request; a review that requests changes starts another round."
|
|
4105
|
+
elif [ -n "$FORGE_ISSUE" ]; then
|
|
4106
|
+
target="$FORGE_ISSUE"
|
|
4107
|
+
text="The harness run on \`$branch\` completed. Its pull request is ready for your review: $pr_url
|
|
4108
|
+
Review it there; a review that requests changes starts another round."
|
|
4109
|
+
else
|
|
4110
|
+
target="$FORGE_PR"
|
|
4111
|
+
text="The harness run on \`$branch\` completed and opened this pull request. Review it; a review that requests changes starts another round."
|
|
4112
|
+
fi
|
|
4113
|
+
if hr_phase_enabled "$root" qa; then
|
|
4114
|
+
text="$text
|
|
4115
|
+
|
|
4116
|
+
The interactive-test phase was skipped on GitHub Actions. Before merging, run \`/autonomous-sdlc-harness:branch-qa-test $branch\` locally."
|
|
4117
|
+
fi
|
|
4118
|
+
|
|
4119
|
+
if [ -z "$target" ]; then
|
|
4120
|
+
echo "remote-run.sh: deliver: $branch has no pull request and no issue it was started from; nothing posted"
|
|
4121
|
+
elif [ -n "$tmp" ] && file=$(mktemp "$tmp/harness-deliver-comment.XXXXXX"); then
|
|
4122
|
+
{
|
|
4123
|
+
printf '%s\n' "$text"
|
|
4124
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4125
|
+
} >"$file"
|
|
4126
|
+
forge_comment "$target" completed "$branch" "$file" || :
|
|
4127
|
+
rm -f "$file"
|
|
4128
|
+
else
|
|
4129
|
+
echo "remote-run.sh: deliver: cannot create the comment file for #$target; no comment posted" >&2
|
|
4130
|
+
fi
|
|
4131
|
+
[ -z "$made_tmp" ] || rmdir "$made_tmp" 2>/dev/null || :
|
|
4132
|
+
|
|
4133
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" done || :
|
|
4134
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" done || :
|
|
4135
|
+
echo "remote-run.sh: deliver: completed on $branch reported${target:+ on #$target}"
|
|
4136
|
+
exit "$EXIT_OK"
|
|
4137
|
+
}
|
|
4138
|
+
|
|
4139
|
+
# ---------------------------------------------------------------------------
|
|
4140
|
+
# `control` — the comment adapter: one GitHub event, one harness action on one
|
|
4141
|
+
# branch, through the child verbs. Event text is data, as in `trigger`.
|
|
4142
|
+
# ---------------------------------------------------------------------------
|
|
4143
|
+
|
|
4144
|
+
CONTROL_BRANCH=""
|
|
4145
|
+
CONTROL_NUMBER=""
|
|
4146
|
+
CONTROL_ACTOR=""
|
|
4147
|
+
CONTROL_VERB=""
|
|
4148
|
+
CONTROL_ARGS=""
|
|
4149
|
+
CONTROL_BODY=""
|
|
4150
|
+
control_tmp=""
|
|
4151
|
+
# Directories control_state_var created, removed on exit.
|
|
4152
|
+
control_dirs=""
|
|
4153
|
+
|
|
4154
|
+
control_cleanup() {
|
|
4155
|
+
local d
|
|
4156
|
+
for d in $control_dirs; do
|
|
4157
|
+
rm -rf -- "$d"
|
|
4158
|
+
done
|
|
4159
|
+
return 0
|
|
4160
|
+
}
|
|
4161
|
+
|
|
4162
|
+
# control_post <text> — post <text> on CONTROL_NUMBER as a `reply` comment;
|
|
4163
|
+
# 1, after an `::error::` line, when it cannot be posted.
|
|
4164
|
+
control_post() {
|
|
4165
|
+
local text="$1" file status=0
|
|
4166
|
+
if ! forge_repo_var; then
|
|
4167
|
+
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
4168
|
+
return 1
|
|
4169
|
+
fi
|
|
4170
|
+
if ! file=$(mktemp "$control_tmp/harness-control-reply.XXXXXX"); then
|
|
4171
|
+
echo "::error::remote-run.sh: control: cannot create the reply file for #$CONTROL_NUMBER under '$control_tmp'"
|
|
4172
|
+
return 1
|
|
4173
|
+
fi
|
|
4174
|
+
{
|
|
4175
|
+
printf '%s\n' "$text"
|
|
4176
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
4177
|
+
} >"$file"
|
|
4178
|
+
if ! forge_comment "$CONTROL_NUMBER" reply "$CONTROL_BRANCH" "$file"; then
|
|
4179
|
+
echo "::error::remote-run.sh: control: the reply on #$CONTROL_NUMBER could not be posted: $GH_ERR"
|
|
4180
|
+
status=1
|
|
4181
|
+
fi
|
|
4182
|
+
rm -f "$file"
|
|
4183
|
+
return "$status"
|
|
4184
|
+
}
|
|
4185
|
+
|
|
4186
|
+
# control_reply <exit> <text> — control_post <text>, then exit <exit>; a reply
|
|
4187
|
+
# that cannot be posted makes the exit 3.
|
|
4188
|
+
control_reply() {
|
|
4189
|
+
control_post "$2" || exit "$EXIT_GH"
|
|
4190
|
+
exit "$1"
|
|
4191
|
+
}
|
|
4192
|
+
|
|
4193
|
+
# control_refuse <exit> <reason> <way on> — the refusal reply, then exit.
|
|
4194
|
+
control_refuse() {
|
|
4195
|
+
echo "remote-run.sh: control: \`${CONTROL_VERB:-$COMMAND_HANDLE}\` refused: $2" >&2
|
|
4196
|
+
control_reply "$1" "@$CONTROL_ACTOR: \`${CONTROL_VERB:-$COMMAND_HANDLE}\` was not run: $2. $3"
|
|
4197
|
+
}
|
|
4198
|
+
|
|
4199
|
+
# control_child <out_file> <args...> — run this script as a child with stderr
|
|
4200
|
+
# captured; CHILD_STATUS is its exit, CHILD_LAST its last non-empty stderr line,
|
|
4201
|
+
# which is also echoed to this process's stderr.
|
|
4202
|
+
CHILD_STATUS=0
|
|
4203
|
+
CHILD_LAST=""
|
|
4204
|
+
control_child() {
|
|
4205
|
+
local out="$1" errfile
|
|
4206
|
+
shift
|
|
4207
|
+
CHILD_STATUS=0
|
|
4208
|
+
CHILD_LAST=""
|
|
4209
|
+
errfile=$(mktemp "$control_tmp/harness-control-err.XXXXXX") || errfile=/dev/null
|
|
4210
|
+
bash "$script_dir/remote-run.sh" "$@" >"$out" 2>"$errfile" || CHILD_STATUS=$?
|
|
4211
|
+
if [ "$errfile" != /dev/null ]; then
|
|
4212
|
+
cat "$errfile" >&2
|
|
4213
|
+
CHILD_LAST=$(grep -v '^[[:space:]]*$' "$errfile" | tail -n 1)
|
|
4214
|
+
rm -f "$errfile"
|
|
4215
|
+
fi
|
|
4216
|
+
[ -n "$CHILD_LAST" ] || CHILD_LAST="exit $CHILD_STATUS, no message"
|
|
4217
|
+
}
|
|
4218
|
+
|
|
4219
|
+
# control_state_var <branch> — the branch's newest remote state, read by a
|
|
4220
|
+
# `fetch` child into a fresh directory: CS_STATE, CS_REASON, CS_ENGINE,
|
|
4221
|
+
# CS_OPEN, CS_DETAIL, CS_URL, CS_RUN_STATUS and CS_DIR. 1 with CS_ERR on a
|
|
4222
|
+
# failed child.
|
|
4223
|
+
CS_STATE=""; CS_REASON=""; CS_ENGINE=""; CS_OPEN=""; CS_DETAIL=""; CS_URL=""
|
|
4224
|
+
CS_RUN_STATUS=""; CS_DIR=""; CS_ERR=""
|
|
4225
|
+
control_state_var() {
|
|
4226
|
+
local out line key value
|
|
4227
|
+
CS_STATE=""; CS_REASON=""; CS_ENGINE=""; CS_OPEN=""; CS_DETAIL=""; CS_URL=""
|
|
4228
|
+
CS_RUN_STATUS=""; CS_DIR=""; CS_ERR=""
|
|
4229
|
+
if ! CS_DIR=$(mktemp -d "$control_tmp/harness-control-fetch.XXXXXX"); then
|
|
4230
|
+
CS_DIR=""
|
|
4231
|
+
CS_ERR="a fetch directory could not be created under '$control_tmp'"
|
|
4232
|
+
return 1
|
|
4233
|
+
fi
|
|
4234
|
+
control_dirs="$control_dirs $CS_DIR"
|
|
4235
|
+
out="$CS_DIR.out"
|
|
4236
|
+
control_dirs="$control_dirs $out"
|
|
4237
|
+
control_child "$out" fetch "$1" "$CS_DIR" --repo "$root"
|
|
4238
|
+
if [ "$CHILD_STATUS" -ne 0 ]; then
|
|
4239
|
+
CS_ERR="$CHILD_LAST"
|
|
4240
|
+
return 1
|
|
4241
|
+
fi
|
|
4242
|
+
while IFS= read -r line; do
|
|
4243
|
+
key=${line%%:*}
|
|
4244
|
+
value=${line#*: }
|
|
4245
|
+
[ "$value" != "$line" ] || value=""
|
|
4246
|
+
case "$key" in
|
|
4247
|
+
state) CS_STATE="$value" ;;
|
|
4248
|
+
pause_reason) CS_REASON="$value" ;;
|
|
4249
|
+
engine) CS_ENGINE="$value" ;;
|
|
4250
|
+
open_questions) CS_OPEN="$value" ;;
|
|
4251
|
+
detail) CS_DETAIL="$value" ;;
|
|
4252
|
+
run_url) CS_URL="$value" ;;
|
|
4253
|
+
run_status) CS_RUN_STATUS="$value" ;;
|
|
4254
|
+
esac
|
|
4255
|
+
done <"$out"
|
|
4256
|
+
return 0
|
|
4257
|
+
}
|
|
4258
|
+
|
|
4259
|
+
# control_check_branch <branch> — the refusals both paths share, in order: a
|
|
4260
|
+
# branch not answered 1 by hr_branch_is_protected, then (after a fetch) one
|
|
4261
|
+
# whose origin tip carries no flow-progress ledger. Each is a reply and exit 2.
|
|
4262
|
+
control_check_branch() {
|
|
4263
|
+
local b="$1" protected=0
|
|
4264
|
+
if ! valid_branch "$b" || ! git check-ref-format --branch "$b" >/dev/null 2>&1; then
|
|
4265
|
+
control_refuse "$EXIT_REFUSED" "\`$b\` is not a valid branch name" "Comment on the pull request of the run's branch instead."
|
|
4266
|
+
fi
|
|
4267
|
+
hr_branch_is_protected "$root" "$b" || protected=$?
|
|
4268
|
+
case "$protected" in
|
|
4269
|
+
1) ;;
|
|
4270
|
+
0) control_refuse "$EXIT_REFUSED" "\`$b\` is a protected branch, which the harness never acts on" \
|
|
4271
|
+
"Comment on the pull request of the run's own branch instead." ;;
|
|
4272
|
+
*) control_refuse "$EXIT_REFUSED" "whether \`$b\` is protected could not be judged from \`harness.config.json\`" \
|
|
4273
|
+
"Fix the configuration on the default branch, then comment again." ;;
|
|
4274
|
+
esac
|
|
4275
|
+
forge_fetch_branch "$b"
|
|
4276
|
+
if ! forge_recognised "$b"; then
|
|
4277
|
+
control_refuse "$EXIT_REFUSED" "\`$b\` is not a harness branch: its tip carries no flow-progress ledger" \
|
|
4278
|
+
"Only a branch a harness run works on can be commanded."
|
|
4279
|
+
fi
|
|
4280
|
+
CONTROL_BRANCH="$b"
|
|
4281
|
+
}
|
|
4282
|
+
|
|
4283
|
+
# control_branch_from_pr <number> — the pull request's head, refused for a
|
|
4284
|
+
# fork, a pull request that is not open, and control_check_branch's refusals.
|
|
4285
|
+
control_branch_from_pr() {
|
|
4286
|
+
local head cross state
|
|
4287
|
+
if ! gh_call pr view "$1" --repo "$FORGE_REPO" --json headRefName,isCrossRepository,state; then
|
|
4288
|
+
control_refuse "$EXIT_GH" "pull request #$1 could not be read ($GH_ERR)" "Comment again to retry."
|
|
4289
|
+
fi
|
|
4290
|
+
head=$(printf '%s' "$GH_OUT" | jq -r '.headRefName // empty' 2>/dev/null) || head=""
|
|
4291
|
+
cross=$(printf '%s' "$GH_OUT" | jq -r '.isCrossRepository | tostring' 2>/dev/null) || cross=""
|
|
4292
|
+
state=$(printf '%s' "$GH_OUT" | jq -r '.state // empty' 2>/dev/null) || state=""
|
|
4293
|
+
# A fork's head is never checked out or run: this event carries this
|
|
4294
|
+
# repository's secrets (docs/github-integration-research.md -> C2).
|
|
4295
|
+
if [ "$cross" != false ]; then
|
|
4296
|
+
control_refuse "$EXIT_REFUSED" "pull request #$1 comes from a fork, and the harness never acts on a fork's pull request" \
|
|
4297
|
+
"Push the branch to this repository and open the pull request from there."
|
|
4298
|
+
fi
|
|
4299
|
+
if [ "$state" != OPEN ]; then
|
|
4300
|
+
control_refuse "$EXIT_REFUSED" "pull request #$1 is ${state:-in an unknown state}, not open" "Reopen it, then comment again."
|
|
4301
|
+
fi
|
|
4302
|
+
control_check_branch "$head"
|
|
4303
|
+
}
|
|
4304
|
+
|
|
4305
|
+
# control_branch_from_issue <number> — the branch of the issue's last genuine
|
|
4306
|
+
# start comment: by `github-actions[bot]`, its first line opening with the
|
|
4307
|
+
# trigger's start sentence and a backticked <b>, and its last non-empty line
|
|
4308
|
+
# exactly `forge_marker started <b>`. A marker anywhere else is never trusted.
|
|
4309
|
+
control_branch_from_issue() {
|
|
4310
|
+
local count i login body first last b found="" lead='Started a harness run on the branch `'
|
|
4311
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/issues/$1/comments" --jq '.[] | {login: .user.login, body: .body}'; then
|
|
4312
|
+
control_refuse "$EXIT_GH" "the comments of issue #$1 could not be read ($GH_ERR)" "Comment again to retry."
|
|
4313
|
+
fi
|
|
4314
|
+
count=$(printf '%s' "$GH_OUT" | jq -s 'length' 2>/dev/null) || count=""
|
|
4315
|
+
case "$count" in
|
|
4316
|
+
''|*[!0-9]*)
|
|
4317
|
+
GH_ERR="its comment list is not the expected JSON"
|
|
4318
|
+
control_refuse "$EXIT_GH" "the comments of issue #$1 could not be read ($GH_ERR)" "Comment again to retry." ;;
|
|
4319
|
+
esac
|
|
4320
|
+
for ((i = 0; i < count; i++)); do
|
|
4321
|
+
login=$(printf '%s' "$GH_OUT" | jq -s -r --argjson i "$i" '.[$i].login // ""' 2>/dev/null) || continue
|
|
4322
|
+
[ "$login" = 'github-actions[bot]' ] || continue
|
|
4323
|
+
body=$(printf '%s' "$GH_OUT" | jq -s -j --argjson i "$i" '.[$i].body // ""' 2>/dev/null) || continue
|
|
4324
|
+
body=${body//$'\r'$'\n'/$'\n'}
|
|
4325
|
+
body=${body%$'\r'}
|
|
4326
|
+
case "$body" in
|
|
4327
|
+
"$lead"*) ;;
|
|
4328
|
+
*) continue ;;
|
|
4329
|
+
esac
|
|
4330
|
+
b=${body#"$lead"}
|
|
4331
|
+
b=${b%%$'\n'*}
|
|
4332
|
+
case "$b" in
|
|
4333
|
+
*'`'*) b=${b%%'`'*} ;;
|
|
4334
|
+
*) continue ;;
|
|
4335
|
+
esac
|
|
4336
|
+
[ -n "$b" ] || continue
|
|
4337
|
+
last=$(printf '%s\n' "$body" | grep -v '^[[:space:]]*$' | tail -n 1)
|
|
4338
|
+
[ "$last" = "$(forge_marker started "$b")" ] || continue
|
|
4339
|
+
found="$b"
|
|
4340
|
+
done
|
|
4341
|
+
if [ -z "$found" ]; then
|
|
4342
|
+
control_refuse "$EXIT_REFUSED" "no harness run was started from this issue" \
|
|
4343
|
+
"Comment on the pull request of the run's branch instead."
|
|
4344
|
+
fi
|
|
4345
|
+
control_check_branch "$found"
|
|
4346
|
+
}
|
|
4347
|
+
|
|
4348
|
+
# control_verb_handled <verb> — 0 when an arm below carries out <verb>.
|
|
4349
|
+
control_verb_handled() {
|
|
4350
|
+
case "$1" in
|
|
4351
|
+
answer|pause|stop|resume|clear) return 0 ;;
|
|
4352
|
+
esac
|
|
4353
|
+
return 1
|
|
4354
|
+
}
|
|
4355
|
+
|
|
4356
|
+
# control_resume_dispatch <reply> [<dispatch flag>] — the resume dispatch the
|
|
4357
|
+
# local relay sends for CS_ENGINE; on 0, <reply>, then `running` on the run's
|
|
4358
|
+
# issue and pull request. An empty engine is refused, never guessed: the run
|
|
4359
|
+
# workflow's `engine` input defaults to `task`.
|
|
4360
|
+
control_resume_dispatch() {
|
|
4361
|
+
local done_text="$1" out status route clear=""
|
|
4362
|
+
shift
|
|
4363
|
+
if [ -z "$CS_ENGINE" ]; then
|
|
4364
|
+
# The run's own engine is unrecorded, so the route names the choice.
|
|
4365
|
+
route=$(hr_github_resume_route "$CONTROL_BRANCH" "<task, user_review or docs: the one the run was started with>")
|
|
4366
|
+
route=${route#or from GitHub: }
|
|
4367
|
+
[ "$#" -eq 0 ] || clear=", with park_loop_clear true as well"
|
|
4368
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (\`$CS_STATE\`${CS_REASON:+, \`$CS_REASON\`}) records no engine, and the harness does not guess one" \
|
|
4369
|
+
"Resume it with the **Run workflow** form instead: $route$clear."
|
|
4370
|
+
fi
|
|
4371
|
+
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
4372
|
+
control_child "$out" dispatch "$CONTROL_BRANCH" --engine "$CS_ENGINE" --resume pause "$@" --chain 0 --repo "$root"
|
|
4373
|
+
[ "$out" = /dev/null ] || { cat "$out"; rm -f "$out"; }
|
|
4374
|
+
case "$CHILD_STATUS" in
|
|
4375
|
+
0) ;;
|
|
4376
|
+
2) control_refuse "$EXIT_REFUSED" "the dispatch was refused ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE $CONTROL_VERB\` again once that is fixed." ;;
|
|
4377
|
+
*) control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE $CONTROL_VERB\` again to retry." ;;
|
|
4378
|
+
esac
|
|
4379
|
+
status="$EXIT_OK"
|
|
4380
|
+
control_post "$done_text" || status="$EXIT_GH"
|
|
4381
|
+
# The job posts its own `resumed` comment; the labels say `running` now.
|
|
4382
|
+
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
4383
|
+
forge_pr_var "$CONTROL_BRANCH" || FORGE_PR=""
|
|
4384
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" running || :
|
|
4385
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" running || :
|
|
4386
|
+
exit "$status"
|
|
4387
|
+
}
|
|
4388
|
+
|
|
4389
|
+
control_resume() {
|
|
4390
|
+
local open
|
|
4391
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4392
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4393
|
+
case "$CS_STATE" in
|
|
4394
|
+
paused)
|
|
4395
|
+
# Every pause reason, `expired` and `killed` included, resumes from the
|
|
4396
|
+
# committed ledger, as the local route does.
|
|
4397
|
+
control_resume_dispatch "Resume requested by @$CONTROL_ACTOR: \`$CONTROL_BRANCH\` continues from its committed ledger." ;;
|
|
4398
|
+
park_loop)
|
|
4399
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard" \
|
|
4400
|
+
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
4401
|
+
parked)
|
|
4402
|
+
open=""
|
|
4403
|
+
[ -z "$CS_OPEN" ] || open=" (open: $CS_OPEN)"
|
|
4404
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is \`parked\`, waiting for an answer$open" \
|
|
4405
|
+
"Comment \`$COMMAND_HANDLE answer <n>\` with the answer to question <n> on the lines below it." ;;
|
|
4406
|
+
running)
|
|
4407
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is already \`running\`" "Nothing needs resuming." ;;
|
|
4408
|
+
*)
|
|
4409
|
+
control_refuse "$EXIT_REFUSED" "only a paused run can be resumed, and the run on \`$CONTROL_BRANCH\` is \`${CS_STATE:-unknown}\`" \
|
|
4410
|
+
"A finished run continues by a review requesting changes on its pull request, or by applying the trigger label to its issue again." ;;
|
|
4411
|
+
esac
|
|
4412
|
+
}
|
|
4413
|
+
|
|
4414
|
+
control_clear() {
|
|
4415
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4416
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4417
|
+
if [ "$CS_STATE" != park_loop ]; then
|
|
4418
|
+
control_refuse "$EXIT_REFUSED" "there is no park-loop hold to clear: the run on \`$CONTROL_BRANCH\` is \`${CS_STATE:-unknown}\`" \
|
|
4419
|
+
"Only a run held by the park-loop guard is cleared."
|
|
4420
|
+
fi
|
|
4421
|
+
# 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; the run resumes from its committed ledger." \
|
|
4423
|
+
--park-loop-clear
|
|
4424
|
+
}
|
|
4425
|
+
|
|
4426
|
+
control_pause() {
|
|
4427
|
+
local out
|
|
4428
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4429
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4430
|
+
if [ "$CS_STATE" != running ]; then
|
|
4431
|
+
control_refuse "$EXIT_REFUSED" "only a running run can be paused, and the run on \`$CONTROL_BRANCH\` is \`$CS_STATE\`" \
|
|
4432
|
+
"Nothing needs pausing."
|
|
4433
|
+
fi
|
|
4434
|
+
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
4435
|
+
control_child "$out" pause "$CONTROL_BRANCH" --repo "$root"
|
|
4436
|
+
[ "$out" = /dev/null ] || { cat "$out"; rm -f "$out"; }
|
|
4437
|
+
if [ "$CHILD_STATUS" -ne 0 ]; then
|
|
4438
|
+
control_refuse "$EXIT_GH" "the pause could not be sent ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE pause\` again to retry."
|
|
4439
|
+
fi
|
|
4440
|
+
control_reply "$EXIT_OK" "Pause requested by @$CONTROL_ACTOR; the run on \`$CONTROL_BRANCH\` yields at its next clean checkpoint, and a paused comment follows."
|
|
4441
|
+
}
|
|
4442
|
+
|
|
4443
|
+
control_stop() {
|
|
4444
|
+
local out
|
|
4445
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4446
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4447
|
+
if [ "$CS_STATE" = none ]; then
|
|
4448
|
+
control_refuse "$EXIT_REFUSED" "there is no harness run on \`$CONTROL_BRANCH\` to stop" "Nothing needs stopping."
|
|
4449
|
+
fi
|
|
4450
|
+
out=$(mktemp "$control_tmp/harness-control-out.XXXXXX") || out=/dev/null
|
|
4451
|
+
control_child "$out" stop "$CONTROL_BRANCH" --actor "$CONTROL_ACTOR" --repo "$root"
|
|
4452
|
+
[ "$out" = /dev/null ] || { cat "$out"; rm -f "$out"; }
|
|
4453
|
+
case "$CHILD_STATUS" in
|
|
4454
|
+
0)
|
|
4455
|
+
# Always replied here: `stop`'s own `stopped` comment goes to the run's
|
|
4456
|
+
# target, which need not be the item the command was typed on.
|
|
4457
|
+
control_reply "$EXIT_OK" "Stop requested by @$CONTROL_ACTOR; the run on \`$CONTROL_BRANCH\` is stopped." ;;
|
|
4458
|
+
3)
|
|
4459
|
+
control_refuse "$EXIT_GH" "the stop of \`$CONTROL_BRANCH\` is partial ($CHILD_LAST)" \
|
|
4460
|
+
"Comment \`$COMMAND_HANDLE stop\` again to finish it." ;;
|
|
4461
|
+
*)
|
|
4462
|
+
control_refuse "$EXIT_GH" "the stop could not be sent ($CHILD_LAST)" "Comment \`$COMMAND_HANDLE stop\` again to retry." ;;
|
|
4463
|
+
esac
|
|
4464
|
+
}
|
|
4465
|
+
|
|
4466
|
+
# control_answer — one answer, one `resume: answer` dispatch with one entry. A
|
|
4467
|
+
# park left partly answered is safe: run_job stops an `answer` job whose park
|
|
4468
|
+
# is not fully answered before any session, and its bundle then carries the
|
|
4469
|
+
# `answer_<n>.md` restore wrote, so the next answer's job finds the set complete.
|
|
4470
|
+
control_answer() {
|
|
4471
|
+
local first short="" below="" text n="" v open_list="" rest="" cmds="" route form dir out status="$EXIT_OK"
|
|
4472
|
+
first=${CONTROL_ARGS%%[$' \t']*}
|
|
4473
|
+
if [[ "$first" =~ ^[1-9][0-9]*$ ]]; then
|
|
4474
|
+
n="$first"
|
|
4475
|
+
short=${CONTROL_ARGS#"$first"}
|
|
4476
|
+
short=${short#"${short%%[!$' \t']*}"}
|
|
4477
|
+
else
|
|
4478
|
+
short="$CONTROL_ARGS"
|
|
4479
|
+
fi
|
|
4480
|
+
case "$CONTROL_BODY" in
|
|
4481
|
+
*$'\n'*) below=${CONTROL_BODY#*$'\n'} ;;
|
|
4482
|
+
esac
|
|
4483
|
+
below=${below//$'\r'$'\n'/$'\n'}
|
|
4484
|
+
below=${below%$'\r'}
|
|
4485
|
+
if [[ "$below" =~ ^[[:space:]]*$ ]]; then
|
|
4486
|
+
text="$short"
|
|
4487
|
+
else
|
|
4488
|
+
text="$below"
|
|
4489
|
+
fi
|
|
4490
|
+
if [[ "$text" =~ ^[[:space:]]*$ ]]; then
|
|
4491
|
+
control_refuse "$EXIT_REFUSED" "the answer is empty" \
|
|
4492
|
+
"Comment \`$COMMAND_HANDLE answer <n>\` with the answer on the lines below it."
|
|
4493
|
+
fi
|
|
4494
|
+
|
|
4495
|
+
control_state_var "$CONTROL_BRANCH" \
|
|
4496
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($CS_ERR)" "Comment again to retry."
|
|
4497
|
+
case "$CS_STATE:$CS_REASON" in
|
|
4498
|
+
park_loop:*)
|
|
4499
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` is held by the park-loop guard, not waiting for an answer" \
|
|
4500
|
+
"Comment \`$COMMAND_HANDLE clear\` to release the hold and resume it." ;;
|
|
4501
|
+
paused:expired)
|
|
4502
|
+
# An expired bundle is reported as expired, never as no park.
|
|
4503
|
+
control_refuse "$EXIT_REFUSED" "${CS_DETAIL:-the state bundle of the run has expired}; the park's questions can no longer be answered here" \
|
|
4504
|
+
"Comment \`$COMMAND_HANDLE resume\` to resume from the committed ledger." ;;
|
|
4505
|
+
running:*)
|
|
4506
|
+
# Refused rather than queued: a newer pending run in the per-branch
|
|
4507
|
+
# concurrency group could cancel a queued one.
|
|
4508
|
+
control_refuse "$EXIT_REFUSED" "a job of the run on \`$CONTROL_BRANCH\` is in progress${CS_URL:+ ($CS_URL)}" \
|
|
4509
|
+
"Send the answer again once it finishes."
|
|
4510
|
+
;;
|
|
4511
|
+
parked:*) ;;
|
|
4512
|
+
*)
|
|
4513
|
+
control_refuse "$EXIT_REFUSED" "only a parked run can be answered, and the run on \`$CONTROL_BRANCH\` is \`${CS_STATE:-unknown}\`" \
|
|
4514
|
+
"Nothing is waiting for an answer." ;;
|
|
4515
|
+
esac
|
|
4516
|
+
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 \`parked\` with none open" \
|
|
4518
|
+
"Nothing is waiting for an answer."
|
|
4519
|
+
fi
|
|
4520
|
+
for v in $CS_OPEN; do
|
|
4521
|
+
open_list="$open_list${open_list:+, }$v"
|
|
4522
|
+
cmds="$cmds${cmds:+, }\`$COMMAND_HANDLE answer $v\`"
|
|
4523
|
+
done
|
|
4524
|
+
if [ -z "$n" ]; then
|
|
4525
|
+
case "$CS_OPEN" in
|
|
4526
|
+
*' '*)
|
|
4527
|
+
control_refuse "$EXIT_REFUSED" "questions $open_list are open, so the command must name one" \
|
|
4528
|
+
"Answer each with its own comment: $cmds." ;;
|
|
4529
|
+
esac
|
|
4530
|
+
n="$CS_OPEN"
|
|
4531
|
+
fi
|
|
4532
|
+
case " $CS_OPEN " in
|
|
4533
|
+
*" $n "*) ;;
|
|
4534
|
+
*)
|
|
4535
|
+
control_refuse "$EXIT_REFUSED" "question $n is not open; the open questions are $open_list" \
|
|
4536
|
+
"Answer one of them: $cmds." ;;
|
|
4537
|
+
esac
|
|
4538
|
+
if [ -z "$CS_ENGINE" ]; then
|
|
4539
|
+
route=$(hr_github_resume_route "$CONTROL_BRANCH" "<task, user_review or docs: the one the run was started with>")
|
|
4540
|
+
route=${route#or from GitHub: }
|
|
4541
|
+
form="resume answer and answers \`{\"$n\": \"<the answer>\"}\`"
|
|
4542
|
+
route=${route/and resume pause/$form}
|
|
4543
|
+
control_refuse "$EXIT_REFUSED" "the run on \`$CONTROL_BRANCH\` (\`parked\`) records no engine, and the harness does not guess one" \
|
|
4544
|
+
"Answer it with the **Run workflow** form instead: $route."
|
|
4545
|
+
fi
|
|
4546
|
+
|
|
4547
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-control-answer.XXXXXX"); then
|
|
4548
|
+
control_refuse "$EXIT_GH" "an answer directory could not be created under '$control_tmp'" "Comment again to retry."
|
|
4549
|
+
fi
|
|
4550
|
+
control_dirs="$control_dirs $dir"
|
|
4551
|
+
# The answer is untrusted data: written by printf, never sourced.
|
|
4552
|
+
if ! mkdir "$dir/answers" || ! printf '%s' "$text" >"$dir/answers/answer_$n.md"; then
|
|
4553
|
+
control_refuse "$EXIT_GH" "the answer could not be written under '$dir'" "Comment again to retry."
|
|
4554
|
+
fi
|
|
4555
|
+
out="$dir/dispatch.out"
|
|
4556
|
+
control_child "$out" dispatch "$CONTROL_BRANCH" --engine "$CS_ENGINE" --resume answer \
|
|
4557
|
+
--answers-from "$dir/answers" --indexes "$n" --chain 0 --repo "$root"
|
|
4558
|
+
cat "$out" 2>/dev/null || :
|
|
4559
|
+
case "$CHILD_STATUS" in
|
|
4560
|
+
0) ;;
|
|
4561
|
+
2)
|
|
4562
|
+
case "$CHILD_LAST" in
|
|
4563
|
+
*"workflow_dispatch limit"*)
|
|
4564
|
+
control_refuse "$EXIT_REFUSED" "the dispatch was refused ($CHILD_LAST)" \
|
|
4565
|
+
"Shorten the answer, or commit it to a file on \`$CONTROL_BRANCH\` and name that file in a shorter answer." ;;
|
|
4566
|
+
esac
|
|
4567
|
+
control_refuse "$EXIT_REFUSED" "the dispatch was refused ($CHILD_LAST)" \
|
|
4568
|
+
"Comment \`$COMMAND_HANDLE answer $n\` again once that is fixed." ;;
|
|
4569
|
+
*)
|
|
4570
|
+
control_refuse "$EXIT_GH" "the dispatch could not be sent ($CHILD_LAST)" \
|
|
4571
|
+
"Comment \`$COMMAND_HANDLE answer $n\` again to retry." ;;
|
|
4572
|
+
esac
|
|
4573
|
+
|
|
4574
|
+
cmds=""
|
|
4575
|
+
for v in $CS_OPEN; do
|
|
4576
|
+
[ "$v" != "$n" ] || continue
|
|
4577
|
+
rest="$rest${rest:+, }$v"
|
|
4578
|
+
cmds="$cmds${cmds:+, }\`$COMMAND_HANDLE answer $v\`"
|
|
4579
|
+
done
|
|
4580
|
+
if [ -n "$rest" ]; then
|
|
4581
|
+
# The label stays `parked` until the last answer: only that job resumes.
|
|
4582
|
+
control_reply "$EXIT_OK" "Answer to question $n received from @$CONTROL_ACTOR and sent; question(s) $rest still need an answer: $cmds."
|
|
4583
|
+
fi
|
|
4584
|
+
control_post "Answer to question $n received from @$CONTROL_ACTOR; every open question is answered, so \`$CONTROL_BRANCH\` resumes." \
|
|
4585
|
+
|| status="$EXIT_GH"
|
|
4586
|
+
forge_issue_var "$CONTROL_BRANCH" || FORGE_ISSUE=""
|
|
4587
|
+
forge_pr_var "$CONTROL_BRANCH" || FORGE_PR=""
|
|
4588
|
+
[ -z "$FORGE_ISSUE" ] || forge_set_state "$FORGE_ISSUE" running || :
|
|
4589
|
+
[ -z "$FORGE_PR" ] || forge_set_state "$FORGE_PR" running || :
|
|
4590
|
+
exit "$status"
|
|
4591
|
+
}
|
|
4592
|
+
|
|
4593
|
+
# The fields of a `pull_request_review` event control reads beyond the shared ones.
|
|
4594
|
+
REVIEW_ID=""
|
|
4595
|
+
REVIEW_URL=""
|
|
4596
|
+
REVIEW_AT=""
|
|
4597
|
+
REVIEW_HEAD=""
|
|
4598
|
+
|
|
4599
|
+
# control_review_story — refused unless origin's tip of CONTROL_BRANCH carries
|
|
4600
|
+
# its story index, which the round's statistics step reads.
|
|
4601
|
+
control_review_story() {
|
|
4602
|
+
local state_rel
|
|
4603
|
+
state_rel=$(hr_state_dir "$root" 2>/dev/null) || state_rel=""
|
|
4604
|
+
state_rel="${state_rel%/}"
|
|
4605
|
+
if [ -z "$state_rel" ] \
|
|
4606
|
+
|| ! git -C "$root" cat-file -e "refs/remotes/origin/$CONTROL_BRANCH:$state_rel/story_plans/${CONTROL_BRANCH}_story_plan.md" 2>/dev/null; then
|
|
4607
|
+
control_refuse "$EXIT_REFUSED" "\`$CONTROL_BRANCH\` carries no story index (\`$state_rel/story_plans/${CONTROL_BRANCH}_story_plan.md\`), which a user-review round reads, so the round cannot start on this branch" \
|
|
4608
|
+
"Review a branch whose run has planned its stories."
|
|
4609
|
+
fi
|
|
4610
|
+
}
|
|
4611
|
+
|
|
4612
|
+
# round_collect <pr_number> <out_file> — the cumulative round of the global
|
|
4613
|
+
# `branch`'s pull request <pr_number>, shared by `control` and `collect`: every
|
|
4614
|
+
# submitted review (never `PENDING`) that requests changes or carries a
|
|
4615
|
+
# non-blank body, and every inline comment, no earlier round consumed, by
|
|
4616
|
+
# every author `authorise_actor` accepts. What earlier rounds consumed is
|
|
4617
|
+
# read from the marker lines of their files on origin's tip; with none marked,
|
|
4618
|
+
# the boundary is the committer time of the newest round file. A comment
|
|
4619
|
+
# belonging to a pending review is pending whatever its `created_at`: a draft
|
|
4620
|
+
# comment is created before its review is submitted. RC_EVENT, set by the
|
|
4621
|
+
# caller, is the event's own review as a JSON object, merged when the listing
|
|
4622
|
+
# lacks it. Sets RC_REVIEWS (the kept reviews requesting changes only),
|
|
4623
|
+
# RC_REVIEWERS (distinct logins of every review written, comma-joined, in
|
|
4624
|
+
# order) and RC_EVENT_ROUND (the round whose marker records RC_EVENT's id).
|
|
4625
|
+
# Returns 0 with <out_file> written; 1 when no review requesting changes is
|
|
4626
|
+
# pending, writing nothing and recording nothing, so a *Comment* or *Approve*
|
|
4627
|
+
# review rides along in the next round one requesting changes starts; 3 a listing or a permission call failed; 4 the
|
|
4628
|
+
# previous rounds or <out_file> could not be read or written. RC_ERR holds why.
|
|
4629
|
+
RC_EVENT=""
|
|
4630
|
+
RC_REVIEWS=0
|
|
4631
|
+
RC_REVIEWERS=""
|
|
4632
|
+
RC_EVENT_ROUND=""
|
|
4633
|
+
RC_ERR=""
|
|
4634
|
+
# round_markers_read <event_review_id> — what the global `branch`'s rounds on
|
|
4635
|
+
# origin's tip consumed, from their marker lines: RC_SEEN_R and RC_SEEN_C (the
|
|
4636
|
+
# recorded review and comment ids, comma-wrapped), RC_MARKED_AT (the
|
|
4637
|
+
# highest-numbered marked round's `collected_at`, empty when none is marked),
|
|
4638
|
+
# RC_EVENT_ROUND (the round recording <event_review_id>), and RC_STATE_REL.
|
|
4639
|
+
# 4 with RC_ERR when a round cannot be listed or read. `control` also calls it
|
|
4640
|
+
# on its own, to name the round a review in flight is already part of.
|
|
4641
|
+
RC_SEEN_R=","
|
|
4642
|
+
RC_SEEN_C=","
|
|
4643
|
+
RC_MARKED_AT=""
|
|
4644
|
+
RC_STATE_REL=""
|
|
4645
|
+
round_markers_read() {
|
|
4646
|
+
local event_id="${1-}" names name path n line marked_max=0
|
|
4647
|
+
RC_SEEN_R=","
|
|
4648
|
+
RC_SEEN_C=","
|
|
4649
|
+
RC_MARKED_AT=""
|
|
4650
|
+
RC_EVENT_ROUND=""
|
|
4651
|
+
RC_STATE_REL=$(hr_state_dir "$root" 2>/dev/null) || RC_STATE_REL=""
|
|
4652
|
+
RC_STATE_REL="${RC_STATE_REL%/}"
|
|
4653
|
+
if [ -z "$RC_STATE_REL" ]; then
|
|
4654
|
+
RC_ERR="the state directory under '$root' could not be resolved"
|
|
4655
|
+
return 4
|
|
4656
|
+
fi
|
|
4657
|
+
if ! names=$(git -C "$root" ls-tree --name-only "refs/remotes/origin/$branch" -- "$RC_STATE_REL/user_reviews/" 2>/dev/null); then
|
|
4658
|
+
RC_ERR="the previous rounds of \`$branch\` could not be listed"
|
|
4659
|
+
return 4
|
|
4660
|
+
fi
|
|
4661
|
+
while IFS= read -r path; do
|
|
4662
|
+
name="${path##*/}"
|
|
4663
|
+
[[ "$name" =~ ^(.+)_review(_([0-9]+))?\.md$ ]] || continue
|
|
4664
|
+
[ "${BASH_REMATCH[1]}" = "$branch" ] || continue
|
|
4665
|
+
n="${BASH_REMATCH[3]:-1}"
|
|
4666
|
+
n=$((10#$n))
|
|
4667
|
+
if ! line=$(git -C "$root" show "refs/remotes/origin/$branch:$path" 2>/dev/null); then
|
|
4668
|
+
RC_ERR="the previous round \`$path\` could not be read"
|
|
4669
|
+
return 4
|
|
4670
|
+
fi
|
|
4671
|
+
# The last marker line wins; a round placed before markers carries none.
|
|
4672
|
+
line=$(printf '%s\n' "$line" | grep -F "$COMMENT_MARKER round collected_at=" | tail -n 1)
|
|
4673
|
+
[[ "$line" =~ ^"$COMMENT_MARKER round collected_at="([0-9T:Z-]+)" reviews="([0-9,]*)" comments="([0-9,]*)" -->"$ ]] || continue
|
|
4674
|
+
RC_SEEN_R="$RC_SEEN_R${BASH_REMATCH[2]}${BASH_REMATCH[2]:+,}"
|
|
4675
|
+
RC_SEEN_C="$RC_SEEN_C${BASH_REMATCH[3]}${BASH_REMATCH[3]:+,}"
|
|
4676
|
+
if [ "$n" -gt "$marked_max" ]; then
|
|
4677
|
+
marked_max="$n"
|
|
4678
|
+
RC_MARKED_AT="${BASH_REMATCH[1]}"
|
|
4679
|
+
fi
|
|
4680
|
+
case ",${BASH_REMATCH[2]}," in
|
|
4681
|
+
*",$event_id,"*) [ -z "$event_id" ] || RC_EVENT_ROUND="$n" ;;
|
|
4682
|
+
esac
|
|
4683
|
+
done <<NAMES
|
|
4684
|
+
$names
|
|
4685
|
+
NAMES
|
|
4686
|
+
return 0
|
|
4687
|
+
}
|
|
4688
|
+
|
|
4689
|
+
round_collect() {
|
|
4690
|
+
local pr="$1" file="$2" collected_at state_rel event_id=""
|
|
4691
|
+
local seen_r seen_c marked_at since="" tmp status
|
|
4692
|
+
local kept authors login type allowed="," count text
|
|
4693
|
+
RC_REVIEWS=0
|
|
4694
|
+
RC_REVIEWERS=""
|
|
4695
|
+
RC_EVENT_ROUND=""
|
|
4696
|
+
RC_ERR=""
|
|
4697
|
+
collected_at=$(date -u +%Y-%m-%dT%H:%M:%SZ)
|
|
4698
|
+
[ -z "$RC_EVENT" ] || event_id=$(printf '%s' "$RC_EVENT" | jq -r '.id // empty' 2>/dev/null) || event_id=""
|
|
4699
|
+
|
|
4700
|
+
round_markers_read "$event_id" || return 4
|
|
4701
|
+
state_rel="$RC_STATE_REL"
|
|
4702
|
+
seen_r="$RC_SEEN_R"
|
|
4703
|
+
seen_c="$RC_SEEN_C"
|
|
4704
|
+
marked_at="$RC_MARKED_AT"
|
|
4705
|
+
# Epoch seconds, so the comparison with `submitted_at` / `created_at` reads no time zone.
|
|
4706
|
+
if [ -n "$marked_at" ]; then
|
|
4707
|
+
if ! since=$(jq -n -r --arg t "$marked_at" --argjson o "$ROUND_OVERLAP_SECS" '($t | fromdateiso8601) - $o' 2>/dev/null); then
|
|
4708
|
+
RC_ERR="the previous round's collected_at '$marked_at' is not a UTC time"
|
|
4709
|
+
return 4
|
|
4710
|
+
fi
|
|
4711
|
+
elif ! since=$(git -C "$root" log -1 --format=%ct "refs/remotes/origin/$branch" -- \
|
|
4712
|
+
"$state_rel/user_reviews/${branch}_review.md" \
|
|
4713
|
+
"$state_rel/user_reviews/${branch}_review_[0-9]*.md" 2>/dev/null); then
|
|
4714
|
+
RC_ERR="the previous round of \`$branch\` could not be read"
|
|
4715
|
+
return 4
|
|
4716
|
+
fi
|
|
4717
|
+
|
|
4718
|
+
if ! tmp=$(mktemp -d); then
|
|
4719
|
+
RC_ERR="a collection directory could not be created"
|
|
4720
|
+
return 4
|
|
4721
|
+
fi
|
|
4722
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/pulls/$pr/reviews"; then
|
|
4723
|
+
RC_ERR="the reviews of pull request #$pr could not be read ($GH_ERR)"
|
|
4724
|
+
rm -rf -- "$tmp"
|
|
4725
|
+
return 3
|
|
4726
|
+
fi
|
|
4727
|
+
printf '%s' "$GH_OUT" >"$tmp/reviews.json"
|
|
4728
|
+
if ! gh_call api --paginate "repos/$FORGE_REPO/pulls/$pr/comments"; then
|
|
4729
|
+
RC_ERR="the inline comments of pull request #$pr could not be read ($GH_ERR)"
|
|
4730
|
+
rm -rf -- "$tmp"
|
|
4731
|
+
return 3
|
|
4732
|
+
fi
|
|
4733
|
+
printf '%s' "$GH_OUT" >"$tmp/comments.json"
|
|
4734
|
+
|
|
4735
|
+
# A paginated listing is one JSON array per page; jq selects, so every
|
|
4736
|
+
# listed field is data and never shell source.
|
|
4737
|
+
if ! kept=$(jq -n -c --slurpfile R "$tmp/reviews.json" --slurpfile C "$tmp/comments.json" \
|
|
4738
|
+
--argjson event "${RC_EVENT:-null}" --arg since "$since" --arg state "$REVIEW_ROUND_STATE" \
|
|
4739
|
+
--arg marker "$COMMENT_MARKER" --arg seen_r "$seen_r" --arg seen_c "$seen_c" '
|
|
4740
|
+
def flat: [ .[] | if type == "array" then .[] else error("not a page") end ];
|
|
4741
|
+
def unseen($ids): ("," + tostring + ",") as $k | ($ids | contains($k)) | not;
|
|
4742
|
+
def since_ok($t): $since == ""
|
|
4743
|
+
or ((($t // "") | try fromdateiso8601 catch 0) >= ($since | tonumber));
|
|
4744
|
+
($R | flat) as $listed
|
|
4745
|
+
| (if $event == null or any($listed[]; .id == $event.id) then $listed else $listed + [$event] end)
|
|
4746
|
+
| [ .[] | ((.state // "") | ascii_downcase) as $s
|
|
4747
|
+
| select($s != "" and $s != "pending")
|
|
4748
|
+
| select($s == $state or ((.body // "") | test("\\S")))
|
|
4749
|
+
| select(((.body // "") | contains($marker)) | not)
|
|
4750
|
+
| select(.id | unseen($seen_r))
|
|
4751
|
+
| select(since_ok(.submitted_at)) ]
|
|
4752
|
+
| sort_by([.submitted_at, .id]) as $reviews
|
|
4753
|
+
| ($reviews | map(.id)) as $ids
|
|
4754
|
+
| ($C | flat)
|
|
4755
|
+
| [ .[] | select(.id | unseen($seen_c))
|
|
4756
|
+
| select(((.body // "") | contains($marker)) | not)
|
|
4757
|
+
| select(since_ok(.created_at) or (.pull_request_review_id as $r | any($ids[]; . == $r))) ]
|
|
4758
|
+
| sort_by([.created_at, .id]) as $comments
|
|
4759
|
+
| {reviews: $reviews, comments: $comments}' 2>/dev/null); then
|
|
4760
|
+
RC_ERR="the reviews or inline comments of pull request #$pr are not the expected JSON"
|
|
4761
|
+
rm -rf -- "$tmp"
|
|
4762
|
+
return 3
|
|
4763
|
+
fi
|
|
4764
|
+
rm -rf -- "$tmp"
|
|
4765
|
+
|
|
4766
|
+
# One permission answer per distinct author; a refused author's items are
|
|
4767
|
+
# dropped with one line, and a failed call fails the collection.
|
|
4768
|
+
authors=$(printf '%s' "$kept" | jq -r '
|
|
4769
|
+
[ (.reviews[], .comments[]) | {l: (.user.login // ""), t: (.user.type // "")} ]
|
|
4770
|
+
| reduce .[] as $a ([]; if any(.[]; .l == $a.l) then . else . + [$a] end)
|
|
4771
|
+
| .[] | "\(.l)\t\(.t)"')
|
|
4772
|
+
while IFS=$'\t' read -r login type; do
|
|
4773
|
+
[ -n "$login$type" ] || continue
|
|
4774
|
+
status=0
|
|
4775
|
+
authorise_actor "$login" "$type" || status=$?
|
|
4776
|
+
case "$status" in
|
|
4777
|
+
0) allowed="$allowed$login," ;;
|
|
4778
|
+
4)
|
|
4779
|
+
RC_ERR="${AUTH_WHY%.}"
|
|
4780
|
+
return 3 ;;
|
|
4781
|
+
*)
|
|
4782
|
+
count=$(printf '%s' "$kept" | jq -r --arg l "$login" '[ (.reviews[], .comments[]) | select((.user.login // "") == $l) ] | length')
|
|
4783
|
+
echo "remote-run.sh: round: dropped $count item(s) by @${login:-(no login)}: $AUTH_WHY" ;;
|
|
4784
|
+
esac
|
|
4785
|
+
done <<AUTHORS
|
|
4786
|
+
$authors
|
|
4787
|
+
AUTHORS
|
|
4788
|
+
kept=$(printf '%s' "$kept" | jq -c --arg allowed "$allowed" '
|
|
4789
|
+
def ok: ("," + (.user.login // "") + ",") as $k | ($allowed | contains($k)) and (.user.login // "") != "";
|
|
4790
|
+
{reviews: [ .reviews[] | select(ok) ], comments: [ .comments[] | select(ok) ]}')
|
|
4791
|
+
|
|
4792
|
+
RC_REVIEWS=$(printf '%s' "$kept" | jq -r --arg s "$REVIEW_ROUND_STATE" \
|
|
4793
|
+
'[ .reviews[] | select(((.state // "") | ascii_downcase) == $s) ] | length')
|
|
4794
|
+
[ "$RC_REVIEWS" -gt 0 ] || return 1
|
|
4795
|
+
RC_REVIEWERS=$(printf '%s' "$kept" | jq -r '
|
|
4796
|
+
reduce (.reviews[] | .user.login) as $l ([]; if any(.[]; . == $l) then . else . + [$l] end) | join(",")')
|
|
4797
|
+
|
|
4798
|
+
# A hunk's fence is one backtick longer than its longest backtick run, at
|
|
4799
|
+
# least three. The trailing `x` keeps the text's final newline through the
|
|
4800
|
+
# substitution.
|
|
4801
|
+
if ! text=$(printf '%s' "$kept" | jq -j --arg pr "$pr" --arg at "$collected_at" --arg marker "$COMMENT_MARKER" --arg state "$REVIEW_ROUND_STATE" '
|
|
4802
|
+
def nl: if endswith("\n") then . else . + "\n" end;
|
|
4803
|
+
.reviews as $rv | .comments as $cm
|
|
4804
|
+
| ($rv | map(
|
|
4805
|
+
((.state // "") | ascii_downcase) as $s
|
|
4806
|
+
| (" pull request #" + $pr + " (" + (.html_url // "") + ") at " + (.submitted_at // "")) as $on
|
|
4807
|
+
| "## Review by @" + .user.login + "\n\n"
|
|
4808
|
+
+ (if (.body // "") == "" then "(The review carries no summary.)\n" else (.body | nl) end)
|
|
4809
|
+
+ "\n"
|
|
4810
|
+
+ (if $s == $state then "Requested changes on" + $on + ".\n"
|
|
4811
|
+
elif $s == "commented" then "Commented on" + $on + ".\n"
|
|
4812
|
+
elif $s == "approved" then "Approved" + $on + ".\n"
|
|
4813
|
+
elif $s == "dismissed" then "Reviewed" + $on + "; the review has since been dismissed.\n"
|
|
4814
|
+
else "Reviewed" + $on + " (state " + $s + ").\n" end)
|
|
4815
|
+
) | join("\n"))
|
|
4816
|
+
+ (if ($cm | length) == 0 then "" else
|
|
4817
|
+
"\n## Inline comments\n" + ($cm | map(
|
|
4818
|
+
(.diff_hunk // "") as $h
|
|
4819
|
+
| (([$h | match("`+"; "g") | .length] | max) // 0) as $m
|
|
4820
|
+
| ("`" * ([$m + 1, 3] | max)) as $f
|
|
4821
|
+
| "\n### `" + (.path // "") + "`"
|
|
4822
|
+
+ (if .line != null then ", line \(.line)"
|
|
4823
|
+
elif .original_line != null then ", original line \(.original_line) (outdated)"
|
|
4824
|
+
else "" end)
|
|
4825
|
+
+ "\n\nMade on commit `" + (.original_commit_id // .commit_id // "") + "`.\n"
|
|
4826
|
+
+ "By @" + .user.login + ": " + (.html_url // "") + "\n\n"
|
|
4827
|
+
+ ((.body // "") | nl)
|
|
4828
|
+
+ "\n" + $f + "diff\n" + ($h | nl) + $f + "\n"
|
|
4829
|
+
) | join(""))
|
|
4830
|
+
end)
|
|
4831
|
+
+ "\n" + $marker + " round collected_at=" + $at
|
|
4832
|
+
+ " reviews=" + ($rv | map(.id | tostring) | join(","))
|
|
4833
|
+
+ " comments=" + ($cm | map(.id | tostring) | join(",")) + " -->\n"
|
|
4834
|
+
+ "x"' 2>/dev/null); then
|
|
4835
|
+
RC_ERR="the round could not be rendered"
|
|
4836
|
+
return 4
|
|
4837
|
+
fi
|
|
4838
|
+
if ! printf '%s' "${text%x}" >"$file"; then
|
|
4839
|
+
RC_ERR="the round could not be written to '$file'"
|
|
4840
|
+
return 4
|
|
4841
|
+
fi
|
|
4842
|
+
return 0
|
|
4843
|
+
}
|
|
4844
|
+
|
|
4845
|
+
# The way on of every review reply that started nothing: a review is never
|
|
4846
|
+
# resubmitted to be kept, only to retry now.
|
|
4847
|
+
REVIEW_RETRY_WAY="The reviews stay on the pull request and are collected by the next round; submit any review requesting changes to retry now."
|
|
4848
|
+
|
|
4849
|
+
# control_settled_var — `branch_settled_var` for CONTROL_BRANCH, with no run
|
|
4850
|
+
# counted as settled, run in a command substitution so that its exit on a
|
|
4851
|
+
# failed read reaches this process as a status, not as an exit with no reply:
|
|
4852
|
+
# BS_SETTLED, BS_STATE and BS_REASON; 1 with BS_ERR, its last stderr line, on a
|
|
4853
|
+
# failed read.
|
|
4854
|
+
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
4855
|
+
control_settled_var() {
|
|
4856
|
+
local dir errfile line status=0
|
|
4857
|
+
BS_SETTLED=0; BS_STATE=""; BS_REASON=""; BS_ERR=""
|
|
4858
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-control-settled.XXXXXX"); then
|
|
4859
|
+
BS_ERR="a state directory could not be created under '$control_tmp'"
|
|
4860
|
+
return 1
|
|
4861
|
+
fi
|
|
4862
|
+
errfile="$dir.err"
|
|
4863
|
+
control_dirs="$control_dirs $dir $errfile"
|
|
4864
|
+
branch="$CONTROL_BRANCH"
|
|
4865
|
+
line=$(branch_settled_var "$dir" 1 >/dev/null 2>"$errfile" \
|
|
4866
|
+
&& printf '%s|%s|%s\n' "$SETTLED" "$RS_STATE" "$RS_PAUSE_REASON") || status=$?
|
|
4867
|
+
cat "$errfile" >&2 2>/dev/null || :
|
|
4868
|
+
if [ "$status" -ne 0 ] || [ -z "$line" ]; then
|
|
4869
|
+
BS_ERR=$(grep -v '^[[:space:]]*$' "$errfile" 2>/dev/null | tail -n 1)
|
|
4870
|
+
[ -n "$BS_ERR" ] || BS_ERR="exit $status, no message"
|
|
4871
|
+
return 1
|
|
4872
|
+
fi
|
|
4873
|
+
IFS='|' read -r BS_SETTLED BS_STATE BS_REASON <<<"$line"
|
|
4874
|
+
return 0
|
|
4875
|
+
}
|
|
4876
|
+
|
|
4877
|
+
# control_review_in_flight — the reply to a review while CONTROL_BRANCH is in
|
|
4878
|
+
# flight (BS_STATE; empty when unknown), then exit 0: never a refusal, nothing
|
|
4879
|
+
# pushed or dispatched. Names the round whose marker on origin's tip already
|
|
4880
|
+
# records the review, else says it was collected, with the state's way on.
|
|
4881
|
+
control_review_in_flight() {
|
|
4882
|
+
local state="in flight" way=""
|
|
4883
|
+
if [ -n "$BS_STATE" ]; then
|
|
4884
|
+
state="\`$BS_STATE\`"
|
|
4885
|
+
[ -z "$BS_REASON" ] || state="$state (\`$BS_REASON\`)"
|
|
4886
|
+
fi
|
|
4887
|
+
case "$BS_STATE" in
|
|
4888
|
+
parked)
|
|
4889
|
+
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." ;;
|
|
4890
|
+
park_loop)
|
|
4891
|
+
way=" The run is held by the park-loop guard: comment \`$COMMAND_HANDLE clear\` to release the hold." ;;
|
|
4892
|
+
paused)
|
|
4893
|
+
if [ "$BS_REASON" = usage ]; then
|
|
4894
|
+
way=" The run resumes by itself once the usage limit resets."
|
|
4895
|
+
else
|
|
4896
|
+
way=" Comment \`$COMMAND_HANDLE resume\` to resume the run from its committed ledger."
|
|
4897
|
+
fi ;;
|
|
4898
|
+
esac
|
|
4899
|
+
branch="$CONTROL_BRANCH"
|
|
4900
|
+
if round_markers_read "$REVIEW_ID" && [ -n "$RC_EVENT_ROUND" ]; then
|
|
4901
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: your review is part of round $RC_EVENT_ROUND, which is $state on \`$CONTROL_BRANCH\`."
|
|
4902
|
+
fi
|
|
4903
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: your review was collected. \`$CONTROL_BRANCH\` is $state; when that run finishes, the next user-review round starts by itself from every review requesting changes and every inline comment left since the previous round, yours included. Nothing needs to be submitted again.$way"
|
|
4904
|
+
}
|
|
4905
|
+
|
|
4906
|
+
# control_review — a review requesting changes. In flight: acknowledged and
|
|
4907
|
+
# left on the pull request for the next round. Settled: the cumulative round,
|
|
4908
|
+
# placed and dispatched by a `review` child, the local relay's own verb.
|
|
4909
|
+
control_review() {
|
|
4910
|
+
local dir file out status
|
|
4911
|
+
control_review_story
|
|
4912
|
+
control_settled_var \
|
|
4913
|
+
|| control_refuse "$EXIT_GH" "the state of the run on \`$CONTROL_BRANCH\` could not be read ($BS_ERR)" "$REVIEW_RETRY_WAY"
|
|
4914
|
+
[ "$BS_SETTLED" = 1 ] || control_review_in_flight
|
|
4915
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-control-review.XXXXXX"); then
|
|
4916
|
+
control_refuse "$EXIT_PLACEMENT" "a round directory could not be created under '$control_tmp', so nothing was dispatched" \
|
|
4917
|
+
"$REVIEW_RETRY_WAY"
|
|
4918
|
+
fi
|
|
4919
|
+
control_dirs="$control_dirs $dir"
|
|
4920
|
+
file="$dir/review.md"
|
|
4921
|
+
if ! RC_EVENT=$(jq -n -c --argjson id "$REVIEW_ID" --arg body "$CONTROL_BODY" --arg url "$REVIEW_URL" \
|
|
4922
|
+
--arg at "$REVIEW_AT" --arg login "$CONTROL_ACTOR" --arg type "$CONTROL_SENDER_TYPE" \
|
|
4923
|
+
'{id: $id, state: "CHANGES_REQUESTED", body: $body, html_url: $url, submitted_at: $at, user: {login: $login, type: $type}}'); then
|
|
4924
|
+
control_refuse "$EXIT_PLACEMENT" "the review could not be read into the round, so nothing was dispatched" \
|
|
4925
|
+
"$REVIEW_RETRY_WAY"
|
|
4926
|
+
fi
|
|
4927
|
+
branch="$CONTROL_BRANCH"
|
|
4928
|
+
status=0
|
|
4929
|
+
round_collect "$CONTROL_NUMBER" "$file" || status=$?
|
|
4930
|
+
case "$status" in
|
|
4931
|
+
0) ;;
|
|
4932
|
+
1)
|
|
4933
|
+
if [ -n "$RC_EVENT_ROUND" ]; then
|
|
4934
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: your review is part of round $RC_EVENT_ROUND of \`$CONTROL_BRANCH\`; nothing new was collected."
|
|
4935
|
+
fi
|
|
4936
|
+
control_reply "$EXIT_OK" "@$CONTROL_ACTOR: no review requesting changes is pending on pull request #$CONTROL_NUMBER since the previous round of \`$CONTROL_BRANCH\`, so no round was started." ;;
|
|
4937
|
+
3)
|
|
4938
|
+
control_refuse "$EXIT_GH" "the round could not be collected: $RC_ERR" "$REVIEW_RETRY_WAY" ;;
|
|
4939
|
+
*)
|
|
4940
|
+
control_refuse "$EXIT_PLACEMENT" "the round could not be collected: $RC_ERR, so nothing was dispatched" \
|
|
4941
|
+
"$REVIEW_RETRY_WAY" ;;
|
|
4942
|
+
esac
|
|
4943
|
+
out="$dir/review.out"
|
|
4944
|
+
set -- review "$CONTROL_BRANCH" --review-file "$file" --allow-no-run --reviewers "$RC_REVIEWERS"
|
|
4945
|
+
case "$FORGE_SERVER" in
|
|
4946
|
+
https://*) set -- "$@" --source "$FORGE_SERVER/$FORGE_REPO/pull/$CONTROL_NUMBER" ;;
|
|
4947
|
+
esac
|
|
4948
|
+
control_child "$out" "$@" --repo "$root"
|
|
4949
|
+
cat "$out" 2>/dev/null || :
|
|
4950
|
+
case "$CHILD_STATUS" in
|
|
4951
|
+
0) exit "$EXIT_OK" ;;
|
|
4952
|
+
2)
|
|
4953
|
+
# The branch became unsettled between the two reads: a run started in
|
|
4954
|
+
# between, so the review waits for it like any review in flight.
|
|
4955
|
+
control_settled_var && [ "$BS_SETTLED" != 1 ] || { BS_STATE=""; BS_REASON=""; }
|
|
4956
|
+
control_review_in_flight ;;
|
|
4957
|
+
3)
|
|
4958
|
+
control_refuse "$EXIT_GH" "the round is pushed but its dispatch failed ($CHILD_LAST)" \
|
|
4959
|
+
"Re-send the dispatch as that line says." ;;
|
|
4960
|
+
4)
|
|
4961
|
+
control_refuse "$EXIT_PLACEMENT" "placing the round failed and nothing was dispatched ($CHILD_LAST)" \
|
|
4962
|
+
"$REVIEW_RETRY_WAY" ;;
|
|
4963
|
+
*)
|
|
4964
|
+
control_refuse "$EXIT_GH" "the round could not be started ($CHILD_LAST)" "$REVIEW_RETRY_WAY" ;;
|
|
4965
|
+
esac
|
|
4966
|
+
}
|
|
4967
|
+
|
|
4968
|
+
# control_review_intake — read a `pull_request_review` event; returns 1 when it
|
|
4969
|
+
# is ignored, after one line.
|
|
4970
|
+
control_review_intake() {
|
|
4971
|
+
local action state head_repo
|
|
4972
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
4973
|
+
&& event_field '.review.state // ""' && state="$EVENT_VALUE" \
|
|
4974
|
+
&& event_field '.review.body // ""' && CONTROL_BODY="$EVENT_VALUE" \
|
|
4975
|
+
&& event_field '.review.id // ""' && REVIEW_ID="$EVENT_VALUE" \
|
|
4976
|
+
&& event_field '.review.html_url // ""' && REVIEW_URL="$EVENT_VALUE" \
|
|
4977
|
+
&& event_field '.review.submitted_at // ""' && REVIEW_AT="$EVENT_VALUE" \
|
|
4978
|
+
&& event_field '.pull_request.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
4979
|
+
&& event_field '.pull_request.head.ref // ""' && REVIEW_HEAD="$EVENT_VALUE" \
|
|
4980
|
+
&& event_field '.pull_request.head.repo.full_name // ""' && head_repo="$EVENT_VALUE" \
|
|
4981
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
4982
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || {
|
|
4983
|
+
echo "remote-run.sh: control: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
4984
|
+
exit "$EXIT_USAGE"
|
|
4985
|
+
}
|
|
4986
|
+
if [ "$action" != submitted ]; then
|
|
4987
|
+
echo "remote-run.sh: control: ignored, a review $action, not submitted"
|
|
4988
|
+
return 1
|
|
4989
|
+
fi
|
|
4990
|
+
# The REST API reports states in uppercase, the webhook in lowercase.
|
|
4991
|
+
if [ "$(printf '%s' "$state" | tr '[:upper:]' '[:lower:]')" != "$REVIEW_ROUND_STATE" ]; then
|
|
4992
|
+
echo "remote-run.sh: control: ignored, a review whose state is ${state:-empty}, not $REVIEW_ROUND_STATE"
|
|
4993
|
+
return 1
|
|
4994
|
+
fi
|
|
4995
|
+
case "$CONTROL_BODY" in
|
|
4996
|
+
*"$COMMENT_MARKER"*)
|
|
4997
|
+
echo "remote-run.sh: control: ignored, a review carrying the harness's marker"
|
|
4998
|
+
return 1 ;;
|
|
4999
|
+
esac
|
|
5000
|
+
# A fork's review job holds a read-only token, so it is not even replied to.
|
|
5001
|
+
if [ -z "$head_repo" ] || [ "$head_repo" != "${GITHUB_REPOSITORY-}" ]; then
|
|
5002
|
+
echo "remote-run.sh: control: ignored, a review of a head in ${head_repo:-an unnamed repository}, not ${GITHUB_REPOSITORY:-this repository}"
|
|
5003
|
+
return 1
|
|
5004
|
+
fi
|
|
5005
|
+
case "$REVIEW_ID" in
|
|
5006
|
+
''|*[!0-9]*|0*)
|
|
5007
|
+
echo "remote-run.sh: control: the event carries no review id" >&2
|
|
5008
|
+
exit "$EXIT_USAGE" ;;
|
|
5009
|
+
esac
|
|
5010
|
+
CONTROL_VERB=review
|
|
5011
|
+
return 0
|
|
5012
|
+
}
|
|
5013
|
+
|
|
5014
|
+
# control_comment_intake — read an `issue_comment` event; returns 1 when it is
|
|
5015
|
+
# ignored, after one line.
|
|
5016
|
+
CONTROL_SENDER_TYPE=""
|
|
5017
|
+
CONTROL_IS_PR=""
|
|
5018
|
+
control_comment_intake() {
|
|
5019
|
+
local action first word rest handle
|
|
5020
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
5021
|
+
&& event_field '.comment.body // ""' && CONTROL_BODY="$EVENT_VALUE" \
|
|
5022
|
+
&& event_field '.issue.number // ""' && CONTROL_NUMBER="$EVENT_VALUE" \
|
|
5023
|
+
&& event_field '.issue.pull_request.url // ""' && CONTROL_IS_PR="$EVENT_VALUE" \
|
|
5024
|
+
&& event_field '.sender.login // ""' && CONTROL_ACTOR="$EVENT_VALUE" \
|
|
5025
|
+
&& event_field '.sender.type // ""' && CONTROL_SENDER_TYPE="$EVENT_VALUE"; } || {
|
|
5026
|
+
echo "remote-run.sh: control: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
5027
|
+
exit "$EXIT_USAGE"
|
|
5028
|
+
}
|
|
5029
|
+
|
|
5030
|
+
if [ "$action" != created ]; then
|
|
5031
|
+
echo "remote-run.sh: control: ignored, a comment $action, not created"
|
|
5032
|
+
return 1
|
|
5033
|
+
fi
|
|
5034
|
+
case "$CONTROL_BODY" in
|
|
5035
|
+
*"$COMMENT_MARKER"*)
|
|
5036
|
+
echo "remote-run.sh: control: ignored, a comment the harness posted"
|
|
5037
|
+
return 1 ;;
|
|
5038
|
+
esac
|
|
5039
|
+
first=${CONTROL_BODY%%$'\n'*}
|
|
5040
|
+
first=${first%$'\r'}
|
|
5041
|
+
first=${first#"${first%%[!$' \t']*}"}
|
|
5042
|
+
word=${first%%[$' \t']*}
|
|
5043
|
+
rest=${first#"$word"}
|
|
5044
|
+
rest=${rest#"${rest%%[!$' \t']*}"}
|
|
5045
|
+
handle=$(printf '%s' "$COMMAND_HANDLE" | tr '[:upper:]' '[:lower:]')
|
|
5046
|
+
if [ "$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')" != "$handle" ]; then
|
|
5047
|
+
echo "remote-run.sh: control: ignored, the first line does not open with $COMMAND_HANDLE"
|
|
5048
|
+
return 1
|
|
5049
|
+
fi
|
|
5050
|
+
word=${rest%%[$' \t']*}
|
|
5051
|
+
CONTROL_VERB=$(printf '%s' "$word" | tr '[:upper:]' '[:lower:]')
|
|
5052
|
+
CONTROL_ARGS=${rest#"$word"}
|
|
5053
|
+
CONTROL_ARGS=${CONTROL_ARGS#"${CONTROL_ARGS%%[!$' \t']*}"}
|
|
5054
|
+
return 0
|
|
5055
|
+
}
|
|
5056
|
+
|
|
5057
|
+
verb_control() {
|
|
5058
|
+
local LC_ALL=C
|
|
5059
|
+
local review=0 forge="" target="" status verbs="" v
|
|
5060
|
+
case "${GITHUB_EVENT_NAME-}" in
|
|
5061
|
+
issue_comment) ;;
|
|
5062
|
+
pull_request_review) review=1 ;;
|
|
5063
|
+
*)
|
|
5064
|
+
echo "remote-run.sh: control handles GITHUB_EVENT_NAME issue_comment or pull_request_review, not '${GITHUB_EVENT_NAME-}'" >&2
|
|
5065
|
+
exit "$EXIT_USAGE" ;;
|
|
5066
|
+
esac
|
|
5067
|
+
if [ -z "${GITHUB_EVENT_PATH-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ] || [ ! -r "$GITHUB_EVENT_PATH" ]; then
|
|
5068
|
+
echo "remote-run.sh: control: cannot read the event file '${GITHUB_EVENT_PATH-}'" >&2
|
|
5069
|
+
exit "$EXIT_USAGE"
|
|
5070
|
+
fi
|
|
5071
|
+
hr_have_jq || { echo "remote-run.sh: control needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
5072
|
+
|
|
5073
|
+
if [ "$review" -eq 1 ]; then
|
|
5074
|
+
control_review_intake || return 0
|
|
5075
|
+
else
|
|
5076
|
+
control_comment_intake || return 0
|
|
5077
|
+
fi
|
|
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" ;;
|
|
5083
|
+
esac
|
|
5084
|
+
|
|
5085
|
+
control_tmp="${RUNNER_TEMP-}"
|
|
5086
|
+
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5087
|
+
control_tmp=$(mktemp -d) || { echo "remote-run.sh: control: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
5088
|
+
control_dirs="$control_tmp"
|
|
5089
|
+
fi
|
|
5090
|
+
trap control_cleanup EXIT
|
|
5091
|
+
|
|
5092
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5093
|
+
control_refuse "$EXIT_REFUSED" "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every command" \
|
|
5094
|
+
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then comment again."
|
|
5095
|
+
fi
|
|
5096
|
+
|
|
5097
|
+
forge=$(hr_forge "$root") || forge=""
|
|
5098
|
+
target=$(hr_execution_target "$root") || target=""
|
|
5099
|
+
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
5100
|
+
control_refuse "$EXIT_REFUSED" "the default branch's \`harness.config.json\` does not turn run control on: it needs \`forge\` set to \`github\` (it is ${forge:-not set or unreadable}) and \`execution.target\` set to \`github-actions\` (it is ${target:-unreadable})" \
|
|
5101
|
+
"Set both keys on the default branch, then comment again."
|
|
5102
|
+
fi
|
|
5103
|
+
|
|
5104
|
+
status=0
|
|
5105
|
+
authorise_actor "$CONTROL_ACTOR" "$CONTROL_SENDER_TYPE" || status=$?
|
|
5106
|
+
if [ "$status" -ne 0 ]; then
|
|
5107
|
+
control_refuse "$EXIT_REFUSED" "${AUTH_WHY%.}" \
|
|
5108
|
+
"Only a collaborator with write, maintain or admin access, or a bot listed in the repository variable \`HARNESS_TRIGGER_ALLOWED_BOTS\`, commands a run."
|
|
5109
|
+
fi
|
|
5110
|
+
|
|
5111
|
+
if [ "$review" -eq 0 ] && { [ -z "$CONTROL_VERB" ] || ! control_verb_handled "$CONTROL_VERB"; }; then
|
|
5112
|
+
for v in $COMMAND_VERBS; do
|
|
5113
|
+
[ "$v" != answer ] || v="answer [<n>]"
|
|
5114
|
+
verbs="$verbs${verbs:+, }\`$COMMAND_HANDLE $v\`"
|
|
5115
|
+
done
|
|
5116
|
+
control_refuse "$EXIT_REFUSED" "it is not a command this harness carries out" \
|
|
5117
|
+
"The commands are $verbs; \`docs/github-run-control.md\` in the harness documentation states each."
|
|
5118
|
+
fi
|
|
5119
|
+
|
|
5120
|
+
forge_repo_var || control_reply "$EXIT_GH" "@$CONTROL_ACTOR: \`$CONTROL_VERB\` was not run: the repository's name could not be read."
|
|
5121
|
+
if [ "$review" -eq 1 ]; then
|
|
5122
|
+
control_check_branch "$REVIEW_HEAD"
|
|
5123
|
+
elif [ -n "$CONTROL_IS_PR" ]; then
|
|
5124
|
+
control_branch_from_pr "$CONTROL_NUMBER"
|
|
5125
|
+
else
|
|
5126
|
+
control_branch_from_issue "$CONTROL_NUMBER"
|
|
5127
|
+
fi
|
|
5128
|
+
|
|
5129
|
+
echo "remote-run.sh: control: $CONTROL_VERB on $CONTROL_BRANCH from @$CONTROL_ACTOR on #$CONTROL_NUMBER"
|
|
5130
|
+
case "$CONTROL_VERB" in
|
|
5131
|
+
answer) control_answer ;;
|
|
5132
|
+
pause) control_pause ;;
|
|
5133
|
+
stop) control_stop ;;
|
|
5134
|
+
resume) control_resume ;;
|
|
5135
|
+
clear) control_clear ;;
|
|
5136
|
+
review) control_review ;;
|
|
5137
|
+
esac
|
|
5138
|
+
}
|
|
5139
|
+
|
|
5140
|
+
# ---------------------------------------------------------------------------
|
|
5141
|
+
# `collect` — the run workflow's last job: the next round from the reviews
|
|
5142
|
+
# collected while the run was in flight. It reuses `control`'s settledness read,
|
|
5143
|
+
# child runner and cleanup, with CONTROL_BRANCH set to the branch.
|
|
5144
|
+
# ---------------------------------------------------------------------------
|
|
5145
|
+
|
|
5146
|
+
# collect_notify <text> — the one comment `collect` posts, on FORGE_PR with the
|
|
5147
|
+
# `reply` marker; a failure is one line.
|
|
5148
|
+
collect_notify() {
|
|
5149
|
+
local file
|
|
5150
|
+
if ! file=$(mktemp "$control_tmp/harness-collect-comment.XXXXXX"); then
|
|
5151
|
+
echo "remote-run.sh: collect: cannot create the comment file for #$FORGE_PR; no comment posted" >&2
|
|
5152
|
+
return 0
|
|
5153
|
+
fi
|
|
5154
|
+
{
|
|
5155
|
+
printf '%s\n' "$1"
|
|
5156
|
+
[ -z "${GITHUB_RUN_ID-}" ] || printf '\nRun: %s\n' "$(this_run_url)"
|
|
5157
|
+
} >"$file"
|
|
5158
|
+
forge_comment "$FORGE_PR" reply "$branch" "$file" || :
|
|
5159
|
+
rm -f "$file"
|
|
5160
|
+
}
|
|
5161
|
+
|
|
5162
|
+
verb_collect() {
|
|
5163
|
+
local status=0 dir file out
|
|
5164
|
+
if ! forge_on; then
|
|
5165
|
+
echo "remote-run.sh: collect: the forge coupling is off (forge github and execution.target github-actions); nothing collected"
|
|
5166
|
+
exit "$EXIT_OK"
|
|
5167
|
+
fi
|
|
5168
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
5169
|
+
echo "remote-run.sh: collect: HARNESS_REMOTE_STOP is set; no round started"
|
|
5170
|
+
exit "$EXIT_OK"
|
|
5171
|
+
fi
|
|
5172
|
+
if ! hr_have_jq; then
|
|
5173
|
+
echo "remote-run.sh: collect: jq is missing; nothing collected"
|
|
5174
|
+
exit "$EXIT_OK"
|
|
5175
|
+
fi
|
|
5176
|
+
forge_repo_var || exit "$EXIT_OK"
|
|
5177
|
+
|
|
5178
|
+
remote_branch_stopped "$branch" || status=$?
|
|
5179
|
+
case "$status" in
|
|
5180
|
+
0)
|
|
5181
|
+
echo "remote-run.sh: collect: $branch is stopped; no round started"
|
|
5182
|
+
exit "$EXIT_OK" ;;
|
|
5183
|
+
2)
|
|
5184
|
+
echo "remote-run.sh: collect: whether $branch is stopped could not be read ($GH_ERR); no round started"
|
|
5185
|
+
exit "$EXIT_OK" ;;
|
|
5186
|
+
esac
|
|
5187
|
+
|
|
5188
|
+
if [ -n "$pr_arg" ]; then
|
|
5189
|
+
FORGE_PR="$pr_arg"
|
|
5190
|
+
elif ! forge_pr_var "$branch"; then
|
|
5191
|
+
echo "remote-run.sh: collect: the pull request of $branch could not be read; nothing collected"
|
|
5192
|
+
exit "$EXIT_OK"
|
|
5193
|
+
fi
|
|
5194
|
+
if [ -z "$FORGE_PR" ]; then
|
|
5195
|
+
echo "remote-run.sh: collect: no open pull request; nothing to collect"
|
|
5196
|
+
exit "$EXIT_OK"
|
|
5197
|
+
fi
|
|
5198
|
+
|
|
5199
|
+
control_tmp="${RUNNER_TEMP-}"
|
|
5200
|
+
if [ -z "$control_tmp" ] || [ ! -d "$control_tmp" ]; then
|
|
5201
|
+
control_tmp=$(mktemp -d) || { echo "remote-run.sh: collect: mktemp failed; nothing collected"; exit "$EXIT_OK"; }
|
|
5202
|
+
control_dirs="$control_tmp"
|
|
5203
|
+
fi
|
|
5204
|
+
trap control_cleanup EXIT
|
|
5205
|
+
|
|
5206
|
+
CONTROL_BRANCH="$branch"
|
|
5207
|
+
if ! control_settled_var; then
|
|
5208
|
+
echo "remote-run.sh: collect: the state of the run on $branch could not be read ($BS_ERR); no round started"
|
|
5209
|
+
exit "$EXIT_OK"
|
|
5210
|
+
fi
|
|
5211
|
+
if [ "$BS_SETTLED" != 1 ]; then
|
|
5212
|
+
echo "remote-run.sh: collect: $branch is ${BS_STATE:-in flight}${BS_REASON:+ ($BS_REASON)}; that run's own end collects"
|
|
5213
|
+
exit "$EXIT_OK"
|
|
5214
|
+
fi
|
|
5215
|
+
|
|
5216
|
+
forge_fetch_branch "$branch"
|
|
5217
|
+
if ! dir=$(mktemp -d "$control_tmp/harness-collect.XXXXXX"); then
|
|
5218
|
+
echo "::warning::remote-run.sh: collect: a round directory could not be created under '$control_tmp'; no round started"
|
|
5219
|
+
exit "$EXIT_OK"
|
|
5220
|
+
fi
|
|
5221
|
+
control_dirs="$control_dirs $dir"
|
|
5222
|
+
file="$dir/review.md"
|
|
5223
|
+
RC_EVENT=""
|
|
5224
|
+
status=0
|
|
5225
|
+
round_collect "$FORGE_PR" "$file" || status=$?
|
|
5226
|
+
case "$status" in
|
|
5227
|
+
0) ;;
|
|
5228
|
+
1)
|
|
5229
|
+
echo "remote-run.sh: collect: no review requesting changes is pending on #$FORGE_PR since the previous round of $branch; no round started"
|
|
5230
|
+
exit "$EXIT_OK" ;;
|
|
5231
|
+
*)
|
|
5232
|
+
echo "::warning::remote-run.sh: collect: the round of $branch could not be collected: $RC_ERR; no round started"
|
|
5233
|
+
exit "$EXIT_OK" ;;
|
|
5234
|
+
esac
|
|
5235
|
+
|
|
5236
|
+
out="$dir/review.out"
|
|
5237
|
+
set -- review "$branch" --review-file "$file" --allow-no-run --reviewers "$RC_REVIEWERS"
|
|
5238
|
+
case "$FORGE_SERVER" in
|
|
5239
|
+
https://*) set -- "$@" --source "$FORGE_SERVER/$FORGE_REPO/pull/$FORGE_PR" ;;
|
|
5240
|
+
esac
|
|
5241
|
+
control_child "$out" "$@" --repo "$root"
|
|
5242
|
+
cat "$out" 2>/dev/null || :
|
|
5243
|
+
if [ "$CHILD_STATUS" -eq 0 ]; then
|
|
5244
|
+
echo "remote-run.sh: collect: started the next round of $branch from @${RC_REVIEWERS//,/, @}"
|
|
5245
|
+
exit "$EXIT_OK"
|
|
5246
|
+
fi
|
|
5247
|
+
collect_notify "The reviews requesting changes collected during the run on \`$branch\` could not start the next round: ${CHILD_LAST%.}. They stay on the pull request; submit a review requesting changes to retry."
|
|
5248
|
+
echo "remote-run.sh: collect: review exited $CHILD_STATUS; the pull request was told, and nothing is retried"
|
|
5249
|
+
exit "$EXIT_OK"
|
|
5250
|
+
}
|
|
5251
|
+
|
|
5252
|
+
# ---------------------------------------------------------------------------
|
|
5253
|
+
# `discard` — remove a directory a command fetched into, inside scratch only.
|
|
5254
|
+
# ---------------------------------------------------------------------------
|
|
5255
|
+
|
|
5256
|
+
# The removal lives here rather than in the command because a supervised or
|
|
5257
|
+
# auto-mode session may refuse a recursive `rm` the agent types, and a
|
|
5258
|
+
# user-level `rm -rf` deny cannot be overridden (`.claude/context/
|
|
5259
|
+
# conventions.md` -> `## Shell assets`). The scope is the scratch directory
|
|
5260
|
+
# only, per the lessons ledger's rule that a script "never removes one it did
|
|
5261
|
+
# not create": scratch holds only throwaway files a session itself wrote. Containment is `hr_scratch_path_var`'s alone; this verb
|
|
5262
|
+
# maps its status and acts on `HR_SCRATCH_TARGET`. It never creates anything.
|
|
5263
|
+
verb_discard() {
|
|
5264
|
+
local status
|
|
5265
|
+
hr_scratch_path_var "$root" "$discard_dir" "$discard_base"
|
|
5266
|
+
status=$?
|
|
5267
|
+
case "$status:$HR_SCRATCH_WHY" in
|
|
5268
|
+
0:*) ;;
|
|
5269
|
+
1:dotdot)
|
|
5270
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' carries '..'" >&2
|
|
5271
|
+
exit "$EXIT_REFUSED" ;;
|
|
5272
|
+
1:charset)
|
|
5273
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' carries a character a scratch path may not" >&2
|
|
5274
|
+
exit "$EXIT_REFUSED" ;;
|
|
5275
|
+
1:itself)
|
|
5276
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' is the scratch directory itself" >&2
|
|
5277
|
+
exit "$EXIT_REFUSED" ;;
|
|
5278
|
+
1:symlink)
|
|
5279
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' is a symlink" >&2
|
|
5280
|
+
exit "$EXIT_REFUSED" ;;
|
|
5281
|
+
1:*)
|
|
5282
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' is not inside '$HR_SCRATCH_DIR/'" >&2
|
|
5283
|
+
exit "$EXIT_REFUSED" ;;
|
|
5284
|
+
2:*)
|
|
5285
|
+
echo "remote-run.sh: discard: the parent directory of '$discard_dir' cannot be resolved; nothing removed" >&2
|
|
5286
|
+
exit "$EXIT_USAGE" ;;
|
|
5287
|
+
3:no-scratch)
|
|
5288
|
+
echo "remote-run.sh: discard: the state directory's scratch/ under '$root' does not exist; nothing removed" >&2
|
|
5289
|
+
exit "$EXIT_USAGE" ;;
|
|
5290
|
+
3:*)
|
|
5291
|
+
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
5292
|
+
exit "$EXIT_USAGE" ;;
|
|
5293
|
+
*)
|
|
5294
|
+
usage "discard needs a <dir>" ;;
|
|
5295
|
+
esac
|
|
5296
|
+
if [ ! -e "$HR_SCRATCH_TARGET" ]; then
|
|
5297
|
+
echo "remote-run.sh: $discard_dir does not exist; nothing removed"
|
|
5298
|
+
return 0
|
|
5299
|
+
fi
|
|
5300
|
+
if [ ! -d "$HR_SCRATCH_TARGET" ]; then
|
|
5301
|
+
echo "remote-run.sh: discard refused, nothing removed: '$discard_dir' is not a directory" >&2
|
|
5302
|
+
exit "$EXIT_REFUSED"
|
|
5303
|
+
fi
|
|
5304
|
+
if ! rm -rf -- "$HR_SCRATCH_TARGET" || [ -e "$HR_SCRATCH_TARGET" ]; then
|
|
5305
|
+
echo "remote-run.sh: discard: removing '$discard_dir' failed" >&2
|
|
5306
|
+
exit "$EXIT_USAGE"
|
|
5307
|
+
fi
|
|
5308
|
+
echo "remote-run.sh: removed $discard_dir"
|
|
2847
5309
|
}
|
|
2848
5310
|
|
|
2849
5311
|
case "$verb" in
|
|
@@ -2864,5 +5326,10 @@ case "$verb" in
|
|
|
2864
5326
|
review) verb_review ;;
|
|
2865
5327
|
trigger) verb_trigger ;;
|
|
2866
5328
|
list) verb_list ;;
|
|
5329
|
+
discard) verb_discard ;;
|
|
5330
|
+
report) verb_report ;;
|
|
5331
|
+
deliver) verb_deliver ;;
|
|
5332
|
+
collect) verb_collect ;;
|
|
5333
|
+
control) verb_control ;;
|
|
2867
5334
|
esac
|
|
2868
5335
|
exit "$EXIT_OK"
|