autonomous-sdlc-harness 0.4.2 → 0.5.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 +101 -13
- package/dist/commands/init.js.map +1 -1
- package/dist/config/check.js +1 -1
- package/dist/config/check.js.map +1 -1
- package/dist/config/model.js +17 -1
- package/dist/config/model.js.map +1 -1
- package/dist/core/writer.js +9 -5
- package/dist/core/writer.js.map +1 -1
- package/dist/core/yamlScalar.js +14 -0
- package/dist/core/yamlScalar.js.map +1 -0
- package/dist/doctor/checks.js +169 -15
- package/dist/doctor/checks.js.map +1 -1
- package/dist/generators/githubWorkflows.js +115 -20
- package/dist/generators/githubWorkflows.js.map +1 -1
- package/dist/generators/repoRoot.js +17 -8
- package/dist/generators/repoRoot.js.map +1 -1
- package/dist/remote/githubActions.js +51 -7
- package/dist/remote/githubActions.js.map +1 -1
- package/package.json +1 -1
- package/templates/README.md +1 -1
- package/templates/github/workflows/harness-resume.yml +2 -0
- package/templates/github/workflows/harness-run.yml +27 -11
- package/templates/github/workflows/harness-trigger.yml +143 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/autonomous-watcher.sh +92 -247
- package/templates/scripts/create-worktree.sh +52 -10
- package/templates/scripts/lib/harness-run-lib.sh +432 -11
- package/templates/scripts/remote-run.sh +1179 -106
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
# workflow's input contract has exactly one producer on the shell side.
|
|
5
5
|
#
|
|
6
6
|
# THE VERBS AND THE EXIT MAP, stated once for every consumer (the watcher's
|
|
7
|
-
# inbox
|
|
8
|
-
#
|
|
7
|
+
# inbox pass, the job-side `continue` / `poll`, the guard's deny entry and the
|
|
8
|
+
# plugin commands that name this file):
|
|
9
9
|
#
|
|
10
10
|
# remote-run.sh dispatch <branch> --engine <kind> [--resume none|answer|pause]
|
|
11
11
|
# [--answers-from <clar_dir> --indexes "<n> <n>..."]
|
|
@@ -15,31 +15,44 @@
|
|
|
15
15
|
# remote-run.sh stop <branch> [--repo <root>]
|
|
16
16
|
# remote-run.sh status <branch> [--repo <root>]
|
|
17
17
|
# remote-run.sh sync <branch> [--repo <root>]
|
|
18
|
+
# remote-run.sh fetch <branch> <out_dir> [--repo <root>]
|
|
18
19
|
# remote-run.sh restore <branch> --resume none|answer|pause [--repo <root>]
|
|
19
20
|
# remote-run.sh save <branch> <out_dir> [--repo <root>]
|
|
20
21
|
# remote-run.sh continue <branch> <bundle_dir> [--repo <root>]
|
|
21
22
|
# remote-run.sh poll [--repo <root>]
|
|
22
23
|
# remote-run.sh pause-requested <branch> <since_epoch> [--repo <root>]
|
|
23
24
|
# remote-run.sh run-created-at <run_id> [--repo <root>]
|
|
25
|
+
# remote-run.sh start <branch> --prompt-file <file> [--repo <root>]
|
|
26
|
+
# remote-run.sh review <branch> --review-file <file> [--repo <root>]
|
|
27
|
+
# remote-run.sh trigger [--repo <root>] (its own exit map: its paragraph)
|
|
28
|
+
# remote-run.sh list [--repo <root>]
|
|
24
29
|
# 0 sent (for stop: the action=stop marker was dispatched, and every
|
|
25
30
|
# queued, waiting or in-progress `harness run` run of that branch was
|
|
26
|
-
# asked to cancel, or there was none); for status
|
|
31
|
+
# asked to cancel, or there was none); for status and fetch: printed
|
|
32
|
+
# (for fetch, `state: none` included); for sync: the record is
|
|
27
33
|
# current (including "no run listed yet", which writes nothing); for
|
|
28
34
|
# restore: restored, or no previous bundle (or an expired one, with a
|
|
29
35
|
# `::warning::` line) under --resume none|pause;
|
|
30
36
|
# for save: ALWAYS, whatever happened; for continue: whatever it
|
|
31
37
|
# decided — every outcome a person must act on is a notification; for
|
|
32
38
|
# poll: the tick finished; for pause-requested: such a run exists; for
|
|
33
|
-
# run-created-at: printed
|
|
39
|
+
# run-created-at: printed; for review: placed, pushed and dispatched;
|
|
40
|
+
# for list: printed
|
|
34
41
|
# 1 usage error, or the library or the configuration could not be
|
|
35
|
-
# resolved; for
|
|
42
|
+
# resolved; for fetch, <out_dir> is not an existing, empty directory;
|
|
43
|
+
# for sync and restore, a local copy or write failed; for
|
|
36
44
|
# pause-requested, also NO such run — a caller that reads 1 as "no
|
|
37
45
|
# pause" passes arguments it has already validated
|
|
38
46
|
# 2 refused, nothing sent or written: execution.target is not
|
|
39
|
-
# github-actions (sending verbs
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
47
|
+
# github-actions (sending verbs, fetch, review and list); for
|
|
48
|
+
# review, a protected branch, a review file that is not a readable
|
|
49
|
+
# regular file, or a run in flight (its paragraph); for sync, the
|
|
50
|
+
# branch's local record does not carry `execution: github-actions`; for
|
|
51
|
+
# status, a local record that does not carry `execution:
|
|
52
|
+
# github-actions`, or no record and `execution.target` not
|
|
53
|
+
# `github-actions`; the record's mirror working copy is missing, or a
|
|
54
|
+
# downloaded bundle is unrecognised (sync, restore, and status with no
|
|
55
|
+
# local record); the inputs payload is over the limit; a named answer
|
|
43
56
|
# file is missing. For restore under --resume answer, "nothing more":
|
|
44
57
|
# no previous bundle, the previous bundle expired (the message names
|
|
45
58
|
# its expiry and the resume command), `HARNESS_INPUT_ANSWERS` not an object of
|
|
@@ -49,7 +62,142 @@
|
|
|
49
62
|
# 3 gh failed: not found, or a non-zero exit — the first line of gh's
|
|
50
63
|
# stderr is named. For poll: the listing or the disable failed. For
|
|
51
64
|
# pause-requested and run-created-at, also an answer that is not the
|
|
52
|
-
# expected JSON; a caller never pauses on a failed read
|
|
65
|
+
# expected JSON; a caller never pauses on a failed read. For start, the
|
|
66
|
+
# dispatch failed AFTER the branch and its task prompt were pushed; for
|
|
67
|
+
# review, the listing failed, or the dispatch failed AFTER the review
|
|
68
|
+
# was pushed. For
|
|
69
|
+
# list, the listing or `git ls-remote` failed; nothing written
|
|
70
|
+
# 4 start: placement failed — the branch cut, the copy, the commit or the
|
|
71
|
+
# push — and nothing was dispatched; the working copy and the local
|
|
72
|
+
# branch the cut created were removed. review: placement failed — the
|
|
73
|
+
# copy (the branch checked out in another working copy included), the
|
|
74
|
+
# fast-forward, the commit or the push — and nothing was dispatched; a
|
|
75
|
+
# copy it cut was removed
|
|
76
|
+
#
|
|
77
|
+
# `start` IS THE ADAPTERS' ONE ENTRY: every trigger (an issue event, a forge
|
|
78
|
+
# dispatch, anything later) reduces to a branch and a task text and ends here.
|
|
79
|
+
# In order, stopping at the first failure: refuse a protected branch (2); refuse
|
|
80
|
+
# a prompt file that is not a readable regular file (2); cut the branch from
|
|
81
|
+
# `origin/<defaultBranch>` with `create-worktree.sh --no-bootstrap`; place the
|
|
82
|
+
# file at `<state_dir>/task_prompts/<branch>_task_prompt.md` in that working
|
|
83
|
+
# copy (the state directory resolved there, never in the main checkout), commit
|
|
84
|
+
# it as `chore: add task prompt for <branch>` and confirm `origin/<branch>`
|
|
85
|
+
# equals `HEAD` (each failure 4); then remove the working copy and the local
|
|
86
|
+
# branch it created — on every exit after the cut, success included and
|
|
87
|
+
# wherever `start` runs, because nothing reads the copy once the push has landed
|
|
88
|
+
# and a leftover branch makes the next cut of that name refuse; a copy or branch
|
|
89
|
+
# that existed before the cut is never removed — and `dispatch --engine task
|
|
90
|
+
# --resume none --chain 0`, composed by `verb_dispatch` itself. The placement is the library's
|
|
91
|
+
# (`hr_task_prompt_rel`, `hr_place_artifact`, `hr_commit_placed`,
|
|
92
|
+
# `hr_push_landed`), the same calls the watcher's inbox pass makes, so nothing
|
|
93
|
+
# downstream can tell where a task came from. It writes no registry record: a
|
|
94
|
+
# trigger job has no registry, and such a run needs no local record: the local
|
|
95
|
+
# commands act on it through GitHub.
|
|
96
|
+
#
|
|
97
|
+
# `fetch` IS THE COMMANDS' READ OF ONE BRANCH ON GITHUB, needing no local
|
|
98
|
+
# record. Gated like a sending verb. <out_dir> must be an existing, empty
|
|
99
|
+
# directory. It reads the newest `harness run <branch>` run through the same
|
|
100
|
+
# derivation `sync` makes (`remote_state`), downloads that run's state bundle
|
|
101
|
+
# into <out_dir> when one applies, and prints these lines, each always present
|
|
102
|
+
# and empty when unknown — the key names are a wire the commands parse:
|
|
103
|
+
# run_id: run_url: run_status: the newest `harness run <branch>` run
|
|
104
|
+
# state: `none` when no such run is listed, else the derivation's
|
|
105
|
+
# pause_reason: engine: detail: from the derivation
|
|
106
|
+
# open_questions: space-separated <n> of every top-level
|
|
107
|
+
# `<out_dir>/clarifications/<branch>/question_<n>.md` with
|
|
108
|
+
# no `answer_<n>.md` beside it, ascending
|
|
109
|
+
# bundle_dir: <out_dir> when a bundle was downloaded
|
|
110
|
+
#
|
|
111
|
+
# `review` PLACES A USER REVIEW ROUND ON THE BRANCH TIP AND DISPATCHES IT, for
|
|
112
|
+
# `/autonomous-sdlc-harness:branch-user-review` on a run that executes on
|
|
113
|
+
# GitHub. In order, stopping at the first failure: refuse a protected branch
|
|
114
|
+
# 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 whose
|
|
116
|
+
# newest `harness run <branch>` run, by `remote_state`, is anything but
|
|
117
|
+
# `completed` or `failed` — none listed, `running`, `parked`, `park_loop` or
|
|
118
|
+
# `paused`, an expired bundle naming its expiry (2); the bundle it reads is
|
|
119
|
+
# downloaded to `sync`'s directory, the one write a refusal makes. The copy: the main
|
|
120
|
+
# checkout's remote record's mirror when its `worktree` exists and is on the
|
|
121
|
+
# branch, never removed; otherwise `create-worktree.sh --existing
|
|
122
|
+
# --no-bootstrap` into `hr_worktree_dir`, removed with the local branch it
|
|
123
|
+
# DWIM-created on every exit, as `start` removes its cut. Either copy is
|
|
124
|
+
# fast-forwarded to `origin/<branch>`. No bootstrap runs: the copy holds one
|
|
125
|
+
# placed file. The round comes from `git ls-tree` of the copy's `HEAD` under
|
|
126
|
+
# `<state_dir>/user_reviews/` — the engine's own round source: each basename
|
|
127
|
+
# matching `^(.+)_review(_[0-9]+)?\.md$` whose captured branch EQUALS <branch>,
|
|
128
|
+
# the unsuffixed file being round 1; next is `<branch>_review.md` when none
|
|
129
|
+
# matched, else `<branch>_review_<max+1>.md`. It is placed, committed as
|
|
130
|
+
# `hr_user_review_subject`'s `chore: add user review for <branch>` and pushed
|
|
131
|
+
# (each failure 4); the cut copy is removed; then `dispatch --engine
|
|
132
|
+
# user_review --resume none --chain 0`. A remote record, when one exists, is
|
|
133
|
+
# set `running` / `user_review` in one write after the dispatch.
|
|
134
|
+
#
|
|
135
|
+
# `trigger` IS THE GITHUB EVENT ADAPTER, the one step of the trigger
|
|
136
|
+
# workflow's job: event -> (branch, task text) -> `start`. It handles
|
|
137
|
+
# `GITHUB_EVENT_NAME` `issues` and `repository_dispatch`; any other name, or an
|
|
138
|
+
# event file it cannot read, exits 1. Like `restore` it acts on `hr_repo_root` of the working
|
|
139
|
+
# directory, and it takes no part in the sending-verb gate: it gates itself, so
|
|
140
|
+
# a refusal can still be commented. It reads, only from the environment:
|
|
141
|
+
# GITHUB_EVENT_NAME, GITHUB_EVENT_PATH the event; each field is read by `jq`
|
|
142
|
+
# into a variable and is only ever an argument or file
|
|
143
|
+
# bytes, never shell source
|
|
144
|
+
# GITHUB_REPOSITORY, GITHUB_SERVER_URL, GITHUB_RUN_ID the `gh` target and
|
|
145
|
+
# the URLs its comments name
|
|
146
|
+
# HARNESS_REMOTE_STOP non-empty: every start is refused
|
|
147
|
+
# HARNESS_TRIGGER_LABEL the trigger label; `DEFAULT_TRIGGER_LABEL` when empty
|
|
148
|
+
# HARNESS_TRIGGER_ALLOWED_BOTS comma-separated bot logins allowed to start
|
|
149
|
+
# HARNESS_TRIGGER_LOOKUP_SECS seconds between run lookups; `5` when empty.
|
|
150
|
+
# A test seam
|
|
151
|
+
# RUNNER_TEMP where the prompt snapshot is written; a `mktemp -d`
|
|
152
|
+
# directory when empty
|
|
153
|
+
# An `action` other than `labeled`, or another label, is one line and exit 0
|
|
154
|
+
# with no `gh` call. Otherwise refused, in this order, each refusal one issue
|
|
155
|
+
# comment naming the reason and the way on:
|
|
156
|
+
# 1. `HARNESS_REMOTE_STOP` is set
|
|
157
|
+
# 2. `hr_forge` is not `github` or `hr_execution_target` is not
|
|
158
|
+
# `github-actions` — before any authorisation, so a disabled trigger asks
|
|
159
|
+
# GitHub nothing about the labeller
|
|
160
|
+
# 3. the issue is not `open`
|
|
161
|
+
# 4. `sender.login` is `ghost` (GitHub's placeholder for a deleted account),
|
|
162
|
+
# empty, or not a login shape (`^[A-Za-z0-9][A-Za-z0-9-]*$`, plus `[bot]`
|
|
163
|
+
# for a `Bot`)
|
|
164
|
+
# 5. `sender.type` is not `User` and the login is not an exact entry of
|
|
165
|
+
# `HARNESS_TRIGGER_ALLOWED_BOTS` — checked by the listing alone, with no
|
|
166
|
+
# permission call, because the permission API answers `none` or 404 for a bot
|
|
167
|
+
# 6. a `User` whose `collaborators/<login>/permission` is not `admin` or
|
|
168
|
+
# `write` — `maintain` reads as `write` and `triage` as `read` there; a
|
|
169
|
+
# failed call is "could not confirm write access", never a pass
|
|
170
|
+
# Then it fetches `origin <defaultBranch>` (a failure tolerated), derives the
|
|
171
|
+
# branch with `hr_derive_branch <title> issue_<number>` (2 or 3 refused), writes
|
|
172
|
+
# the snapshot — `# <title>`, the body's bytes, `---` and a provenance sentence
|
|
173
|
+
# 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, at most
|
|
175
|
+
# `TRIGGER_RUN_LOOKUP_TRIES` times, falling back to the branch's filtered run
|
|
176
|
+
# list, and comments the branch and that URL. Every comment is followed by
|
|
177
|
+
# removing the label, so re-applying it is deliberate; a removal that fails is
|
|
178
|
+
# one `::warning::` line.
|
|
179
|
+
# A `repository_dispatch` reads `.action` (where GitHub puts the `event_type`)
|
|
180
|
+
# and `client_payload`'s `title`, `body` and `source`, the contract being
|
|
181
|
+
# {"event_type": TRIGGER_DISPATCH_EVENT_TYPE, "client_payload": {"title": …,
|
|
182
|
+
# "body": …, "source": …}}
|
|
183
|
+
# within GitHub's `client_payload` limits: at most 10 top-level properties and
|
|
184
|
+
# under 64 KB, so a longer task text does not fit. Another `.action` is one line
|
|
185
|
+
# and exit 0 with no `gh` call; an empty or missing `title` is refused. It has
|
|
186
|
+
# no labeller: GitHub sends one only for a fine-grained token with Contents
|
|
187
|
+
# write or a classic token with `repo`, so the token holder is the authority,
|
|
188
|
+
# and refusals 3 to 6 do not apply. The fallback name is `task_<GITHUB_RUN_ID>`
|
|
189
|
+
# and the snapshot's provenance sentence names the event type, `source` when
|
|
190
|
+
# set, and the time. There is no issue, so no comment and no label: every
|
|
191
|
+
# outcome is printed to stdout and appended as a Markdown block to
|
|
192
|
+
# `GITHUB_STEP_SUMMARY` when that is set, an append failure one `::warning::`.
|
|
193
|
+
# 0 started (commented), or ignored
|
|
194
|
+
# 1 not an `issues` or `repository_dispatch` event, or the event could not
|
|
195
|
+
# be read
|
|
196
|
+
# 2 refused (commented)
|
|
197
|
+
# 3 a `gh` step after the decision failed: the comment could not be posted
|
|
198
|
+
# (an `::error::` line; issues only), or `start` pushed the branch but
|
|
199
|
+
# its dispatch failed (commented with the manual Run-workflow way on)
|
|
200
|
+
# 4 `start` refused or failed its placement (commented)
|
|
53
201
|
#
|
|
54
202
|
# `restore` AND `save` ARE THE JOB-SIDE VERBS: the run workflow calls them in
|
|
55
203
|
# its job, before and (under `always()`) after the harness step. Without
|
|
@@ -204,11 +352,28 @@
|
|
|
204
352
|
# `gh run view <run_id> --json createdAt` as an epoch second.
|
|
205
353
|
#
|
|
206
354
|
# `status` AND `sync` READ THE RECORD, NOT THE KEY. A run keeps the execution
|
|
207
|
-
# it started with, so they test
|
|
208
|
-
# `execution.target`; the configuration is still read for `stateDir`.
|
|
355
|
+
# it started with, so where a record exists they test its `execution` field and
|
|
356
|
+
# never `execution.target`; the configuration is still read for `stateDir`.
|
|
357
|
+
# Only `status` with no record reads the key.
|
|
358
|
+
#
|
|
359
|
+
# `status` WITH NO LOCAL RECORD (no registry file, or no record of the branch)
|
|
360
|
+
# is gated like a sending verb and answers from GitHub alone: the same runs
|
|
361
|
+
# listing, then `remote_state` with the bundle downloaded into a `mktemp -d`
|
|
362
|
+
# directory removed on exit, printing the state, pause reason, detail, engine
|
|
363
|
+
# and run URL, each open question (the rule `fetch` states) with its `## Q<k>`
|
|
364
|
+
# heading lines; an expired bundle's expired line is its `detail`. No `harness run
|
|
365
|
+
# <branch>` run listed prints one line and exits 0. There is no sync to compare
|
|
366
|
+
# against, so no finished-since line.
|
|
209
367
|
#
|
|
210
|
-
# `
|
|
211
|
-
#
|
|
368
|
+
# `list` IS `branch-status`'s DIGEST: one listing (`list_all_runs`) and one
|
|
369
|
+
# `git ls-remote --heads origin`, never a bundle. It prints `on GitHub, no
|
|
370
|
+
# local record: <branch> <url>` for each branch `unrecorded_runs` keeps — its
|
|
371
|
+
# newest run titled exactly `harness run <branch>`, no registry record,
|
|
372
|
+
# unprotected and a live head on origin — or `no run on GitHub without a local
|
|
373
|
+
# record`.
|
|
374
|
+
#
|
|
375
|
+
# `status` WITH A RECORD WRITES NOTHING AT ALL — no registry (it does not even
|
|
376
|
+
# create an absent one), no download, no file. It prints the branch's newest runs titled
|
|
212
377
|
# `harness run <branch>` or `harness pause <branch>` (bounded), the record's
|
|
213
378
|
# `status`, `pause_reason`, `remote_run_url` and `remote_synced_at`, whether
|
|
214
379
|
# a `harness run` finished after the last sync, and — reading the newest
|
|
@@ -234,7 +399,8 @@
|
|
|
234
399
|
# `mirror` mode into the record's `worktree`, `run.log` copied to the main
|
|
235
400
|
# checkout's `autonomous_logs/<branch>.remote.log`, and `status`,
|
|
236
401
|
# `pause_reason`, `usage_resume_at`, `park_loop_cycles`, `remote_run_id`,
|
|
237
|
-
# `remote_run_url`, `remote_detail` and `remote_synced_at` written
|
|
402
|
+
# `remote_run_url`, `remote_detail` and `remote_synced_at` written, and
|
|
403
|
+
# `engine` when the bundle names `task`, `user_review` or `docs`. A
|
|
238
404
|
# `mirror` restore places no planning draft
|
|
239
405
|
# 4. no artifact, while some bundle exists (`remote_run_id` is set, or an
|
|
240
406
|
# older finished run carries one): a job that died before its upload.
|
|
@@ -294,7 +460,18 @@
|
|
|
294
460
|
# root's `harness.config.json`.
|
|
295
461
|
#
|
|
296
462
|
# WHAT IT NEVER DOES. It never launches a local session, never writes the
|
|
297
|
-
# inbox,
|
|
463
|
+
# inbox, and never watches a run it sent. Only `start` and `review` push, and
|
|
464
|
+
# only through `create-worktree.sh` and `push-branch.sh`; `start`'s writes are
|
|
465
|
+
# the prompt committed on `origin/<branch>`, through a working copy and a local
|
|
466
|
+
# branch it removes before it returns; `review`'s are the round committed on
|
|
467
|
+
# `origin/<branch>`, through the record's mirror or a copy and a local branch
|
|
468
|
+
# it removes, the record's `status` / `engine`, and the bundle download
|
|
469
|
+
# directory `sync` uses. A user's chain-0 `dispatch --resume answer|pause`
|
|
470
|
+
# writes the record's `status`, `resumed_at` and `resume_kind` in one write
|
|
471
|
+
# when the main checkout's registry file exists and holds a record with
|
|
472
|
+
# `execution: github-actions`; any other `dispatch` writes nothing. `trigger` writes its snapshot and comment
|
|
473
|
+
# 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. Every other verb's only writes are the
|
|
298
475
|
# registry record (`stop`, `sync`) and, for `sync`, the download directory
|
|
299
476
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
300
477
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
@@ -304,7 +481,9 @@
|
|
|
304
481
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
305
482
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
306
483
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
307
|
-
# and `current/`. `
|
|
484
|
+
# and `current/`. For `fetch`, <out_dir> only. `pause-requested`,
|
|
485
|
+
# `run-created-at`, `list` and `status` write nothing; a no-record `status`
|
|
486
|
+
# downloads into a temporary directory it removes on exit.
|
|
308
487
|
#
|
|
309
488
|
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
310
489
|
# rename there is an edit here, byte for byte:
|
|
@@ -313,6 +492,10 @@
|
|
|
313
492
|
# STATE_ARTIFACT_NAME mirrors STATE_ARTIFACT_NAME
|
|
314
493
|
# POLL_STATE_ARTIFACT_NAME mirrors POLL_STATE_ARTIFACT_NAME
|
|
315
494
|
# HARNESS_GH_CLI mirrors GH_CLI_VARIABLE (the binary run as `gh`)
|
|
495
|
+
# HARNESS_TRIGGER_LABEL mirrors TRIGGER_LABEL_VARIABLE
|
|
496
|
+
# DEFAULT_TRIGGER_LABEL mirrors DEFAULT_TRIGGER_LABEL
|
|
497
|
+
# HARNESS_TRIGGER_ALLOWED_BOTS mirrors TRIGGER_ALLOWED_BOTS_VARIABLE
|
|
498
|
+
# TRIGGER_DISPATCH_EVENT_TYPE mirrors TRIGGER_DISPATCH_EVENT_TYPE ('harness-task')
|
|
316
499
|
#
|
|
317
500
|
# `set -u` WITHOUT `-e`: every refusal is reported with its own exit code rather
|
|
318
501
|
# than aborting mid-decision.
|
|
@@ -345,6 +528,37 @@
|
|
|
345
528
|
# bash scripts/remote-run.sh pause feat_x -> 3, names `boom`
|
|
346
529
|
# gh absent HARNESS_GH_CLI=/nonexistent bash scripts/remote-run.sh warm -> 3
|
|
347
530
|
#
|
|
531
|
+
# start needs the adopted tree on origin's default branch (commit and push
|
|
532
|
+
# it first) and a prompt file outside the checkout, say /tmp/p.md:
|
|
533
|
+
# start bash scripts/remote-run.sh start feat_x --prompt-file /tmp/p.md
|
|
534
|
+
# -> 0; origin/feat_x gains `chore: add task prompt for feat_x`,
|
|
535
|
+
# then "$s.log" gains the same `workflow run` line as dispatch
|
|
536
|
+
# protected bash scripts/remote-run.sh start main --prompt-file /tmp/p.md
|
|
537
|
+
# -> 2, log unchanged, nothing pushed
|
|
538
|
+
# no prompt bash scripts/remote-run.sh start feat_y --prompt-file /nonexistent
|
|
539
|
+
# -> 2, log unchanged, nothing pushed
|
|
540
|
+
# review after that start, a `run list` answer whose newest `harness run
|
|
541
|
+
# feat_x` run is `completed` with a bundle saying `completed`:
|
|
542
|
+
# bash scripts/remote-run.sh review feat_x --review-file /tmp/r.md
|
|
543
|
+
# -> 0; origin/feat_x gains `chore: add user review for feat_x`
|
|
544
|
+
# placing feat_x_review.md, then one `-f engine=user_review`
|
|
545
|
+
# dispatch; no copy or local feat_x is left
|
|
546
|
+
# in flight the newest run `in_progress` -> 2, nothing pushed or sent
|
|
547
|
+
# fetch t=$(mktemp -d); bash scripts/remote-run.sh fetch feat_x "$t"
|
|
548
|
+
# -> 0; prints `state: running` (or, with no run listed,
|
|
549
|
+
# `state: none`), every other key present
|
|
550
|
+
#
|
|
551
|
+
# no record with no registry, the "a bundle" stub below with question_1.md:
|
|
552
|
+
# bash scripts/remote-run.sh status feat_x -> 0; prints `state:
|
|
553
|
+
# parked` and `open question question_1.md`; nothing written
|
|
554
|
+
# list start's setup, a feat_x pushed to origin, no registry, and a
|
|
555
|
+
# `run list` answer carrying `headBranch` feat_x, `displayTitle`
|
|
556
|
+
# `harness run feat_x` and a `url`: bash scripts/remote-run.sh
|
|
557
|
+
# list -> 0; prints `on GitHub, no local record: feat_x <url>`;
|
|
558
|
+
# no registry is created
|
|
559
|
+
# adopt bash scripts/remote-run.sh adopt -> 1, an unknown verb; no gh
|
|
560
|
+
# call, no registry
|
|
561
|
+
#
|
|
348
562
|
# status and sync need a remote record, and a `run list` answer whose runs
|
|
349
563
|
# carry `displayTitle` `harness run feat_x` and a `url`:
|
|
350
564
|
# r=sdlc-harness/autonomous_logs/registry.json; mkdir -p "${r%/*}"
|
|
@@ -432,6 +646,26 @@
|
|
|
432
646
|
# run-created-at a stub answering `run view 42 --json createdAt` with
|
|
433
647
|
# {"createdAt":"2026-01-01T00:00:10Z"}: bash scripts/remote-run.sh
|
|
434
648
|
# run-created-at 42 -> prints 1767225610, 0; a failing stub -> 3
|
|
649
|
+
#
|
|
650
|
+
# trigger needs start's setup plus `"forge": "github"`, an event file e.json
|
|
651
|
+
# {"action":"labeled","label":{"name":"harness"},"sender":{"login":"alice",
|
|
652
|
+
# "type":"User"},"issue":{"number":7,"title":"Add comments","body":"x",
|
|
653
|
+
# "html_url":"https://github.com/o/r/issues/7","state":"open"}}, and a stub
|
|
654
|
+
# answering `api repos/o/r/collaborators/alice/permission` with
|
|
655
|
+
# {"permission":"write"}; export GITHUB_EVENT_NAME=issues GITHUB_EVENT_PATH=e.json
|
|
656
|
+
# GITHUB_REPOSITORY=o/r HARNESS_TRIGGER_LOOKUP_SECS=0:
|
|
657
|
+
# trigger bash scripts/remote-run.sh trigger -> 0; origin/add_comments gains
|
|
658
|
+
# the prompt commit, "$s.log" gains `workflow run harness-run.yml
|
|
659
|
+
# --ref add_comments ...`, `issue comment 7 ...` naming the branch,
|
|
660
|
+
# then `issue edit 7 ... --remove-label harness`
|
|
661
|
+
# read the permission answer {"permission":"read"} -> 2, no `workflow
|
|
662
|
+
# run`, one comment naming write access, the label removed
|
|
663
|
+
# ignored e.json's label name `bug` -> 0, one line, "$s.log" unchanged
|
|
664
|
+
# dispatch GITHUB_EVENT_NAME=repository_dispatch GITHUB_RUN_ID=9
|
|
665
|
+
# GITHUB_STEP_SUMMARY=/tmp/s, e.json {"action":<TRIGGER_DISPATCH_EVENT_TYPE>,
|
|
666
|
+
# "client_payload":{"title":"Add tags","body":"x","source":"jira"}}
|
|
667
|
+
# -> 0; `workflow run ... --ref add_tags ...`, /tmp/s names
|
|
668
|
+
# add_tags, no `issue` call; without "title" -> 2, no `workflow run`
|
|
435
669
|
|
|
436
670
|
set -u
|
|
437
671
|
|
|
@@ -448,6 +682,8 @@ WORKFLOW_RUN_FILE='harness-run.yml'
|
|
|
448
682
|
WORKFLOW_RESUME_FILE='harness-resume.yml'
|
|
449
683
|
STATE_ARTIFACT_NAME='harness-state'
|
|
450
684
|
POLL_STATE_ARTIFACT_NAME='harness-poll-state'
|
|
685
|
+
DEFAULT_TRIGGER_LABEL='harness'
|
|
686
|
+
TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
451
687
|
GH="${HARNESS_GH_CLI:-gh}"
|
|
452
688
|
|
|
453
689
|
# How many runs `status` prints, and how many `run list` returns for status
|
|
@@ -468,6 +704,10 @@ POLL_GIVE_UP_AFTER_MINUTES_DEFAULT=360
|
|
|
468
704
|
# How many of the poller's own runs `poll` searches for the previous tick's
|
|
469
705
|
# state artifact; each one without it costs an artifact lookup.
|
|
470
706
|
POLL_STATE_RUNS_LIMIT=10
|
|
707
|
+
# `trigger`'s bound on looking up the run its `start` dispatched; the wait
|
|
708
|
+
# between tries is `HARNESS_TRIGGER_LOOKUP_SECS`, a test seam.
|
|
709
|
+
TRIGGER_RUN_LOOKUP_TRIES=6
|
|
710
|
+
TRIGGER_LOOKUP_SECS_DEFAULT=5
|
|
471
711
|
|
|
472
712
|
# GitHub's documented limit on a `workflow_dispatch` inputs payload: "The
|
|
473
713
|
# maximum payload for inputs is 65,535 characters."
|
|
@@ -478,6 +718,7 @@ EXIT_OK=0
|
|
|
478
718
|
EXIT_USAGE=1
|
|
479
719
|
EXIT_REFUSED=2
|
|
480
720
|
EXIT_GH=3
|
|
721
|
+
EXIT_PLACEMENT=4
|
|
481
722
|
# pause-requested only: the read succeeded and found no pause.
|
|
482
723
|
EXIT_NO_PAUSE=1
|
|
483
724
|
|
|
@@ -489,12 +730,17 @@ usage() {
|
|
|
489
730
|
echo " remote-run.sh stop <branch> [--repo <root>]" >&2
|
|
490
731
|
echo " remote-run.sh status <branch> [--repo <root>]" >&2
|
|
491
732
|
echo " remote-run.sh sync <branch> [--repo <root>]" >&2
|
|
733
|
+
echo " remote-run.sh fetch <branch> <out_dir> [--repo <root>]" >&2
|
|
492
734
|
echo " remote-run.sh restore <branch> --resume none|answer|pause [--repo <root>]" >&2
|
|
493
735
|
echo " remote-run.sh save <branch> <out_dir> [--repo <root>]" >&2
|
|
494
736
|
echo " remote-run.sh continue <branch> <bundle_dir> [--repo <root>]" >&2
|
|
495
737
|
echo " remote-run.sh poll [--repo <root>]" >&2
|
|
496
738
|
echo " remote-run.sh pause-requested <branch> <since_epoch> [--repo <root>]" >&2
|
|
497
739
|
echo " remote-run.sh run-created-at <run_id> [--repo <root>]" >&2
|
|
740
|
+
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
|
|
742
|
+
echo " remote-run.sh trigger [--repo <root>]" >&2
|
|
743
|
+
echo " remote-run.sh list [--repo <root>]" >&2
|
|
498
744
|
[ "${verb-}" != save ] || exit "$EXIT_OK"
|
|
499
745
|
exit "$EXIT_USAGE"
|
|
500
746
|
}
|
|
@@ -545,7 +791,7 @@ verb=""
|
|
|
545
791
|
verb="$1"
|
|
546
792
|
shift
|
|
547
793
|
case "$verb" in
|
|
548
|
-
dispatch|pause|warm|stop|status|sync|restore|save|continue|poll|pause-requested|run-created-at) ;;
|
|
794
|
+
dispatch|pause|warm|stop|status|sync|fetch|restore|save|continue|poll|pause-requested|run-created-at|start|review|trigger|list) ;;
|
|
549
795
|
*) usage "unknown verb '$verb'" ;;
|
|
550
796
|
esac
|
|
551
797
|
|
|
@@ -563,6 +809,8 @@ chain="0"
|
|
|
563
809
|
repo_arg=""
|
|
564
810
|
since_arg=""
|
|
565
811
|
run_id_arg=""
|
|
812
|
+
prompt_file=""
|
|
813
|
+
review_file=""
|
|
566
814
|
|
|
567
815
|
while [ "$#" -gt 0 ]; do
|
|
568
816
|
case "$1" in
|
|
@@ -586,10 +834,19 @@ while [ "$#" -gt 0 ]; do
|
|
|
586
834
|
--park-loop-clear)
|
|
587
835
|
[ "$verb" = dispatch ] || usage "$1 is a dispatch option"
|
|
588
836
|
park_loop_clear=1; shift ;;
|
|
837
|
+
--prompt-file)
|
|
838
|
+
[ "$verb" = start ] || usage "$1 is a start option"
|
|
839
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
840
|
+
prompt_file="$2"; shift 2 ;;
|
|
841
|
+
--review-file)
|
|
842
|
+
[ "$verb" = review ] || usage "$1 is a review option"
|
|
843
|
+
[ "$#" -ge 2 ] && [ -n "$2" ] || usage "$1 needs a value"
|
|
844
|
+
review_file="$2"; shift 2 ;;
|
|
589
845
|
-*)
|
|
590
846
|
usage "unknown option '$1'" ;;
|
|
591
847
|
*)
|
|
592
|
-
[ "$verb" != warm ] && [ "$verb" != poll ]
|
|
848
|
+
[ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != trigger ] \
|
|
849
|
+
&& [ "$verb" != list ] || usage "$verb takes no branch"
|
|
593
850
|
if [ "$verb" = run-created-at ]; then
|
|
594
851
|
[ -z "$run_id_arg" ] || usage "unexpected argument '$1'"
|
|
595
852
|
run_id_arg="$1"
|
|
@@ -597,7 +854,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
597
854
|
branch="$1"
|
|
598
855
|
elif [ "$verb" = pause-requested ] && [ -z "$since_arg" ]; then
|
|
599
856
|
since_arg="$1"
|
|
600
|
-
elif [ "$verb" = save ] && [ -z "$out_dir" ]; then
|
|
857
|
+
elif { [ "$verb" = save ] || [ "$verb" = fetch ]; } && [ -z "$out_dir" ]; then
|
|
601
858
|
out_dir="$1"
|
|
602
859
|
elif [ "$verb" = continue ] && [ -z "$bundle_dir" ]; then
|
|
603
860
|
bundle_dir="$1"
|
|
@@ -608,7 +865,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
608
865
|
esac
|
|
609
866
|
done
|
|
610
867
|
|
|
611
|
-
if [ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != run-created-at ]
|
|
868
|
+
if [ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != run-created-at ] && [ "$verb" != trigger ] \
|
|
869
|
+
&& [ "$verb" != list ]; then
|
|
612
870
|
valid_branch "$branch" || usage "$verb needs a <branch>"
|
|
613
871
|
fi
|
|
614
872
|
|
|
@@ -634,6 +892,18 @@ if [ "$verb" = save ] && [ -z "$out_dir" ]; then
|
|
|
634
892
|
usage "save needs an <out_dir>"
|
|
635
893
|
fi
|
|
636
894
|
|
|
895
|
+
if [ "$verb" = fetch ] && [ -z "$out_dir" ]; then
|
|
896
|
+
usage "fetch needs an <out_dir>"
|
|
897
|
+
fi
|
|
898
|
+
|
|
899
|
+
if [ "$verb" = review ] && [ -z "$review_file" ]; then
|
|
900
|
+
usage "review needs --review-file"
|
|
901
|
+
fi
|
|
902
|
+
|
|
903
|
+
if [ "$verb" = start ] && [ -z "$prompt_file" ]; then
|
|
904
|
+
usage "start needs --prompt-file"
|
|
905
|
+
fi
|
|
906
|
+
|
|
637
907
|
if [ "$verb" = restore ]; then
|
|
638
908
|
[ "$resume_given" -eq 1 ] || usage "restore needs --resume"
|
|
639
909
|
case "$resume" in
|
|
@@ -683,7 +953,7 @@ setup_fail() {
|
|
|
683
953
|
if [ -n "$repo_arg" ]; then
|
|
684
954
|
root=$(hr_repo_root "$repo_arg") || setup_fail "'$repo_arg' is not a git repository"
|
|
685
955
|
elif [ "$verb" = restore ] || [ "$verb" = save ] || [ "$verb" = continue ] || [ "$verb" = poll ] \
|
|
686
|
-
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ]; then
|
|
956
|
+
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ]; then
|
|
687
957
|
root=$(hr_repo_root "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
688
958
|
else
|
|
689
959
|
root=$(hr_main_repo "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
@@ -691,6 +961,7 @@ fi
|
|
|
691
961
|
|
|
692
962
|
hr_config_load "$root" || :
|
|
693
963
|
registry=""
|
|
964
|
+
status_no_record=0
|
|
694
965
|
case "$verb" in
|
|
695
966
|
restore|save|continue|poll)
|
|
696
967
|
hr_state_path "$root" >/dev/null || setup_fail "cannot resolve '$root/harness.config.json'"
|
|
@@ -698,6 +969,9 @@ case "$verb" in
|
|
|
698
969
|
pause-requested|run-created-at)
|
|
699
970
|
# Read verbs: no gate, and nothing of the configuration is read.
|
|
700
971
|
;;
|
|
972
|
+
trigger)
|
|
973
|
+
# Gates itself, after reading the event, so a refusal can still be commented.
|
|
974
|
+
;;
|
|
701
975
|
status|sync)
|
|
702
976
|
registry=$(hr_state_path "$root" autonomous_logs/registry.json) || {
|
|
703
977
|
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
@@ -705,7 +979,18 @@ case "$verb" in
|
|
|
705
979
|
}
|
|
706
980
|
# Tested with -f first: hr_registry_get creates an absent registry, and
|
|
707
981
|
# status writes nothing.
|
|
708
|
-
if [ ! -f "$registry" ] || [ "$(hr_registry_get "$registry" "$branch"
|
|
982
|
+
if [ "$verb" = status ] && { [ ! -f "$registry" ] || [ -z "$(hr_registry_get "$registry" "$branch" branch)" ]; }; then
|
|
983
|
+
# No record: GitHub alone answers, behind the sending verbs' gate.
|
|
984
|
+
status_no_record=1
|
|
985
|
+
target=$(hr_execution_target "$root") || {
|
|
986
|
+
echo "remote-run.sh: cannot resolve '$root/harness.config.json' (or execution.target is outside its enum)" >&2
|
|
987
|
+
exit "$EXIT_USAGE"
|
|
988
|
+
}
|
|
989
|
+
if [ "$target" != github-actions ]; then
|
|
990
|
+
echo "remote-run.sh: refused, nothing sent: execution.target is '$target', not github-actions" >&2
|
|
991
|
+
exit "$EXIT_REFUSED"
|
|
992
|
+
fi
|
|
993
|
+
elif [ ! -f "$registry" ] || [ "$(hr_registry_get "$registry" "$branch" execution)" != github-actions ]; then
|
|
709
994
|
echo "remote-run.sh: refused, nothing written: the local record of '$branch' does not carry execution: github-actions" >&2
|
|
710
995
|
exit "$EXIT_REFUSED"
|
|
711
996
|
fi
|
|
@@ -727,6 +1012,20 @@ case "$bundle_dir" in
|
|
|
727
1012
|
''|/*) ;;
|
|
728
1013
|
*) bundle_dir="${PWD-.}/$bundle_dir" ;;
|
|
729
1014
|
esac
|
|
1015
|
+
case "$prompt_file" in
|
|
1016
|
+
''|/*) ;;
|
|
1017
|
+
*) prompt_file="${PWD-.}/$prompt_file" ;;
|
|
1018
|
+
esac
|
|
1019
|
+
case "$review_file" in
|
|
1020
|
+
''|/*) ;;
|
|
1021
|
+
*) review_file="${PWD-.}/$review_file" ;;
|
|
1022
|
+
esac
|
|
1023
|
+
if [ "$verb" = fetch ]; then
|
|
1024
|
+
case "$out_dir" in
|
|
1025
|
+
/*) ;;
|
|
1026
|
+
*) out_dir="${PWD-.}/$out_dir" ;;
|
|
1027
|
+
esac
|
|
1028
|
+
fi
|
|
730
1029
|
|
|
731
1030
|
cd "$root" || setup_fail "cannot enter '$root'"
|
|
732
1031
|
|
|
@@ -734,6 +1033,9 @@ cd "$root" || setup_fail "cannot enter '$root'"
|
|
|
734
1033
|
# The verbs.
|
|
735
1034
|
# ---------------------------------------------------------------------------
|
|
736
1035
|
|
|
1036
|
+
# Appended to a failed dispatch's message; `start` sets it once its branch is pushed.
|
|
1037
|
+
dispatch_fail_note=""
|
|
1038
|
+
|
|
737
1039
|
verb_dispatch() {
|
|
738
1040
|
local answers="" value n file payload
|
|
739
1041
|
local -a inputs
|
|
@@ -770,8 +1072,19 @@ verb_dispatch() {
|
|
|
770
1072
|
exit "$EXIT_REFUSED"
|
|
771
1073
|
fi
|
|
772
1074
|
|
|
773
|
-
gh_call workflow run "$WORKFLOW_RUN_FILE" --ref "$branch" "${inputs[@]}" || gh_fail "dispatch of '$branch' failed"
|
|
1075
|
+
gh_call workflow run "$WORKFLOW_RUN_FILE" --ref "$branch" "${inputs[@]}" || gh_fail "dispatch of '$branch' failed$dispatch_fail_note"
|
|
774
1076
|
echo "remote-run.sh: dispatched action=run engine=$engine resume=$resume for $branch"
|
|
1077
|
+
|
|
1078
|
+
# A user's resume: the main checkout's remote record, when one exists, is
|
|
1079
|
+
# running now.
|
|
1080
|
+
if [ "$((10#$chain))" -eq 0 ] && { [ "$resume" = answer ] || [ "$resume" = pause ]; }; then
|
|
1081
|
+
local reg
|
|
1082
|
+
reg=$(hr_state_path "$root" autonomous_logs/registry.json) || reg=""
|
|
1083
|
+
if remote_record_exists "$reg"; then
|
|
1084
|
+
hr_registry_set "$reg" "$branch" status running resumed_at "$(date '+%Y-%m-%dT%H:%M:%S')" resume_kind "$resume" \
|
|
1085
|
+
|| echo "remote-run.sh: dispatched, but the local record of $branch could not be updated" >&2
|
|
1086
|
+
fi
|
|
1087
|
+
fi
|
|
775
1088
|
}
|
|
776
1089
|
|
|
777
1090
|
verb_pause() {
|
|
@@ -921,6 +1234,55 @@ set_many_or_fail() {
|
|
|
921
1234
|
}
|
|
922
1235
|
}
|
|
923
1236
|
|
|
1237
|
+
# open_questions_in <bundle_dir> — OPEN_QUESTIONS: the space-separated <n>,
|
|
1238
|
+
# ascending, of every top-level `clarifications/<branch>/question_<n>.md` in the
|
|
1239
|
+
# bundle with no `answer_<n>.md` beside it.
|
|
1240
|
+
OPEN_QUESTIONS=""
|
|
1241
|
+
open_questions_in() {
|
|
1242
|
+
local clar f n
|
|
1243
|
+
OPEN_QUESTIONS=""
|
|
1244
|
+
hr_remote_names_var
|
|
1245
|
+
clar="$1/$HR_REMOTE_CLARIFY_DIR/$branch"
|
|
1246
|
+
for f in "$clar"/question_*.md; do
|
|
1247
|
+
[ -f "$f" ] || continue
|
|
1248
|
+
n="${f##*/question_}"
|
|
1249
|
+
n="${n%.md}"
|
|
1250
|
+
[[ "$n" =~ ^[0-9]+$ ]] || continue
|
|
1251
|
+
[ -e "$clar/answer_$n.md" ] || OPEN_QUESTIONS="$OPEN_QUESTIONS $n"
|
|
1252
|
+
done
|
|
1253
|
+
if [ -n "$OPEN_QUESTIONS" ]; then
|
|
1254
|
+
OPEN_QUESTIONS=$(printf '%s\n' $OPEN_QUESTIONS | sort -n | tr '\n' ' ')
|
|
1255
|
+
OPEN_QUESTIONS="${OPEN_QUESTIONS% }"
|
|
1256
|
+
fi
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
# status_from_github — `status` with no local record: the newest run's state
|
|
1260
|
+
# read through `remote_state` into a temporary directory removed on exit.
|
|
1261
|
+
status_tmp=""
|
|
1262
|
+
status_from_github() {
|
|
1263
|
+
local n
|
|
1264
|
+
if [ -z "$(titled_runs "harness run $branch" | jq -c '.[0] // empty')" ]; then
|
|
1265
|
+
echo "remote-run.sh: no local record, and no run titled 'harness run $branch' on GitHub"
|
|
1266
|
+
return 0
|
|
1267
|
+
fi
|
|
1268
|
+
echo "remote-run.sh: no local record; state from GitHub (newest run):"
|
|
1269
|
+
status_tmp=$(mktemp -d) || { echo "remote-run.sh: cannot create a temporary directory" >&2; exit "$EXIT_USAGE"; }
|
|
1270
|
+
trap 'rm -rf "$status_tmp"' EXIT
|
|
1271
|
+
remote_state "$status_tmp"
|
|
1272
|
+
printf ' state: %s\n' "$RS_STATE"
|
|
1273
|
+
printf ' pause_reason: %s\n' "$RS_PAUSE_REASON"
|
|
1274
|
+
printf ' detail: %s\n' "$RS_DETAIL"
|
|
1275
|
+
printf ' engine: %s\n' "$RS_ENGINE"
|
|
1276
|
+
printf ' run_url: %s\n' "$RS_RUN_URL"
|
|
1277
|
+
if [ "$RS_BUNDLE" -eq 1 ]; then
|
|
1278
|
+
open_questions_in "$status_tmp"
|
|
1279
|
+
for n in $OPEN_QUESTIONS; do
|
|
1280
|
+
echo "remote-run.sh: open question question_$n.md"
|
|
1281
|
+
grep -E '^## Q[0-9]+' "$status_tmp/$HR_REMOTE_CLARIFY_DIR/$branch/question_$n.md" | sed 's/^/ /'
|
|
1282
|
+
done
|
|
1283
|
+
fi
|
|
1284
|
+
}
|
|
1285
|
+
|
|
924
1286
|
verb_status() {
|
|
925
1287
|
local runs finished_id synced_id field
|
|
926
1288
|
list_runs
|
|
@@ -929,6 +1291,10 @@ verb_status() {
|
|
|
929
1291
|
printf '%s' "$runs" | jq -r --argjson n "$STATUS_RUNS_SHOWN" '
|
|
930
1292
|
if length == 0 then " (none)" else
|
|
931
1293
|
.[:$n][] | " \(.databaseId) \(.displayTitle) \(.status)/\(.conclusion // "") \(.createdAt) \(.url)" end'
|
|
1294
|
+
if [ "$status_no_record" -eq 1 ]; then
|
|
1295
|
+
status_from_github
|
|
1296
|
+
return 0
|
|
1297
|
+
fi
|
|
932
1298
|
echo "remote-run.sh: local record (last synced):"
|
|
933
1299
|
for field in status pause_reason remote_run_url remote_synced_at; do
|
|
934
1300
|
printf ' %s: %s\n' "$field" "$(hr_registry_get "$registry" "$branch" "$field")"
|
|
@@ -957,78 +1323,171 @@ sync_expired() {
|
|
|
957
1323
|
echo "remote-run.sh: $line"
|
|
958
1324
|
}
|
|
959
1325
|
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1326
|
+
# remote_state <download_dir> [<applied_run_id>] — the one derivation of a
|
|
1327
|
+
# branch's newest remote state, from list_runs' answer in GH_OUT; `sync`,
|
|
1328
|
+
# `fetch`, `review` and `status` with no local record all call it. RS_STATE is `none` (no `harness run
|
|
1329
|
+
# <branch>` run listed), `running` (the newest is not `completed`), `applied`
|
|
1330
|
+
# (its id is <applied_run_id>: `sync`'s case 1, decided there), or the state
|
|
1331
|
+
# of `sync`'s cases 2-5, which it derives in that order. A bundle is downloaded
|
|
1332
|
+
# into <download_dir> — `sync`'s per-run directory under the main checkout when
|
|
1333
|
+
# empty — skipped when that directory already holds its status.json, and
|
|
1334
|
+
# RS_BUNDLE is then 1. <applied_run_id>, when set, also counts as a bundle
|
|
1335
|
+
# existing for case 4. Exits 3 when gh fails, 2 for an unrecognised bundle.
|
|
1336
|
+
RS_RUNS=""
|
|
1337
|
+
RS_RUN_ID=""
|
|
1338
|
+
RS_RUN_URL=""
|
|
1339
|
+
RS_GH_STATUS=""
|
|
1340
|
+
RS_STATE=""
|
|
1341
|
+
RS_PAUSE_REASON=""
|
|
1342
|
+
RS_DETAIL=""
|
|
1343
|
+
RS_ENGINE=""
|
|
1344
|
+
RS_USAGE_RESUME_AT=""
|
|
1345
|
+
RS_PARK_LOOP_CYCLES=""
|
|
1346
|
+
RS_BUNDLE=0
|
|
1347
|
+
RS_DOWNLOAD=""
|
|
1348
|
+
remote_state() {
|
|
1349
|
+
local download="${1-}" applied="${2-}" newest status_file older bundle_exists=0
|
|
1350
|
+
RS_RUNS=""; RS_RUN_ID=""; RS_RUN_URL=""; RS_GH_STATUS=""; RS_STATE=""
|
|
1351
|
+
RS_PAUSE_REASON=""; RS_DETAIL=""; RS_ENGINE=""; RS_USAGE_RESUME_AT=""
|
|
1352
|
+
RS_PARK_LOOP_CYCLES=""; RS_BUNDLE=0; RS_DOWNLOAD=""
|
|
1353
|
+
RS_RUNS=$(titled_runs "harness run $branch") || RS_RUNS='[]'
|
|
1354
|
+
newest=$(printf '%s' "$RS_RUNS" | jq -c '.[0] // empty')
|
|
971
1355
|
if [ -z "$newest" ]; then
|
|
972
|
-
|
|
1356
|
+
RS_STATE=none
|
|
973
1357
|
return 0
|
|
974
1358
|
fi
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
if [ "$state" != completed ]; then
|
|
981
|
-
set_many_or_fail status running remote_synced_at "$now"
|
|
982
|
-
echo "remote-run.sh: run $id of $branch is $state; the record is running, nothing downloaded"
|
|
1359
|
+
RS_RUN_ID=$(printf '%s' "$newest" | jq -r '.databaseId | tostring')
|
|
1360
|
+
RS_GH_STATUS=$(printf '%s' "$newest" | jq -r '.status // ""')
|
|
1361
|
+
RS_RUN_URL=$(printf '%s' "$newest" | jq -r '.url // ""')
|
|
1362
|
+
if [ "$RS_GH_STATUS" != completed ]; then
|
|
1363
|
+
RS_STATE=running
|
|
983
1364
|
return 0
|
|
984
1365
|
fi
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
# Case 1 — already applied. A record still waiting on this run's bundle is
|
|
988
|
-
# re-checked: once it expires, the job can no longer take an answer.
|
|
989
|
-
if [ "$id" = "$synced_id" ]; then
|
|
990
|
-
case "$(hr_registry_get "$registry" "$branch" status)/$(hr_registry_get "$registry" "$branch" pause_reason)" in
|
|
991
|
-
paused/expired) ;;
|
|
992
|
-
parked/*|park_loop/*|paused/*)
|
|
993
|
-
bundle_state "$id"
|
|
994
|
-
if [ "$BUNDLE_STATE" = expired ]; then
|
|
995
|
-
sync_expired "$id" "$url" "$now"
|
|
996
|
-
return 0
|
|
997
|
-
fi
|
|
998
|
-
;;
|
|
999
|
-
esac
|
|
1000
|
-
set_or_fail remote_synced_at "$now"
|
|
1001
|
-
echo "remote-run.sh: run $id of $branch is the one last synced; the mirror is current"
|
|
1366
|
+
if [ -n "$applied" ] && [ "$RS_RUN_ID" = "$applied" ]; then
|
|
1367
|
+
RS_STATE=applied
|
|
1002
1368
|
return 0
|
|
1003
1369
|
fi
|
|
1004
1370
|
|
|
1005
|
-
# Case 2 —
|
|
1006
|
-
bundle_state "$
|
|
1371
|
+
# Case 2 — its bundle has expired.
|
|
1372
|
+
bundle_state "$RS_RUN_ID"
|
|
1007
1373
|
if [ "$BUNDLE_STATE" = expired ]; then
|
|
1008
|
-
|
|
1374
|
+
RS_STATE=paused
|
|
1375
|
+
RS_PAUSE_REASON=expired
|
|
1376
|
+
RS_DETAIL=$(expired_line "$RS_RUN_ID")
|
|
1009
1377
|
return 0
|
|
1010
1378
|
fi
|
|
1011
1379
|
|
|
1012
|
-
# Case 3 — a
|
|
1380
|
+
# Case 3 — a bundle.
|
|
1013
1381
|
if [ "$BUNDLE_STATE" = present ]; then
|
|
1014
1382
|
hr_remote_names_var
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1383
|
+
if [ -z "$download" ]; then
|
|
1384
|
+
download=$(hr_state_path "$root" "autonomous_logs/remote_download/$branch/$RS_RUN_ID") || {
|
|
1385
|
+
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
1386
|
+
exit "$EXIT_USAGE"
|
|
1387
|
+
}
|
|
1388
|
+
fi
|
|
1389
|
+
RS_DOWNLOAD="$download"
|
|
1019
1390
|
status_file="$download/$HR_REMOTE_STATUS_FILE"
|
|
1020
1391
|
if [ ! -f "$status_file" ]; then
|
|
1021
1392
|
mkdir -p "$download" || { echo "remote-run.sh: cannot create '$download'" >&2; exit "$EXIT_USAGE"; }
|
|
1022
|
-
gh_call run download "$
|
|
1393
|
+
gh_call run download "$RS_RUN_ID" -n "$STATE_ARTIFACT_NAME" -D "$download" || gh_fail "downloading the bundle of run $RS_RUN_ID failed"
|
|
1023
1394
|
fi
|
|
1024
|
-
|
|
1025
|
-
|
|
1395
|
+
RS_BUNDLE=1
|
|
1396
|
+
RS_STATE=$(hr_remote_status_get "$status_file" status) || RS_STATE=""
|
|
1397
|
+
case "$RS_STATE" in
|
|
1026
1398
|
running|parked|park_loop|paused|completed|failed) ;;
|
|
1027
1399
|
*)
|
|
1028
1400
|
echo "remote-run.sh: refused, nothing written: the bundle in '$download' is unrecognised" >&2
|
|
1029
1401
|
exit "$EXIT_REFUSED"
|
|
1030
1402
|
;;
|
|
1031
1403
|
esac
|
|
1404
|
+
RS_PAUSE_REASON=$(hr_remote_status_get "$status_file" pause_reason) || RS_PAUSE_REASON=""
|
|
1405
|
+
RS_DETAIL=$(hr_remote_status_get "$status_file" detail) || RS_DETAIL=""
|
|
1406
|
+
if [ "$RS_STATE" = running ]; then
|
|
1407
|
+
RS_STATE=paused
|
|
1408
|
+
RS_PAUSE_REASON=killed
|
|
1409
|
+
RS_DETAIL="the job ended mid-run (its bundle still says running): $RS_RUN_URL"
|
|
1410
|
+
fi
|
|
1411
|
+
RS_USAGE_RESUME_AT=$(hr_remote_status_get "$status_file" usage_resume_at) || RS_USAGE_RESUME_AT=""
|
|
1412
|
+
RS_PARK_LOOP_CYCLES=$(hr_remote_status_get "$status_file" park_loop_cycles) || RS_PARK_LOOP_CYCLES=""
|
|
1413
|
+
RS_ENGINE=$(hr_remote_status_get "$status_file" engine) || RS_ENGINE=""
|
|
1414
|
+
return 0
|
|
1415
|
+
fi
|
|
1416
|
+
|
|
1417
|
+
# Case 4 — no bundle, while some bundle exists.
|
|
1418
|
+
if [ -n "$applied" ]; then
|
|
1419
|
+
bundle_exists=1
|
|
1420
|
+
else
|
|
1421
|
+
for older in $(printf '%s' "$RS_RUNS" | jq -r '.[1:][] | select(.status == "completed") | .databaseId | tostring'); do
|
|
1422
|
+
if has_bundle "$older"; then bundle_exists=1; break; fi
|
|
1423
|
+
done
|
|
1424
|
+
fi
|
|
1425
|
+
if [ "$bundle_exists" -eq 1 ]; then
|
|
1426
|
+
RS_STATE=paused
|
|
1427
|
+
RS_PAUSE_REASON=killed
|
|
1428
|
+
RS_DETAIL="run $RS_RUN_ID ended with no state bundle (killed, cancelled or replaced): $RS_RUN_URL"
|
|
1429
|
+
return 0
|
|
1430
|
+
fi
|
|
1431
|
+
|
|
1432
|
+
# Case 5 — no bundle in any run.
|
|
1433
|
+
RS_STATE=failed
|
|
1434
|
+
RS_DETAIL="no run of $branch ever uploaded a state bundle; newest: $RS_RUN_URL"
|
|
1435
|
+
return 0
|
|
1436
|
+
}
|
|
1437
|
+
|
|
1438
|
+
verb_sync() {
|
|
1439
|
+
local worktree id url synced_id now download
|
|
1440
|
+
local status reason detail
|
|
1441
|
+
worktree=$(hr_registry_get "$registry" "$branch" worktree)
|
|
1442
|
+
if [ -z "$worktree" ] || [ ! -d "$worktree" ]; then
|
|
1443
|
+
echo "remote-run.sh: refused, nothing written: the mirror working copy '$worktree' of $branch is missing" >&2
|
|
1444
|
+
exit "$EXIT_REFUSED"
|
|
1445
|
+
fi
|
|
1446
|
+
list_runs
|
|
1447
|
+
synced_id=$(hr_registry_get "$registry" "$branch" remote_run_id)
|
|
1448
|
+
now=$(date +%s)
|
|
1449
|
+
remote_state "" "$synced_id"
|
|
1450
|
+
id="$RS_RUN_ID"
|
|
1451
|
+
url="$RS_RUN_URL"
|
|
1452
|
+
|
|
1453
|
+
case "$RS_STATE" in
|
|
1454
|
+
none)
|
|
1455
|
+
echo "remote-run.sh: no run titled 'harness run $branch' is listed yet; the record is unchanged"
|
|
1456
|
+
return 0
|
|
1457
|
+
;;
|
|
1458
|
+
running)
|
|
1459
|
+
set_many_or_fail status running remote_synced_at "$now"
|
|
1460
|
+
echo "remote-run.sh: run $id of $branch is $RS_GH_STATUS; the record is running, nothing downloaded"
|
|
1461
|
+
return 0
|
|
1462
|
+
;;
|
|
1463
|
+
applied)
|
|
1464
|
+
# Case 1 — already applied. A record still waiting on this run's bundle
|
|
1465
|
+
# is re-checked: once it expires, the job can no longer take an answer.
|
|
1466
|
+
case "$(hr_registry_get "$registry" "$branch" status)/$(hr_registry_get "$registry" "$branch" pause_reason)" in
|
|
1467
|
+
paused/expired) ;;
|
|
1468
|
+
parked/*|park_loop/*|paused/*)
|
|
1469
|
+
bundle_state "$id"
|
|
1470
|
+
if [ "$BUNDLE_STATE" = expired ]; then
|
|
1471
|
+
sync_expired "$id" "$url" "$now"
|
|
1472
|
+
return 0
|
|
1473
|
+
fi
|
|
1474
|
+
;;
|
|
1475
|
+
esac
|
|
1476
|
+
set_or_fail remote_synced_at "$now"
|
|
1477
|
+
echo "remote-run.sh: run $id of $branch is the one last synced; the mirror is current"
|
|
1478
|
+
return 0
|
|
1479
|
+
;;
|
|
1480
|
+
esac
|
|
1481
|
+
|
|
1482
|
+
# Case 2 — a newer run whose bundle has expired.
|
|
1483
|
+
if [ "$RS_PAUSE_REASON" = expired ] && [ "$RS_BUNDLE" -eq 0 ]; then
|
|
1484
|
+
sync_expired "$id" "$url" "$now"
|
|
1485
|
+
return 0
|
|
1486
|
+
fi
|
|
1487
|
+
|
|
1488
|
+
# Case 3 — a newer run with a bundle.
|
|
1489
|
+
if [ "$RS_BUNDLE" -eq 1 ]; then
|
|
1490
|
+
download="$RS_DOWNLOAD"
|
|
1032
1491
|
hr_remote_bundle_restore "$download" "$worktree" "$branch" mirror
|
|
1033
1492
|
case $? in
|
|
1034
1493
|
0) ;;
|
|
@@ -1043,44 +1502,33 @@ verb_sync() {
|
|
|
1043
1502
|
|| { echo "remote-run.sh: copying the run log to '$remote_log' failed" >&2; exit "$EXIT_USAGE"; }
|
|
1044
1503
|
fi
|
|
1045
1504
|
fi
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
status=paused
|
|
1050
|
-
reason=killed
|
|
1051
|
-
detail="the job ended mid-run (its bundle still says running): $url"
|
|
1052
|
-
fi
|
|
1505
|
+
status="$RS_STATE"
|
|
1506
|
+
reason="$RS_PAUSE_REASON"
|
|
1507
|
+
detail="$RS_DETAIL"
|
|
1053
1508
|
[ -n "$detail" ] || detail="synced from $url"
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1509
|
+
if valid_engine "$RS_ENGINE"; then
|
|
1510
|
+
set_many_or_fail status "$status" pause_reason "$reason" usage_resume_at "$RS_USAGE_RESUME_AT" \
|
|
1511
|
+
park_loop_cycles "$RS_PARK_LOOP_CYCLES" remote_run_id "$id" remote_run_url "$url" \
|
|
1512
|
+
remote_detail "$detail" remote_synced_at "$now" engine "$RS_ENGINE"
|
|
1513
|
+
else
|
|
1514
|
+
set_many_or_fail status "$status" pause_reason "$reason" usage_resume_at "$RS_USAGE_RESUME_AT" \
|
|
1515
|
+
park_loop_cycles "$RS_PARK_LOOP_CYCLES" remote_run_id "$id" remote_run_url "$url" \
|
|
1516
|
+
remote_detail "$detail" remote_synced_at "$now"
|
|
1517
|
+
fi
|
|
1059
1518
|
echo "remote-run.sh: synced run $id of $branch: $status${reason:+ ($reason)}"
|
|
1060
1519
|
return 0
|
|
1061
1520
|
fi
|
|
1062
1521
|
|
|
1063
1522
|
# Case 4 — a newer run with no bundle, while some bundle exists.
|
|
1064
|
-
|
|
1065
|
-
if [ -n "$synced_id" ]; then
|
|
1066
|
-
bundle_exists=1
|
|
1067
|
-
else
|
|
1068
|
-
for older in $(printf '%s' "$runs" | jq -r '.[1:][] | select(.status == "completed") | .databaseId | tostring'); do
|
|
1069
|
-
if has_bundle "$older"; then bundle_exists=1; break; fi
|
|
1070
|
-
done
|
|
1071
|
-
fi
|
|
1072
|
-
if [ "$bundle_exists" -eq 1 ]; then
|
|
1523
|
+
if [ "$RS_STATE" = paused ]; then
|
|
1073
1524
|
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
1074
|
-
remote_detail "
|
|
1075
|
-
remote_synced_at "$now"
|
|
1525
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
1076
1526
|
echo "remote-run.sh: run $id of $branch left no bundle; the record is paused (killed), nothing restored"
|
|
1077
1527
|
return 0
|
|
1078
1528
|
fi
|
|
1079
1529
|
|
|
1080
1530
|
# Case 5 — no bundle in any run.
|
|
1081
|
-
set_many_or_fail status failed
|
|
1082
|
-
remote_detail "no run of $branch ever uploaded a state bundle; newest: $url" \
|
|
1083
|
-
remote_synced_at "$now"
|
|
1531
|
+
set_many_or_fail status failed remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
1084
1532
|
echo "remote-run.sh: no run of $branch carries a state bundle; the record is failed"
|
|
1085
1533
|
}
|
|
1086
1534
|
|
|
@@ -1315,7 +1763,7 @@ ALL_RUNS_LISTED=0
|
|
|
1315
1763
|
list_all_runs() {
|
|
1316
1764
|
[ "$ALL_RUNS_LISTED" -eq 0 ] || return 0
|
|
1317
1765
|
gh_call run list --workflow "$WORKFLOW_RUN_FILE" \
|
|
1318
|
-
--json databaseId,headBranch,displayTitle,status,createdAt --limit "$ALL_RUNS_LIMIT" || return 1
|
|
1766
|
+
--json databaseId,headBranch,displayTitle,status,createdAt,url --limit "$ALL_RUNS_LIMIT" || return 1
|
|
1319
1767
|
printf '%s' "$GH_OUT" | jq -e 'type == "array"' >/dev/null 2>&1 || {
|
|
1320
1768
|
GH_ERR="its run list is not the expected JSON"
|
|
1321
1769
|
return 1
|
|
@@ -1389,13 +1837,13 @@ RESUME_HINT="/autonomous-sdlc-harness:branch-resume"
|
|
|
1389
1837
|
continue_redispatch() {
|
|
1390
1838
|
local status_file="$1" engine_value
|
|
1391
1839
|
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
1392
|
-
notify paused "$branch" "Not re-dispatched: remote stop is set. Run $RESUME_HINT $branch to continue."
|
|
1840
|
+
notify paused "$branch" "Not re-dispatched: remote stop is set. Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "")."
|
|
1393
1841
|
return 0
|
|
1394
1842
|
fi
|
|
1395
1843
|
remote_branch_stopped "$branch"
|
|
1396
1844
|
case $? in
|
|
1397
1845
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); not re-dispatched"; return 0 ;;
|
|
1398
|
-
2) notify paused "$branch" "Not re-dispatched: the stop-marker check failed ($GH_ERR). Run $RESUME_HINT $branch to continue."; 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 ;;
|
|
1399
1847
|
esac
|
|
1400
1848
|
if ! max_chain_var; then
|
|
1401
1849
|
notify failed "$branch" "Not re-dispatched: HARNESS_MAX_CHAIN '$MAX_CHAIN' is not a non-negative integer."
|
|
@@ -1412,19 +1860,19 @@ continue_redispatch() {
|
|
|
1412
1860
|
return 0
|
|
1413
1861
|
fi
|
|
1414
1862
|
redispatch "$engine_value" "$NEXT_CHAIN" \
|
|
1415
|
-
|| notify paused "$branch" "Re-dispatch failed ($REDISPATCH_ERR). Run $RESUME_HINT $branch to continue."
|
|
1863
|
+
|| notify paused "$branch" "Re-dispatch failed ($REDISPATCH_ERR). Run $RESUME_HINT $branch to continue; $(hr_github_resume_route "$branch" "$engine_value")."
|
|
1416
1864
|
}
|
|
1417
1865
|
|
|
1418
1866
|
continue_wait_poller() {
|
|
1419
1867
|
remote_branch_stopped "$branch"
|
|
1420
1868
|
case $? in
|
|
1421
1869
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); the resume poller is not enabled"; return 0 ;;
|
|
1422
|
-
2) notify paused "$branch" "Auto-resume not enabled: the stop-marker check failed ($GH_ERR). Run $RESUME_HINT $branch to continue."; 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 ;;
|
|
1423
1871
|
esac
|
|
1424
1872
|
if gh_call workflow enable "$WORKFLOW_RESUME_FILE"; then
|
|
1425
1873
|
echo "remote-run.sh: enabled $WORKFLOW_RESUME_FILE for $branch"
|
|
1426
1874
|
else
|
|
1427
|
-
notify paused "$branch" "Auto-resume is unavailable: enabling $WORKFLOW_RESUME_FILE failed ($GH_ERR). Run $RESUME_HINT $branch after the usage reset."
|
|
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" "")."
|
|
1428
1876
|
fi
|
|
1429
1877
|
}
|
|
1430
1878
|
|
|
@@ -1648,7 +2096,7 @@ poll_branch() {
|
|
|
1648
2096
|
if [ "$failures" -ge "$((10#$POLL_MAX_FAILURES))" ] \
|
|
1649
2097
|
|| [ "$now" -gt "$((10#$at + 10#$POLL_GIVE_UP_MINUTES * 60))" ]; then
|
|
1650
2098
|
poll_state_put "$branch" "$id" "$failures" 1
|
|
1651
|
-
notify paused "$branch" "The resume poller could not re-dispatch $branch ($REDISPATCH_ERR) after $failures attempts; automatic resume has stopped. Run $RESUME_HINT $branch."
|
|
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")."
|
|
1652
2100
|
return 1
|
|
1653
2101
|
fi
|
|
1654
2102
|
poll_state_put "$branch" "$id" "$failures" ""
|
|
@@ -1705,7 +2153,7 @@ poll_recheck() {
|
|
|
1705
2153
|
return 0
|
|
1706
2154
|
fi
|
|
1707
2155
|
for b in $POLL_WAITING; do
|
|
1708
|
-
notify paused "$b" "Auto-resume is unavailable: re-enabling $WORKFLOW_RESUME_FILE failed ($GH_ERR). Run $RESUME_HINT $b after the usage reset."
|
|
2156
|
+
notify paused "$b" "Auto-resume is unavailable: re-enabling $WORKFLOW_RESUME_FILE failed ($GH_ERR). Run $RESUME_HINT $b after the usage reset; $(hr_github_resume_route "$b" "")."
|
|
1709
2157
|
done
|
|
1710
2158
|
}
|
|
1711
2159
|
|
|
@@ -1778,6 +2226,626 @@ verb_run_created_at() {
|
|
|
1778
2226
|
printf '%s\n' "$epoch"
|
|
1779
2227
|
}
|
|
1780
2228
|
|
|
2229
|
+
# placement_fail <step> — exit 4, naming the step; nothing was dispatched.
|
|
2230
|
+
placement_fail() {
|
|
2231
|
+
echo "remote-run.sh: start of '$branch' failed at $1; nothing was dispatched" >&2
|
|
2232
|
+
exit "$EXIT_PLACEMENT"
|
|
2233
|
+
}
|
|
2234
|
+
|
|
2235
|
+
# start_remove_copy — remove the working copy and the local branch `start`
|
|
2236
|
+
# cut, and only those: `had_copy` / `had_branch` record what existed before the
|
|
2237
|
+
# cut. Idempotent; a failed step is one stderr line and never changes the exit
|
|
2238
|
+
# status already decided. `--force` because a failure exit may leave the placed,
|
|
2239
|
+
# uncommitted prompt, which is only a copy of --prompt-file.
|
|
2240
|
+
start_remove_copy() {
|
|
2241
|
+
if [ "$had_copy" -eq 0 ] && [ -e "$worktree" ]; then
|
|
2242
|
+
git -C "$root" worktree remove --force "$worktree" >/dev/null 2>&1 \
|
|
2243
|
+
|| echo "remote-run.sh: could not remove the working copy '$worktree'" >&2
|
|
2244
|
+
fi
|
|
2245
|
+
git -C "$root" worktree prune >/dev/null 2>&1 \
|
|
2246
|
+
|| echo "remote-run.sh: git worktree prune failed in '$root'" >&2
|
|
2247
|
+
if [ "$had_branch" -eq 0 ] && git -C "$root" show-ref --verify --quiet "refs/heads/$branch"; then
|
|
2248
|
+
git -C "$root" branch -D "$branch" >/dev/null 2>&1 \
|
|
2249
|
+
|| echo "remote-run.sh: could not delete the local branch '$branch'" >&2
|
|
2250
|
+
fi
|
|
2251
|
+
return 0
|
|
2252
|
+
}
|
|
2253
|
+
|
|
2254
|
+
verb_start() {
|
|
2255
|
+
local protected=0 status=0 state_rel rel subject
|
|
2256
|
+
hr_branch_is_protected "$root" "$branch" || protected=$?
|
|
2257
|
+
case "$protected" in
|
|
2258
|
+
0)
|
|
2259
|
+
echo "remote-run.sh: refused, nothing written: $branch is protected" >&2
|
|
2260
|
+
exit "$EXIT_REFUSED" ;;
|
|
2261
|
+
2)
|
|
2262
|
+
echo "remote-run.sh: refused, nothing written: cannot judge whether $branch is protected" >&2
|
|
2263
|
+
exit "$EXIT_REFUSED" ;;
|
|
2264
|
+
esac
|
|
2265
|
+
if [ ! -f "$prompt_file" ] || [ ! -r "$prompt_file" ]; then
|
|
2266
|
+
echo "remote-run.sh: refused, nothing written: the prompt file '$prompt_file' is not a readable regular file" >&2
|
|
2267
|
+
exit "$EXIT_REFUSED"
|
|
2268
|
+
fi
|
|
2269
|
+
|
|
2270
|
+
# Global, not local: the EXIT trap runs after this function's frame is gone.
|
|
2271
|
+
worktree=$(hr_worktree_dir "$root" "$branch") || placement_fail "resolving the working copy"
|
|
2272
|
+
had_copy=0
|
|
2273
|
+
[ ! -e "$worktree" ] || had_copy=1
|
|
2274
|
+
had_branch=0
|
|
2275
|
+
! git -C "$root" show-ref --verify --quiet "refs/heads/$branch" || had_branch=1
|
|
2276
|
+
|
|
2277
|
+
# The cut itself can leave a half-created copy, and every failure below exits.
|
|
2278
|
+
trap start_remove_copy EXIT
|
|
2279
|
+
bash "$script_dir/create-worktree.sh" --no-bootstrap "$branch" >&2 || status=$?
|
|
2280
|
+
[ "$status" -eq 0 ] || placement_fail "the branch cut (create-worktree.sh exited $status)"
|
|
2281
|
+
|
|
2282
|
+
state_rel=$(hr_state_dir "$worktree") || placement_fail "resolving the state directory in '$worktree'"
|
|
2283
|
+
[ -n "$state_rel" ] || placement_fail "resolving the state directory in '$worktree'"
|
|
2284
|
+
rel=$(hr_task_prompt_rel "$state_rel" "$branch")
|
|
2285
|
+
subject=$(hr_task_prompt_subject "$branch")
|
|
2286
|
+
|
|
2287
|
+
hr_place_artifact "$worktree" "$prompt_file" "$rel" || placement_fail "copying the prompt to '$worktree/$rel'"
|
|
2288
|
+
# 3 (nothing staged) cannot happen on a freshly cut branch, so it reads as 0.
|
|
2289
|
+
status=0
|
|
2290
|
+
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$subject" >&2 || status=$?
|
|
2291
|
+
[ "$status" -ne 1 ] || placement_fail "committing '$rel'"
|
|
2292
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 \
|
|
2293
|
+
|| placement_fail "pushing $branch (origin/$branch is not HEAD)"
|
|
2294
|
+
start_remove_copy
|
|
2295
|
+
trap - EXIT
|
|
2296
|
+
|
|
2297
|
+
engine=task
|
|
2298
|
+
resume=none
|
|
2299
|
+
chain=0
|
|
2300
|
+
dispatch_fail_note="; $branch and its task prompt are already pushed to origin, so re-send with: remote-run.sh dispatch $branch --engine task"
|
|
2301
|
+
verb_dispatch
|
|
2302
|
+
echo "remote-run.sh: started $branch"
|
|
2303
|
+
}
|
|
2304
|
+
|
|
2305
|
+
# ---------------------------------------------------------------------------
|
|
2306
|
+
# `fetch` — the newest remote state of one branch, for the plugin commands.
|
|
2307
|
+
# ---------------------------------------------------------------------------
|
|
2308
|
+
|
|
2309
|
+
verb_fetch() {
|
|
2310
|
+
local open_questions="" bundle_dir=""
|
|
2311
|
+
if [ ! -d "$out_dir" ]; then
|
|
2312
|
+
echo "remote-run.sh: fetch needs an existing directory, not '$out_dir'" >&2
|
|
2313
|
+
exit "$EXIT_USAGE"
|
|
2314
|
+
fi
|
|
2315
|
+
if [ -n "$(ls -A "$out_dir" 2>/dev/null)" ]; then
|
|
2316
|
+
echo "remote-run.sh: fetch needs an empty directory, and '$out_dir' is not" >&2
|
|
2317
|
+
exit "$EXIT_USAGE"
|
|
2318
|
+
fi
|
|
2319
|
+
list_runs
|
|
2320
|
+
remote_state "$out_dir"
|
|
2321
|
+
if [ "$RS_BUNDLE" -eq 1 ]; then
|
|
2322
|
+
bundle_dir="$out_dir"
|
|
2323
|
+
open_questions_in "$out_dir"
|
|
2324
|
+
open_questions="$OPEN_QUESTIONS"
|
|
2325
|
+
fi
|
|
2326
|
+
printf 'run_id: %s\n' "$RS_RUN_ID"
|
|
2327
|
+
printf 'run_url: %s\n' "$RS_RUN_URL"
|
|
2328
|
+
printf 'run_status: %s\n' "$RS_GH_STATUS"
|
|
2329
|
+
printf 'state: %s\n' "$RS_STATE"
|
|
2330
|
+
printf 'pause_reason: %s\n' "$RS_PAUSE_REASON"
|
|
2331
|
+
printf 'engine: %s\n' "$RS_ENGINE"
|
|
2332
|
+
printf 'detail: %s\n' "$RS_DETAIL"
|
|
2333
|
+
printf 'open_questions: %s\n' "$open_questions"
|
|
2334
|
+
printf 'bundle_dir: %s\n' "$bundle_dir"
|
|
2335
|
+
}
|
|
2336
|
+
|
|
2337
|
+
# ---------------------------------------------------------------------------
|
|
2338
|
+
# `review` — a user review round placed on the branch tip, then dispatched.
|
|
2339
|
+
# ---------------------------------------------------------------------------
|
|
2340
|
+
|
|
2341
|
+
# review_fail <step> — exit 4, naming the step; nothing was dispatched.
|
|
2342
|
+
review_fail() {
|
|
2343
|
+
echo "remote-run.sh: review of '$branch' failed at $1; nothing was dispatched" >&2
|
|
2344
|
+
exit "$EXIT_PLACEMENT"
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
# remote_record_exists <registry> — 0 when <registry> is a file holding a
|
|
2348
|
+
# record of the branch with `execution: github-actions`. Tested with -f first:
|
|
2349
|
+
# hr_registry_get creates an absent registry.
|
|
2350
|
+
remote_record_exists() {
|
|
2351
|
+
[ -n "${1-}" ] && [ -f "$1" ] && [ "$(hr_registry_get "$1" "$branch" execution)" = github-actions ]
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
verb_review() {
|
|
2355
|
+
local protected=0 status=0 reg="" record_wt="" use_mirror=0 state_rel names name round max=0 next rel
|
|
2356
|
+
hr_branch_is_protected "$root" "$branch" || protected=$?
|
|
2357
|
+
case "$protected" in
|
|
2358
|
+
0)
|
|
2359
|
+
echo "remote-run.sh: refused, nothing written: $branch is protected" >&2
|
|
2360
|
+
exit "$EXIT_REFUSED" ;;
|
|
2361
|
+
2)
|
|
2362
|
+
echo "remote-run.sh: refused, nothing written: cannot judge whether $branch is protected" >&2
|
|
2363
|
+
exit "$EXIT_REFUSED" ;;
|
|
2364
|
+
esac
|
|
2365
|
+
if [ ! -f "$review_file" ] || [ ! -r "$review_file" ]; then
|
|
2366
|
+
echo "remote-run.sh: refused, nothing written: the review file '$review_file' is not a readable regular file" >&2
|
|
2367
|
+
exit "$EXIT_REFUSED"
|
|
2368
|
+
fi
|
|
2369
|
+
|
|
2370
|
+
# A run in flight takes no review: the local rule, `completed` or `failed` only.
|
|
2371
|
+
list_runs
|
|
2372
|
+
remote_state ""
|
|
2373
|
+
case "$RS_STATE" in
|
|
2374
|
+
completed|failed) ;;
|
|
2375
|
+
none)
|
|
2376
|
+
echo "remote-run.sh: refused, nothing written: no \`harness run $branch\` run on GitHub" >&2
|
|
2377
|
+
exit "$EXIT_REFUSED" ;;
|
|
2378
|
+
paused)
|
|
2379
|
+
if [ "$RS_PAUSE_REASON" = expired ]; then
|
|
2380
|
+
echo "remote-run.sh: refused, nothing written: $RS_DETAIL" >&2
|
|
2381
|
+
else
|
|
2382
|
+
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
|
|
2383
|
+
fi
|
|
2384
|
+
exit "$EXIT_REFUSED" ;;
|
|
2385
|
+
*)
|
|
2386
|
+
echo "remote-run.sh: refused, nothing written: $branch is $RS_STATE on GitHub; a review waits until its run is completed or failed" >&2
|
|
2387
|
+
exit "$EXIT_REFUSED" ;;
|
|
2388
|
+
esac
|
|
2389
|
+
|
|
2390
|
+
# The copy: the remote record's mirror when it is on the branch, never
|
|
2391
|
+
# removed; else a copy this verb cuts and removes on every exit.
|
|
2392
|
+
reg=$(hr_state_path "$root" autonomous_logs/registry.json) || reg=""
|
|
2393
|
+
if remote_record_exists "$reg"; then
|
|
2394
|
+
record_wt=$(hr_registry_get "$reg" "$branch" worktree)
|
|
2395
|
+
if [ -n "$record_wt" ] && [ -d "$record_wt" ] \
|
|
2396
|
+
&& [ "$(git -C "$record_wt" symbolic-ref --short HEAD 2>/dev/null)" = "$branch" ]; then
|
|
2397
|
+
use_mirror=1
|
|
2398
|
+
fi
|
|
2399
|
+
fi
|
|
2400
|
+
if [ "$use_mirror" -eq 1 ]; then
|
|
2401
|
+
worktree="$record_wt"
|
|
2402
|
+
else
|
|
2403
|
+
# Global, not local: the EXIT trap runs after this function's frame is gone.
|
|
2404
|
+
worktree=$(hr_worktree_dir "$root" "$branch") || review_fail "resolving the working copy"
|
|
2405
|
+
had_copy=0
|
|
2406
|
+
[ ! -e "$worktree" ] || had_copy=1
|
|
2407
|
+
had_branch=0
|
|
2408
|
+
! git -C "$root" show-ref --verify --quiet "refs/heads/$branch" || had_branch=1
|
|
2409
|
+
trap start_remove_copy EXIT
|
|
2410
|
+
bash "$script_dir/create-worktree.sh" --existing --no-bootstrap "$branch" >&2 || status=$?
|
|
2411
|
+
[ "$status" -eq 0 ] || review_fail "the working copy (create-worktree.sh exited $status)"
|
|
2412
|
+
fi
|
|
2413
|
+
# Also for a cut copy: a local branch that existed before may be behind origin.
|
|
2414
|
+
git -C "$worktree" fetch origin "$branch" >&2 || review_fail "git fetch origin $branch in '$worktree'"
|
|
2415
|
+
git -C "$worktree" merge --ff-only "origin/$branch" >&2 || review_fail "git merge --ff-only origin/$branch in '$worktree'"
|
|
2416
|
+
|
|
2417
|
+
# The round, from the branch tip: the engine's own round source.
|
|
2418
|
+
state_rel=$(hr_state_dir "$worktree") || review_fail "resolving the state directory in '$worktree'"
|
|
2419
|
+
[ -n "$state_rel" ] || review_fail "resolving the state directory in '$worktree'"
|
|
2420
|
+
state_rel="${state_rel%/}"
|
|
2421
|
+
names=$(git -C "$worktree" ls-tree --name-only HEAD -- "$state_rel/user_reviews/") \
|
|
2422
|
+
|| review_fail "listing $state_rel/user_reviews/ on $branch"
|
|
2423
|
+
while IFS= read -r name; do
|
|
2424
|
+
name="${name##*/}"
|
|
2425
|
+
[[ "$name" =~ ^(.+)_review(_([0-9]+))?\.md$ ]] || continue
|
|
2426
|
+
[ "${BASH_REMATCH[1]}" = "$branch" ] || continue
|
|
2427
|
+
round="${BASH_REMATCH[3]:-1}"
|
|
2428
|
+
round=$((10#$round))
|
|
2429
|
+
[ "$round" -le "$max" ] || max="$round"
|
|
2430
|
+
done <<NAMES
|
|
2431
|
+
$names
|
|
2432
|
+
NAMES
|
|
2433
|
+
if [ "$max" -eq 0 ]; then
|
|
2434
|
+
round=1
|
|
2435
|
+
next="${branch}_review.md"
|
|
2436
|
+
else
|
|
2437
|
+
round=$((max + 1))
|
|
2438
|
+
next="${branch}_review_$round.md"
|
|
2439
|
+
fi
|
|
2440
|
+
rel="$state_rel/user_reviews/$next"
|
|
2441
|
+
|
|
2442
|
+
hr_place_artifact "$worktree" "$review_file" "$rel" || review_fail "copying the review to '$worktree/$rel'"
|
|
2443
|
+
# 3 (nothing staged) cannot happen for a new round's file, so it reads as a failure.
|
|
2444
|
+
status=0
|
|
2445
|
+
hr_commit_placed "$script_dir/commit-on-branch.sh" "$worktree" "$rel" "$(hr_user_review_subject "$branch")" >&2 || status=$?
|
|
2446
|
+
[ "$status" -eq 0 ] || review_fail "committing '$rel'"
|
|
2447
|
+
hr_push_landed "$script_dir/push-branch.sh" "$worktree" "$branch" >&2 \
|
|
2448
|
+
|| review_fail "pushing $branch (origin/$branch is not HEAD)"
|
|
2449
|
+
if [ "$use_mirror" -eq 0 ]; then
|
|
2450
|
+
start_remove_copy
|
|
2451
|
+
trap - EXIT
|
|
2452
|
+
fi
|
|
2453
|
+
|
|
2454
|
+
echo "remote-run.sh: placed $rel (round $round) on $branch"
|
|
2455
|
+
engine=user_review
|
|
2456
|
+
resume=none
|
|
2457
|
+
chain=0
|
|
2458
|
+
dispatch_fail_note="; the review is already pushed to origin/$branch, so re-send with: remote-run.sh dispatch $branch --engine user_review"
|
|
2459
|
+
verb_dispatch
|
|
2460
|
+
|
|
2461
|
+
if remote_record_exists "$reg"; then
|
|
2462
|
+
hr_registry_set "$reg" "$branch" status running engine user_review \
|
|
2463
|
+
|| echo "remote-run.sh: dispatched, but the local record of $branch could not be updated" >&2
|
|
2464
|
+
fi
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
# unrecorded_runs — UNRECORDED: one `<branch>\t<url>` line per branch whose
|
|
2468
|
+
# newest `harness run <branch>` run is on GitHub, newest first, dropping a
|
|
2469
|
+
# branch with a registry record, one `hr_branch_is_protected` does not answer 1
|
|
2470
|
+
# for, and one that is not a live head on origin. One listing and one
|
|
2471
|
+
# `ls-remote`; writes nothing. Exits 3 when either fails.
|
|
2472
|
+
UNRECORDED=""
|
|
2473
|
+
unrecorded_runs() {
|
|
2474
|
+
local titled reg recorded="" heads="" live=$'\n' ref b url protected
|
|
2475
|
+
UNRECORDED=""
|
|
2476
|
+
reg=$(hr_state_path "$root" autonomous_logs/registry.json) || {
|
|
2477
|
+
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
2478
|
+
exit "$EXIT_USAGE"
|
|
2479
|
+
}
|
|
2480
|
+
list_all_runs || gh_fail "listing the runs of $WORKFLOW_RUN_FILE failed"
|
|
2481
|
+
# Newest first, one line per branch: its newest `harness run <branch>` run.
|
|
2482
|
+
titled=$(printf '%s' "$ALL_RUNS" | jq -r '
|
|
2483
|
+
[.[] | select((.displayTitle // "") | startswith("harness run "))
|
|
2484
|
+
| . + {b: (.displayTitle | ltrimstr("harness run "))}]
|
|
2485
|
+
| sort_by([.createdAt, .databaseId]) | reverse
|
|
2486
|
+
| reduce .[] as $r ({seen: {}, out: []};
|
|
2487
|
+
if .seen[$r.b] then . else (.seen[$r.b] = true | .out += [$r]) end)
|
|
2488
|
+
| .out[] | "\(.b)\t\(.url // "")"' 2>/dev/null) || {
|
|
2489
|
+
GH_ERR="its run list is not the expected JSON"
|
|
2490
|
+
gh_fail "listing the runs of $WORKFLOW_RUN_FILE failed"
|
|
2491
|
+
}
|
|
2492
|
+
|
|
2493
|
+
# Tested with -f first: hr_registry_get creates an absent registry.
|
|
2494
|
+
if [ -f "$reg" ]; then
|
|
2495
|
+
recorded=$(jq -r '.runs | keys[]' "$reg" 2>/dev/null) || {
|
|
2496
|
+
echo "remote-run.sh: cannot read the registry '$reg'" >&2
|
|
2497
|
+
exit "$EXIT_USAGE"
|
|
2498
|
+
}
|
|
2499
|
+
fi
|
|
2500
|
+
recorded=$'\n'"$recorded"$'\n'
|
|
2501
|
+
|
|
2502
|
+
if [ -n "$titled" ]; then
|
|
2503
|
+
heads=$(git -C "$root" ls-remote --heads origin 2>&1) || {
|
|
2504
|
+
echo "remote-run.sh: reading origin's branches failed: ${heads%%$'\n'*}" >&2
|
|
2505
|
+
exit "$EXIT_GH"
|
|
2506
|
+
}
|
|
2507
|
+
while IFS=$'\t' read -r _ ref; do
|
|
2508
|
+
case "$ref" in refs/heads/*) live="$live${ref#refs/heads/}"$'\n' ;; esac
|
|
2509
|
+
done <<EOF
|
|
2510
|
+
$heads
|
|
2511
|
+
EOF
|
|
2512
|
+
fi
|
|
2513
|
+
|
|
2514
|
+
while IFS=$'\t' read -r b url; do
|
|
2515
|
+
valid_branch "$b" || continue
|
|
2516
|
+
case "$recorded" in *$'\n'"$b"$'\n'*) continue ;; esac
|
|
2517
|
+
protected=0
|
|
2518
|
+
hr_branch_is_protected "$root" "$b" || protected=$?
|
|
2519
|
+
[ "$protected" -eq 1 ] || continue
|
|
2520
|
+
case "$live" in *$'\n'"$b"$'\n'*) ;; *) continue ;; esac
|
|
2521
|
+
UNRECORDED="$UNRECORDED$b"$'\t'"$url"$'\n'
|
|
2522
|
+
done <<EOF
|
|
2523
|
+
$titled
|
|
2524
|
+
EOF
|
|
2525
|
+
}
|
|
2526
|
+
|
|
2527
|
+
# ---------------------------------------------------------------------------
|
|
2528
|
+
# `list` — the runs on GitHub with no local record, for `branch-status`.
|
|
2529
|
+
# ---------------------------------------------------------------------------
|
|
2530
|
+
|
|
2531
|
+
verb_list() {
|
|
2532
|
+
local b url
|
|
2533
|
+
unrecorded_runs
|
|
2534
|
+
if [ -z "$UNRECORDED" ]; then
|
|
2535
|
+
echo "remote-run.sh: no run on GitHub without a local record"
|
|
2536
|
+
return 0
|
|
2537
|
+
fi
|
|
2538
|
+
while IFS=$'\t' read -r b url; do
|
|
2539
|
+
[ -n "$b" ] || continue
|
|
2540
|
+
echo "remote-run.sh: on GitHub, no local record: $b $url"
|
|
2541
|
+
done <<EOF
|
|
2542
|
+
$UNRECORDED
|
|
2543
|
+
EOF
|
|
2544
|
+
}
|
|
2545
|
+
|
|
2546
|
+
# ---------------------------------------------------------------------------
|
|
2547
|
+
# `trigger` — the event adapter. Event text is data: every field is read by
|
|
2548
|
+
# `jq` into a variable and reaches a command only as one argument or as file
|
|
2549
|
+
# bytes, never as shell source.
|
|
2550
|
+
# ---------------------------------------------------------------------------
|
|
2551
|
+
|
|
2552
|
+
trigger_tmp=""
|
|
2553
|
+
trigger_label=""
|
|
2554
|
+
issue_number=""
|
|
2555
|
+
# `issue` or `dispatch`: where trigger_finish reports.
|
|
2556
|
+
trigger_source=""
|
|
2557
|
+
|
|
2558
|
+
# event_field <jq filter> — one field of the event into EVENT_VALUE, its bytes
|
|
2559
|
+
# kept (a command substitution alone would drop trailing newlines). 1 when jq
|
|
2560
|
+
# cannot read it.
|
|
2561
|
+
EVENT_VALUE=""
|
|
2562
|
+
event_field() {
|
|
2563
|
+
local out
|
|
2564
|
+
out=$(jq -j "$1" "$GITHUB_EVENT_PATH" 2>/dev/null && printf x) || return 1
|
|
2565
|
+
EVENT_VALUE=${out%x}
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
# trigger_finish <exit> <comment> — post <comment> on the issue, remove the
|
|
2569
|
+
# trigger label, and exit <exit>. A comment that cannot be posted makes the exit
|
|
2570
|
+
# 3; a label that cannot be removed is a warning only. For a dispatch event,
|
|
2571
|
+
# print <comment> and append it to GITHUB_STEP_SUMMARY when set; a summary that
|
|
2572
|
+
# cannot be appended is a warning only, since stdout already carries it.
|
|
2573
|
+
trigger_finish() {
|
|
2574
|
+
local code="$1" body="$2" file
|
|
2575
|
+
if [ "$trigger_source" = dispatch ]; then
|
|
2576
|
+
printf '%s\n' "$body"
|
|
2577
|
+
if [ -n "${GITHUB_STEP_SUMMARY-}" ]; then
|
|
2578
|
+
printf '%s\n' "### harness trigger" "" "$body" "" >>"$GITHUB_STEP_SUMMARY" \
|
|
2579
|
+
|| echo "::warning::remote-run.sh: trigger: cannot append to GITHUB_STEP_SUMMARY"
|
|
2580
|
+
fi
|
|
2581
|
+
exit "$code"
|
|
2582
|
+
fi
|
|
2583
|
+
if [ -n "${GITHUB_RUN_ID-}" ]; then
|
|
2584
|
+
body="$body
|
|
2585
|
+
|
|
2586
|
+
_Posted by the trigger job ${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY-}/actions/runs/$GITHUB_RUN_ID._"
|
|
2587
|
+
fi
|
|
2588
|
+
if ! file=$(mktemp "$trigger_tmp/harness-trigger-comment.XXXXXX"); then
|
|
2589
|
+
echo "::error::remote-run.sh: trigger: cannot create the comment file for issue #$issue_number under '$trigger_tmp'"
|
|
2590
|
+
exit "$EXIT_GH"
|
|
2591
|
+
fi
|
|
2592
|
+
printf '%s\n' "$body" >"$file"
|
|
2593
|
+
if ! gh_call issue comment "$issue_number" --repo "${GITHUB_REPOSITORY-}" --body-file "$file"; then
|
|
2594
|
+
echo "::error::remote-run.sh: trigger: the comment on issue #$issue_number could not be posted: $GH_ERR"
|
|
2595
|
+
code="$EXIT_GH"
|
|
2596
|
+
fi
|
|
2597
|
+
rm -f "$file"
|
|
2598
|
+
if ! gh_call issue edit "$issue_number" --repo "${GITHUB_REPOSITORY-}" --remove-label "$trigger_label"; then
|
|
2599
|
+
echo "::warning::remote-run.sh: trigger: removing the label '$trigger_label' from issue #$issue_number failed: $GH_ERR"
|
|
2600
|
+
fi
|
|
2601
|
+
exit "$code"
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
# trigger_refuse <reason> <way on> — print the reason, comment both, exit 2.
|
|
2605
|
+
trigger_refuse() {
|
|
2606
|
+
echo "remote-run.sh: trigger: refused, nothing sent: $1" >&2
|
|
2607
|
+
trigger_finish "$EXIT_REFUSED" "No run started: $1
|
|
2608
|
+
|
|
2609
|
+
$2"
|
|
2610
|
+
}
|
|
2611
|
+
|
|
2612
|
+
# trigger_bot_listed <login> — 0 when <login> is an exact entry of
|
|
2613
|
+
# HARNESS_TRIGGER_ALLOWED_BOTS, split on `,` with each entry trimmed.
|
|
2614
|
+
trigger_bot_listed() {
|
|
2615
|
+
local rest="${HARNESS_TRIGGER_ALLOWED_BOTS-}," entry
|
|
2616
|
+
while [ -n "$rest" ]; do
|
|
2617
|
+
entry=${rest%%,*}
|
|
2618
|
+
rest=${rest#*,}
|
|
2619
|
+
entry=${entry#"${entry%%[![:space:]]*}"}
|
|
2620
|
+
entry=${entry%"${entry##*[![:space:]]}"}
|
|
2621
|
+
[ -n "$entry" ] && [ "$entry" = "$1" ] && return 0
|
|
2622
|
+
done
|
|
2623
|
+
return 1
|
|
2624
|
+
}
|
|
2625
|
+
|
|
2626
|
+
# trigger_run_url — the URL of the `harness run <branch>` run `start` just
|
|
2627
|
+
# dispatched, looked up at most TRIGGER_RUN_LOOKUP_TRIES times; the branch's
|
|
2628
|
+
# filtered run list when none appears. Never fails.
|
|
2629
|
+
trigger_run_url() {
|
|
2630
|
+
local try=1 secs url=""
|
|
2631
|
+
secs="${HARNESS_TRIGGER_LOOKUP_SECS-}"
|
|
2632
|
+
case "$secs" in
|
|
2633
|
+
''|*[!0-9]*) secs="$TRIGGER_LOOKUP_SECS_DEFAULT" ;;
|
|
2634
|
+
esac
|
|
2635
|
+
while :; do
|
|
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=""
|
|
2639
|
+
[ -z "$url" ] || break
|
|
2640
|
+
else
|
|
2641
|
+
echo "remote-run.sh: trigger: looking up the run of $branch failed: $GH_ERR" >&2
|
|
2642
|
+
fi
|
|
2643
|
+
[ "$try" -lt "$TRIGGER_RUN_LOOKUP_TRIES" ] || break
|
|
2644
|
+
try=$((try + 1))
|
|
2645
|
+
sleep "$secs"
|
|
2646
|
+
done
|
|
2647
|
+
# A derived name is `[a-z0-9_]` only, so it needs no encoding in the query.
|
|
2648
|
+
[ -n "$url" ] || url="${GITHUB_SERVER_URL:-https://github.com}/${GITHUB_REPOSITORY-}/actions/workflows/${WORKFLOW_RUN_FILE}?query=branch%3A$branch"
|
|
2649
|
+
printf '%s\n' "$url"
|
|
2650
|
+
}
|
|
2651
|
+
|
|
2652
|
+
verb_trigger() {
|
|
2653
|
+
local LC_ALL=C
|
|
2654
|
+
local action label title body html_url state login sender_type source=""
|
|
2655
|
+
local forge="" target="" default name_file status permission prompt errfile last url
|
|
2656
|
+
local fallback retry_then retry_again task_what
|
|
2657
|
+
case "${GITHUB_EVENT_NAME-}" in
|
|
2658
|
+
issues) trigger_source=issue ;;
|
|
2659
|
+
repository_dispatch) trigger_source=dispatch ;;
|
|
2660
|
+
*)
|
|
2661
|
+
echo "remote-run.sh: trigger handles GITHUB_EVENT_NAME issues or repository_dispatch, not '${GITHUB_EVENT_NAME-}'" >&2
|
|
2662
|
+
exit "$EXIT_USAGE" ;;
|
|
2663
|
+
esac
|
|
2664
|
+
if [ -z "${GITHUB_EVENT_PATH-}" ] || [ ! -f "$GITHUB_EVENT_PATH" ] || [ ! -r "$GITHUB_EVENT_PATH" ]; then
|
|
2665
|
+
echo "remote-run.sh: trigger: cannot read the event file '${GITHUB_EVENT_PATH-}'" >&2
|
|
2666
|
+
exit "$EXIT_USAGE"
|
|
2667
|
+
fi
|
|
2668
|
+
hr_have_jq || { echo "remote-run.sh: trigger needs jq" >&2; exit "$EXIT_USAGE"; }
|
|
2669
|
+
|
|
2670
|
+
if [ "$trigger_source" = issue ]; then
|
|
2671
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
2672
|
+
&& event_field '.label.name // ""' && label="$EVENT_VALUE" \
|
|
2673
|
+
&& event_field '.issue.number // ""' && issue_number="$EVENT_VALUE" \
|
|
2674
|
+
&& event_field '.issue.title // ""' && title="$EVENT_VALUE" \
|
|
2675
|
+
&& event_field '.issue.body // ""' && body="$EVENT_VALUE" \
|
|
2676
|
+
&& event_field '.issue.html_url // ""' && html_url="$EVENT_VALUE" \
|
|
2677
|
+
&& event_field '.issue.state // ""' && state="$EVENT_VALUE" \
|
|
2678
|
+
&& event_field '.sender.login // ""' && login="$EVENT_VALUE" \
|
|
2679
|
+
&& event_field '.sender.type // ""' && sender_type="$EVENT_VALUE"; } || {
|
|
2680
|
+
echo "remote-run.sh: trigger: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
2681
|
+
exit "$EXIT_USAGE"
|
|
2682
|
+
}
|
|
2683
|
+
trigger_label="${HARNESS_TRIGGER_LABEL:-$DEFAULT_TRIGGER_LABEL}"
|
|
2684
|
+
if [ "$action" != labeled ] || [ "$label" != "$trigger_label" ]; then
|
|
2685
|
+
echo "remote-run.sh: trigger: ignored, not the label '$trigger_label' being applied"
|
|
2686
|
+
return 0
|
|
2687
|
+
fi
|
|
2688
|
+
case "$issue_number" in
|
|
2689
|
+
''|*[!0-9]*|0*)
|
|
2690
|
+
echo "remote-run.sh: trigger: the event carries no issue number" >&2
|
|
2691
|
+
exit "$EXIT_USAGE" ;;
|
|
2692
|
+
esac
|
|
2693
|
+
fallback="issue_$issue_number"
|
|
2694
|
+
retry_then="re-apply the label \`$trigger_label\`"
|
|
2695
|
+
retry_again="Re-apply the label \`$trigger_label\` to try again."
|
|
2696
|
+
task_what="this issue's task"
|
|
2697
|
+
else
|
|
2698
|
+
# GitHub puts a repository_dispatch's `event_type` in `.action`.
|
|
2699
|
+
{ event_field '.action // ""' && action="$EVENT_VALUE" \
|
|
2700
|
+
&& event_field '.client_payload.title // ""' && title="$EVENT_VALUE" \
|
|
2701
|
+
&& event_field '.client_payload.body // ""' && body="$EVENT_VALUE" \
|
|
2702
|
+
&& event_field '.client_payload.source // ""' && source="$EVENT_VALUE"; } || {
|
|
2703
|
+
echo "remote-run.sh: trigger: '$GITHUB_EVENT_PATH' is not a readable event" >&2
|
|
2704
|
+
exit "$EXIT_USAGE"
|
|
2705
|
+
}
|
|
2706
|
+
if [ "$action" != "$TRIGGER_DISPATCH_EVENT_TYPE" ]; then
|
|
2707
|
+
echo "remote-run.sh: trigger: ignored, a repository_dispatch of type '$action', not '$TRIGGER_DISPATCH_EVENT_TYPE'"
|
|
2708
|
+
return 0
|
|
2709
|
+
fi
|
|
2710
|
+
fallback="task_${GITHUB_RUN_ID-}"
|
|
2711
|
+
retry_then="send the \`$TRIGGER_DISPATCH_EVENT_TYPE\` dispatch again"
|
|
2712
|
+
retry_again="Send the \`$TRIGGER_DISPATCH_EVENT_TYPE\` dispatch again to try again."
|
|
2713
|
+
task_what="the dispatched task"
|
|
2714
|
+
fi
|
|
2715
|
+
|
|
2716
|
+
trigger_tmp="${RUNNER_TEMP-}"
|
|
2717
|
+
if [ -z "$trigger_tmp" ] || [ ! -d "$trigger_tmp" ]; then
|
|
2718
|
+
trigger_tmp=$(mktemp -d) || { echo "remote-run.sh: trigger: mktemp failed" >&2; exit "$EXIT_USAGE"; }
|
|
2719
|
+
fi
|
|
2720
|
+
|
|
2721
|
+
if [ "$trigger_source" = dispatch ] && [ -z "$title" ]; then
|
|
2722
|
+
trigger_refuse "the dispatch's \`client_payload\` carries no \`title\`." \
|
|
2723
|
+
"Send it as \`{\"event_type\": \"$TRIGGER_DISPATCH_EVENT_TYPE\", \"client_payload\": {\"title\": …, \"body\": …, \"source\": …}}\`, with a non-empty \`title\`."
|
|
2724
|
+
fi
|
|
2725
|
+
|
|
2726
|
+
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
2727
|
+
trigger_refuse "the repository variable \`HARNESS_REMOTE_STOP\` is set, which stops every start." \
|
|
2728
|
+
"Clear it under **Settings → Secrets and variables → Actions → Variables**, then $retry_then."
|
|
2729
|
+
fi
|
|
2730
|
+
|
|
2731
|
+
forge=$(hr_forge "$root") || forge=""
|
|
2732
|
+
target=$(hr_execution_target "$root") || target=""
|
|
2733
|
+
if [ "$forge" != github ] || [ "$target" != github-actions ]; then
|
|
2734
|
+
trigger_refuse "the default branch's \`harness.config.json\` does not turn the $trigger_source trigger 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})." \
|
|
2735
|
+
"Set both keys on the default branch, then $retry_then."
|
|
2736
|
+
fi
|
|
2737
|
+
|
|
2738
|
+
if [ "$trigger_source" = issue ]; then
|
|
2739
|
+
if [ "$state" != open ]; then
|
|
2740
|
+
trigger_refuse "this issue is not open." "Reopen it, then re-apply the label \`$trigger_label\`."
|
|
2741
|
+
fi
|
|
2742
|
+
|
|
2743
|
+
if [ "$login" = ghost ] || [ -z "$login" ] \
|
|
2744
|
+
|| ! { [[ "$login" =~ ^[A-Za-z0-9][A-Za-z0-9-]*$ ]] \
|
|
2745
|
+
|| { [ "$sender_type" = Bot ] && [[ "$login" =~ ^[A-Za-z0-9][A-Za-z0-9-]*\[bot\]$ ]]; }; }; then
|
|
2746
|
+
trigger_refuse "the label was applied by an account GitHub does not name (a deleted account shows as \`ghost\`)." \
|
|
2747
|
+
"A collaborator with write access can re-apply the label \`$trigger_label\`."
|
|
2748
|
+
fi
|
|
2749
|
+
|
|
2750
|
+
if [ "$sender_type" != User ]; then
|
|
2751
|
+
trigger_bot_listed "$login" || trigger_refuse \
|
|
2752
|
+
"@$login is not a person, and is not listed in the repository variable \`HARNESS_TRIGGER_ALLOWED_BOTS\`." \
|
|
2753
|
+
"Add \`$login\` to that comma-separated list to let it start runs, or have a collaborator with write access apply the label \`$trigger_label\`."
|
|
2754
|
+
else
|
|
2755
|
+
permission=""
|
|
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
|
|
2768
|
+
fi
|
|
2769
|
+
|
|
2770
|
+
# The name check reads origin/<defaultBranch>; a failed fetch leaves it to say so.
|
|
2771
|
+
default=$(hr_default_branch "$root") || default=""
|
|
2772
|
+
if [ -n "$default" ]; then
|
|
2773
|
+
git -C "$root" fetch --quiet origin "$default" >&2 || echo "remote-run.sh: trigger: fetching origin $default failed" >&2
|
|
2774
|
+
fi
|
|
2775
|
+
name_file=$(mktemp "$trigger_tmp/harness-trigger-branch.XXXXXX") || trigger_refuse \
|
|
2776
|
+
"the branch name could not be derived (mktemp failed)." "$retry_again"
|
|
2777
|
+
status=0
|
|
2778
|
+
hr_derive_branch "$root" "$title" "$fallback" >"$name_file" || status=$?
|
|
2779
|
+
branch=""
|
|
2780
|
+
IFS= read -r branch <"$name_file" || :
|
|
2781
|
+
rm -f "$name_file"
|
|
2782
|
+
case "$status" in
|
|
2783
|
+
0) ;;
|
|
2784
|
+
3) if [ "$trigger_source" = issue ]; then
|
|
2785
|
+
trigger_refuse "every branch name derived from this issue's title, through the suffix \`_99\`, is already taken." \
|
|
2786
|
+
"Retitle the issue, then re-apply the label \`$trigger_label\`."
|
|
2787
|
+
fi
|
|
2788
|
+
trigger_refuse "every branch name derived from the dispatch's \`title\`, through the suffix \`_99\`, is already taken." \
|
|
2789
|
+
"Send the dispatch again with another \`title\`." ;;
|
|
2790
|
+
*) trigger_refuse "the branch name for $task_what could not be checked (${HR_TAKEN_WHY:-no usable name})." \
|
|
2791
|
+
"$retry_again" ;;
|
|
2792
|
+
esac
|
|
2793
|
+
|
|
2794
|
+
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"
|
|
2796
|
+
if [ "$trigger_source" = issue ]; then
|
|
2797
|
+
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
|
+
"$title" "$body" "$html_url" "$login" "$trigger_label" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$prompt"
|
|
2799
|
+
else
|
|
2800
|
+
[ -z "$source" ] || source=", from $source"
|
|
2801
|
+
printf '# %s\n\n%s\n\n---\n\nStarted by a repository_dispatch event of type `%s`%s at %s.\n' \
|
|
2802
|
+
"$title" "$body" "$TRIGGER_DISPATCH_EVENT_TYPE" "$source" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" >"$prompt"
|
|
2803
|
+
fi
|
|
2804
|
+
|
|
2805
|
+
errfile=$(mktemp "$trigger_tmp/harness-trigger-start.XXXXXX") || errfile=/dev/null
|
|
2806
|
+
status=0
|
|
2807
|
+
bash "$script_dir/remote-run.sh" start "$branch" --prompt-file "$prompt" --repo "$root" 2>"$errfile" || status=$?
|
|
2808
|
+
last=""
|
|
2809
|
+
if [ "$errfile" != /dev/null ]; then
|
|
2810
|
+
cat "$errfile" >&2
|
|
2811
|
+
last=$(grep -v '^[[:space:]]*$' "$errfile" | tail -n 1)
|
|
2812
|
+
rm -f "$errfile"
|
|
2813
|
+
fi
|
|
2814
|
+
case "$status" in
|
|
2815
|
+
0) ;;
|
|
2816
|
+
3)
|
|
2817
|
+
echo "remote-run.sh: trigger: $branch is pushed, but its dispatch failed" >&2
|
|
2818
|
+
trigger_finish "$EXIT_GH" "The branch \`$branch\` was pushed with $task_what, but dispatching its run failed:
|
|
2819
|
+
|
|
2820
|
+
\`\`\`
|
|
2821
|
+
$last
|
|
2822
|
+
\`\`\`
|
|
2823
|
+
|
|
2824
|
+
Start it by hand: **Actions → \`$WORKFLOW_RUN_FILE\` → Run workflow**, with *Use workflow from* set to \`$branch\`, \`action\` \`run\` and \`branch\` \`$branch\`." ;;
|
|
2825
|
+
*)
|
|
2826
|
+
echo "remote-run.sh: trigger: the start of $branch failed (exit $status)" >&2
|
|
2827
|
+
trigger_finish "$EXIT_PLACEMENT" "No run started: placing $task_what on the branch \`$branch\` failed:
|
|
2828
|
+
|
|
2829
|
+
\`\`\`
|
|
2830
|
+
$last
|
|
2831
|
+
\`\`\`
|
|
2832
|
+
|
|
2833
|
+
$retry_again" ;;
|
|
2834
|
+
esac
|
|
2835
|
+
|
|
2836
|
+
url=$(trigger_run_url)
|
|
2837
|
+
if [ "$trigger_source" = issue ]; then
|
|
2838
|
+
echo "remote-run.sh: trigger: started $branch from issue #$issue_number: $url"
|
|
2839
|
+
trigger_finish "$EXIT_OK" "Started a harness run on the branch \`$branch\`: $url
|
|
2840
|
+
|
|
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."
|
|
2842
|
+
fi
|
|
2843
|
+
echo "remote-run.sh: trigger: started $branch from a repository_dispatch: $url"
|
|
2844
|
+
trigger_finish "$EXIT_OK" "Started a harness run on the branch \`$branch\`: $url
|
|
2845
|
+
|
|
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."
|
|
2847
|
+
}
|
|
2848
|
+
|
|
1781
2849
|
case "$verb" in
|
|
1782
2850
|
dispatch) verb_dispatch ;;
|
|
1783
2851
|
pause) verb_pause ;;
|
|
@@ -1791,5 +2859,10 @@ case "$verb" in
|
|
|
1791
2859
|
poll) verb_poll ;;
|
|
1792
2860
|
pause-requested) verb_pause_requested ;;
|
|
1793
2861
|
run-created-at) verb_run_created_at ;;
|
|
2862
|
+
start) verb_start ;;
|
|
2863
|
+
fetch) verb_fetch ;;
|
|
2864
|
+
review) verb_review ;;
|
|
2865
|
+
trigger) verb_trigger ;;
|
|
2866
|
+
list) verb_list ;;
|
|
1794
2867
|
esac
|
|
1795
2868
|
exit "$EXIT_OK"
|