autonomous-sdlc-harness 0.4.1 → 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 +23 -5
- package/templates/github/workflows/harness-run.yml +68 -30
- package/templates/github/workflows/harness-trigger.yml +143 -0
- package/templates/repo/gitignore +5 -0
- package/templates/scripts/autonomous-watcher.sh +95 -248
- package/templates/scripts/create-worktree.sh +52 -10
- package/templates/scripts/lib/harness-run-lib.sh +553 -26
- package/templates/scripts/remote-run.sh +1198 -114
|
@@ -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
|
|
@@ -71,11 +219,15 @@
|
|
|
71
219
|
# past a run with none, and stopping at one whose artifact has expired, since
|
|
72
220
|
# an older copy would be staler state. An expired one restores nothing: under
|
|
73
221
|
# --resume answer it exits 2; otherwise it prints a `::warning::` line naming
|
|
74
|
-
# the run, the expiry and the lost counts
|
|
75
|
-
# job continues from the committed ledger. An unexpired one it downloads to `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/`
|
|
222
|
+
# the run, the expiry and the lost counts, clarification history and
|
|
223
|
+
# uncommitted planning drafts, and the job continues from the committed ledger. An unexpired one it downloads to `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/`
|
|
76
224
|
# (skipped when that directory already holds its status.json); and restores it
|
|
77
225
|
# in `job` mode — on every --resume kind, `none` included, because a reused
|
|
78
|
-
# branch keeps its clarification history.
|
|
226
|
+
# branch keeps its clarification history. A job-mode restore also places the
|
|
227
|
+
# bundle's `planning/` drafts at their paths under the state directory, never
|
|
228
|
+
# over a file the checkout already has, and when it placed or kept any prints
|
|
229
|
+
# `placed <n> planning file(s) for <branch>; kept <n> the checkout already
|
|
230
|
+
# carries`. Then, under --resume answer, it
|
|
79
231
|
# writes each `"<n>": "<text>"` entry to `clarifications/<branch>/answer_<n>.md`
|
|
80
232
|
# with the exact bytes, after checking every entry first; and with
|
|
81
233
|
# `HARNESS_INPUT_PARK_LOOP_CLEAR` exactly `true` it sets `park_loop_cycles` to
|
|
@@ -200,11 +352,28 @@
|
|
|
200
352
|
# `gh run view <run_id> --json createdAt` as an epoch second.
|
|
201
353
|
#
|
|
202
354
|
# `status` AND `sync` READ THE RECORD, NOT THE KEY. A run keeps the execution
|
|
203
|
-
# it started with, so they test
|
|
204
|
-
# `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.
|
|
205
367
|
#
|
|
206
|
-
# `
|
|
207
|
-
#
|
|
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
|
|
208
377
|
# `harness run <branch>` or `harness pause <branch>` (bounded), the record's
|
|
209
378
|
# `status`, `pause_reason`, `remote_run_url` and `remote_synced_at`, whether
|
|
210
379
|
# a `harness run` finished after the last sync, and — reading the newest
|
|
@@ -230,7 +399,9 @@
|
|
|
230
399
|
# `mirror` mode into the record's `worktree`, `run.log` copied to the main
|
|
231
400
|
# checkout's `autonomous_logs/<branch>.remote.log`, and `status`,
|
|
232
401
|
# `pause_reason`, `usage_resume_at`, `park_loop_cycles`, `remote_run_id`,
|
|
233
|
-
# `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
|
|
404
|
+
# `mirror` restore places no planning draft
|
|
234
405
|
# 4. no artifact, while some bundle exists (`remote_run_id` is set, or an
|
|
235
406
|
# older finished run carries one): a job that died before its upload.
|
|
236
407
|
# `paused` / `killed`, `remote_run_id` / `remote_run_url` re-pointed at
|
|
@@ -289,16 +460,30 @@
|
|
|
289
460
|
# root's `harness.config.json`.
|
|
290
461
|
#
|
|
291
462
|
# WHAT IT NEVER DOES. It never launches a local session, never writes the
|
|
292
|
-
# 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
|
|
293
475
|
# registry record (`stop`, `sync`) and, for `sync`, the download directory
|
|
294
476
|
# `<state_dir>/autonomous_logs/remote_download/<branch>/<id>/` and
|
|
295
477
|
# `<branch>.remote.log` in the main checkout, plus the mirror restore
|
|
296
478
|
# `hr_remote_bundle_restore` performs in the record's `worktree`; for
|
|
297
|
-
# `restore`, that download directory, the job restore
|
|
479
|
+
# `restore`, that download directory, the job restore (the planning drafts
|
|
480
|
+
# among it), `answer_<n>.md` and the
|
|
298
481
|
# `park_loop_cycles` rewrite of `remote_status.json`, all in the job's
|
|
299
482
|
# checkout; for `save`, <out_dir> and the step summary; for `poll`, its
|
|
300
483
|
# download directories and `<state_dir>/autonomous_logs/poll_state/previous/`
|
|
301
|
-
# 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.
|
|
302
487
|
#
|
|
303
488
|
# MIRRORS OF `cli/src/remote/githubActions.ts`, which owns these names; a
|
|
304
489
|
# rename there is an edit here, byte for byte:
|
|
@@ -307,6 +492,10 @@
|
|
|
307
492
|
# STATE_ARTIFACT_NAME mirrors STATE_ARTIFACT_NAME
|
|
308
493
|
# POLL_STATE_ARTIFACT_NAME mirrors POLL_STATE_ARTIFACT_NAME
|
|
309
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')
|
|
310
499
|
#
|
|
311
500
|
# `set -u` WITHOUT `-e`: every refusal is reported with its own exit code rather
|
|
312
501
|
# than aborting mid-decision.
|
|
@@ -339,6 +528,37 @@
|
|
|
339
528
|
# bash scripts/remote-run.sh pause feat_x -> 3, names `boom`
|
|
340
529
|
# gh absent HARNESS_GH_CLI=/nonexistent bash scripts/remote-run.sh warm -> 3
|
|
341
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
|
+
#
|
|
342
562
|
# status and sync need a remote record, and a `run list` answer whose runs
|
|
343
563
|
# carry `displayTitle` `harness run feat_x` and a `url`:
|
|
344
564
|
# r=sdlc-harness/autonomous_logs/registry.json; mkdir -p "${r%/*}"
|
|
@@ -363,7 +583,8 @@
|
|
|
363
583
|
# flow_walker_state) and GITHUB_RUN_ID set to another id:
|
|
364
584
|
# restore bash scripts/remote-run.sh restore feat_x --resume none -> 0; the
|
|
365
585
|
# checkout carries clarifications/feat_x/question_1.md,
|
|
366
|
-
# .flow_walker_state and autonomous_logs/remote_status.json
|
|
586
|
+
# .flow_walker_state and autonomous_logs/remote_status.json, and
|
|
587
|
+
# places the bundle's planning/ drafts where the checkout has none
|
|
367
588
|
# answer HARNESS_INPUT_ANSWERS='{"1":"Use B.\n"}' ... --resume answer -> 0;
|
|
368
589
|
# clarifications/feat_x/answer_1.md holds exactly `Use B.` + newline
|
|
369
590
|
# no question HARNESS_INPUT_ANSWERS='{"2":"x"}' ... --resume answer
|
|
@@ -381,7 +602,7 @@
|
|
|
381
602
|
# -> prints the expired line, "$r" byte-identical
|
|
382
603
|
# save bash scripts/remote-run.sh save feat_x /tmp/b -> 0; /tmp/b holds
|
|
383
604
|
# status.json, clarifications/feat_x/, flow_walker_state (and
|
|
384
|
-
# PAUSE_PROGRESS.md, run.log when present); with
|
|
605
|
+
# PAUSE_PROGRESS.md, run.log, planning/ when present); with
|
|
385
606
|
# GITHUB_STEP_SUMMARY=/tmp/s, /tmp/s gains the status table
|
|
386
607
|
# never started no remote_status.json and no registry: save -> 0, /tmp/b
|
|
387
608
|
# empty
|
|
@@ -425,6 +646,26 @@
|
|
|
425
646
|
# run-created-at a stub answering `run view 42 --json createdAt` with
|
|
426
647
|
# {"createdAt":"2026-01-01T00:00:10Z"}: bash scripts/remote-run.sh
|
|
427
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`
|
|
428
669
|
|
|
429
670
|
set -u
|
|
430
671
|
|
|
@@ -441,6 +682,8 @@ WORKFLOW_RUN_FILE='harness-run.yml'
|
|
|
441
682
|
WORKFLOW_RESUME_FILE='harness-resume.yml'
|
|
442
683
|
STATE_ARTIFACT_NAME='harness-state'
|
|
443
684
|
POLL_STATE_ARTIFACT_NAME='harness-poll-state'
|
|
685
|
+
DEFAULT_TRIGGER_LABEL='harness'
|
|
686
|
+
TRIGGER_DISPATCH_EVENT_TYPE='harness-task'
|
|
444
687
|
GH="${HARNESS_GH_CLI:-gh}"
|
|
445
688
|
|
|
446
689
|
# How many runs `status` prints, and how many `run list` returns for status
|
|
@@ -461,6 +704,10 @@ POLL_GIVE_UP_AFTER_MINUTES_DEFAULT=360
|
|
|
461
704
|
# How many of the poller's own runs `poll` searches for the previous tick's
|
|
462
705
|
# state artifact; each one without it costs an artifact lookup.
|
|
463
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
|
|
464
711
|
|
|
465
712
|
# GitHub's documented limit on a `workflow_dispatch` inputs payload: "The
|
|
466
713
|
# maximum payload for inputs is 65,535 characters."
|
|
@@ -471,6 +718,7 @@ EXIT_OK=0
|
|
|
471
718
|
EXIT_USAGE=1
|
|
472
719
|
EXIT_REFUSED=2
|
|
473
720
|
EXIT_GH=3
|
|
721
|
+
EXIT_PLACEMENT=4
|
|
474
722
|
# pause-requested only: the read succeeded and found no pause.
|
|
475
723
|
EXIT_NO_PAUSE=1
|
|
476
724
|
|
|
@@ -482,12 +730,17 @@ usage() {
|
|
|
482
730
|
echo " remote-run.sh stop <branch> [--repo <root>]" >&2
|
|
483
731
|
echo " remote-run.sh status <branch> [--repo <root>]" >&2
|
|
484
732
|
echo " remote-run.sh sync <branch> [--repo <root>]" >&2
|
|
733
|
+
echo " remote-run.sh fetch <branch> <out_dir> [--repo <root>]" >&2
|
|
485
734
|
echo " remote-run.sh restore <branch> --resume none|answer|pause [--repo <root>]" >&2
|
|
486
735
|
echo " remote-run.sh save <branch> <out_dir> [--repo <root>]" >&2
|
|
487
736
|
echo " remote-run.sh continue <branch> <bundle_dir> [--repo <root>]" >&2
|
|
488
737
|
echo " remote-run.sh poll [--repo <root>]" >&2
|
|
489
738
|
echo " remote-run.sh pause-requested <branch> <since_epoch> [--repo <root>]" >&2
|
|
490
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
|
|
491
744
|
[ "${verb-}" != save ] || exit "$EXIT_OK"
|
|
492
745
|
exit "$EXIT_USAGE"
|
|
493
746
|
}
|
|
@@ -538,7 +791,7 @@ verb=""
|
|
|
538
791
|
verb="$1"
|
|
539
792
|
shift
|
|
540
793
|
case "$verb" in
|
|
541
|
-
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) ;;
|
|
542
795
|
*) usage "unknown verb '$verb'" ;;
|
|
543
796
|
esac
|
|
544
797
|
|
|
@@ -556,6 +809,8 @@ chain="0"
|
|
|
556
809
|
repo_arg=""
|
|
557
810
|
since_arg=""
|
|
558
811
|
run_id_arg=""
|
|
812
|
+
prompt_file=""
|
|
813
|
+
review_file=""
|
|
559
814
|
|
|
560
815
|
while [ "$#" -gt 0 ]; do
|
|
561
816
|
case "$1" in
|
|
@@ -579,10 +834,19 @@ while [ "$#" -gt 0 ]; do
|
|
|
579
834
|
--park-loop-clear)
|
|
580
835
|
[ "$verb" = dispatch ] || usage "$1 is a dispatch option"
|
|
581
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 ;;
|
|
582
845
|
-*)
|
|
583
846
|
usage "unknown option '$1'" ;;
|
|
584
847
|
*)
|
|
585
|
-
[ "$verb" != warm ] && [ "$verb" != poll ]
|
|
848
|
+
[ "$verb" != warm ] && [ "$verb" != poll ] && [ "$verb" != trigger ] \
|
|
849
|
+
&& [ "$verb" != list ] || usage "$verb takes no branch"
|
|
586
850
|
if [ "$verb" = run-created-at ]; then
|
|
587
851
|
[ -z "$run_id_arg" ] || usage "unexpected argument '$1'"
|
|
588
852
|
run_id_arg="$1"
|
|
@@ -590,7 +854,7 @@ while [ "$#" -gt 0 ]; do
|
|
|
590
854
|
branch="$1"
|
|
591
855
|
elif [ "$verb" = pause-requested ] && [ -z "$since_arg" ]; then
|
|
592
856
|
since_arg="$1"
|
|
593
|
-
elif [ "$verb" = save ] && [ -z "$out_dir" ]; then
|
|
857
|
+
elif { [ "$verb" = save ] || [ "$verb" = fetch ]; } && [ -z "$out_dir" ]; then
|
|
594
858
|
out_dir="$1"
|
|
595
859
|
elif [ "$verb" = continue ] && [ -z "$bundle_dir" ]; then
|
|
596
860
|
bundle_dir="$1"
|
|
@@ -601,7 +865,8 @@ while [ "$#" -gt 0 ]; do
|
|
|
601
865
|
esac
|
|
602
866
|
done
|
|
603
867
|
|
|
604
|
-
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
|
|
605
870
|
valid_branch "$branch" || usage "$verb needs a <branch>"
|
|
606
871
|
fi
|
|
607
872
|
|
|
@@ -627,6 +892,18 @@ if [ "$verb" = save ] && [ -z "$out_dir" ]; then
|
|
|
627
892
|
usage "save needs an <out_dir>"
|
|
628
893
|
fi
|
|
629
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
|
+
|
|
630
907
|
if [ "$verb" = restore ]; then
|
|
631
908
|
[ "$resume_given" -eq 1 ] || usage "restore needs --resume"
|
|
632
909
|
case "$resume" in
|
|
@@ -676,7 +953,7 @@ setup_fail() {
|
|
|
676
953
|
if [ -n "$repo_arg" ]; then
|
|
677
954
|
root=$(hr_repo_root "$repo_arg") || setup_fail "'$repo_arg' is not a git repository"
|
|
678
955
|
elif [ "$verb" = restore ] || [ "$verb" = save ] || [ "$verb" = continue ] || [ "$verb" = poll ] \
|
|
679
|
-
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ]; then
|
|
956
|
+
|| [ "$verb" = pause-requested ] || [ "$verb" = run-created-at ] || [ "$verb" = trigger ]; then
|
|
680
957
|
root=$(hr_repo_root "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
681
958
|
else
|
|
682
959
|
root=$(hr_main_repo "${PWD-.}") || setup_fail "'${PWD-.}' is not inside a git repository"
|
|
@@ -684,6 +961,7 @@ fi
|
|
|
684
961
|
|
|
685
962
|
hr_config_load "$root" || :
|
|
686
963
|
registry=""
|
|
964
|
+
status_no_record=0
|
|
687
965
|
case "$verb" in
|
|
688
966
|
restore|save|continue|poll)
|
|
689
967
|
hr_state_path "$root" >/dev/null || setup_fail "cannot resolve '$root/harness.config.json'"
|
|
@@ -691,6 +969,9 @@ case "$verb" in
|
|
|
691
969
|
pause-requested|run-created-at)
|
|
692
970
|
# Read verbs: no gate, and nothing of the configuration is read.
|
|
693
971
|
;;
|
|
972
|
+
trigger)
|
|
973
|
+
# Gates itself, after reading the event, so a refusal can still be commented.
|
|
974
|
+
;;
|
|
694
975
|
status|sync)
|
|
695
976
|
registry=$(hr_state_path "$root" autonomous_logs/registry.json) || {
|
|
696
977
|
echo "remote-run.sh: cannot resolve '$root/harness.config.json'" >&2
|
|
@@ -698,7 +979,18 @@ case "$verb" in
|
|
|
698
979
|
}
|
|
699
980
|
# Tested with -f first: hr_registry_get creates an absent registry, and
|
|
700
981
|
# status writes nothing.
|
|
701
|
-
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
|
|
702
994
|
echo "remote-run.sh: refused, nothing written: the local record of '$branch' does not carry execution: github-actions" >&2
|
|
703
995
|
exit "$EXIT_REFUSED"
|
|
704
996
|
fi
|
|
@@ -720,6 +1012,20 @@ case "$bundle_dir" in
|
|
|
720
1012
|
''|/*) ;;
|
|
721
1013
|
*) bundle_dir="${PWD-.}/$bundle_dir" ;;
|
|
722
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
|
|
723
1029
|
|
|
724
1030
|
cd "$root" || setup_fail "cannot enter '$root'"
|
|
725
1031
|
|
|
@@ -727,6 +1033,9 @@ cd "$root" || setup_fail "cannot enter '$root'"
|
|
|
727
1033
|
# The verbs.
|
|
728
1034
|
# ---------------------------------------------------------------------------
|
|
729
1035
|
|
|
1036
|
+
# Appended to a failed dispatch's message; `start` sets it once its branch is pushed.
|
|
1037
|
+
dispatch_fail_note=""
|
|
1038
|
+
|
|
730
1039
|
verb_dispatch() {
|
|
731
1040
|
local answers="" value n file payload
|
|
732
1041
|
local -a inputs
|
|
@@ -763,8 +1072,19 @@ verb_dispatch() {
|
|
|
763
1072
|
exit "$EXIT_REFUSED"
|
|
764
1073
|
fi
|
|
765
1074
|
|
|
766
|
-
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"
|
|
767
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
|
|
768
1088
|
}
|
|
769
1089
|
|
|
770
1090
|
verb_pause() {
|
|
@@ -914,6 +1234,55 @@ set_many_or_fail() {
|
|
|
914
1234
|
}
|
|
915
1235
|
}
|
|
916
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
|
+
|
|
917
1286
|
verb_status() {
|
|
918
1287
|
local runs finished_id synced_id field
|
|
919
1288
|
list_runs
|
|
@@ -922,6 +1291,10 @@ verb_status() {
|
|
|
922
1291
|
printf '%s' "$runs" | jq -r --argjson n "$STATUS_RUNS_SHOWN" '
|
|
923
1292
|
if length == 0 then " (none)" else
|
|
924
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
|
|
925
1298
|
echo "remote-run.sh: local record (last synced):"
|
|
926
1299
|
for field in status pause_reason remote_run_url remote_synced_at; do
|
|
927
1300
|
printf ' %s: %s\n' "$field" "$(hr_registry_get "$registry" "$branch" "$field")"
|
|
@@ -950,78 +1323,171 @@ sync_expired() {
|
|
|
950
1323
|
echo "remote-run.sh: $line"
|
|
951
1324
|
}
|
|
952
1325
|
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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')
|
|
964
1355
|
if [ -z "$newest" ]; then
|
|
965
|
-
|
|
1356
|
+
RS_STATE=none
|
|
966
1357
|
return 0
|
|
967
1358
|
fi
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
if [ "$state" != completed ]; then
|
|
974
|
-
set_many_or_fail status running remote_synced_at "$now"
|
|
975
|
-
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
|
|
976
1364
|
return 0
|
|
977
1365
|
fi
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
# Case 1 — already applied. A record still waiting on this run's bundle is
|
|
981
|
-
# re-checked: once it expires, the job can no longer take an answer.
|
|
982
|
-
if [ "$id" = "$synced_id" ]; then
|
|
983
|
-
case "$(hr_registry_get "$registry" "$branch" status)/$(hr_registry_get "$registry" "$branch" pause_reason)" in
|
|
984
|
-
paused/expired) ;;
|
|
985
|
-
parked/*|park_loop/*|paused/*)
|
|
986
|
-
bundle_state "$id"
|
|
987
|
-
if [ "$BUNDLE_STATE" = expired ]; then
|
|
988
|
-
sync_expired "$id" "$url" "$now"
|
|
989
|
-
return 0
|
|
990
|
-
fi
|
|
991
|
-
;;
|
|
992
|
-
esac
|
|
993
|
-
set_or_fail remote_synced_at "$now"
|
|
994
|
-
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
|
|
995
1368
|
return 0
|
|
996
1369
|
fi
|
|
997
1370
|
|
|
998
|
-
# Case 2 —
|
|
999
|
-
bundle_state "$
|
|
1371
|
+
# Case 2 — its bundle has expired.
|
|
1372
|
+
bundle_state "$RS_RUN_ID"
|
|
1000
1373
|
if [ "$BUNDLE_STATE" = expired ]; then
|
|
1001
|
-
|
|
1374
|
+
RS_STATE=paused
|
|
1375
|
+
RS_PAUSE_REASON=expired
|
|
1376
|
+
RS_DETAIL=$(expired_line "$RS_RUN_ID")
|
|
1002
1377
|
return 0
|
|
1003
1378
|
fi
|
|
1004
1379
|
|
|
1005
|
-
# Case 3 — a
|
|
1380
|
+
# Case 3 — a bundle.
|
|
1006
1381
|
if [ "$BUNDLE_STATE" = present ]; then
|
|
1007
1382
|
hr_remote_names_var
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
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"
|
|
1012
1390
|
status_file="$download/$HR_REMOTE_STATUS_FILE"
|
|
1013
1391
|
if [ ! -f "$status_file" ]; then
|
|
1014
1392
|
mkdir -p "$download" || { echo "remote-run.sh: cannot create '$download'" >&2; exit "$EXIT_USAGE"; }
|
|
1015
|
-
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"
|
|
1016
1394
|
fi
|
|
1017
|
-
|
|
1018
|
-
|
|
1395
|
+
RS_BUNDLE=1
|
|
1396
|
+
RS_STATE=$(hr_remote_status_get "$status_file" status) || RS_STATE=""
|
|
1397
|
+
case "$RS_STATE" in
|
|
1019
1398
|
running|parked|park_loop|paused|completed|failed) ;;
|
|
1020
1399
|
*)
|
|
1021
1400
|
echo "remote-run.sh: refused, nothing written: the bundle in '$download' is unrecognised" >&2
|
|
1022
1401
|
exit "$EXIT_REFUSED"
|
|
1023
1402
|
;;
|
|
1024
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"
|
|
1025
1491
|
hr_remote_bundle_restore "$download" "$worktree" "$branch" mirror
|
|
1026
1492
|
case $? in
|
|
1027
1493
|
0) ;;
|
|
@@ -1036,44 +1502,33 @@ verb_sync() {
|
|
|
1036
1502
|
|| { echo "remote-run.sh: copying the run log to '$remote_log' failed" >&2; exit "$EXIT_USAGE"; }
|
|
1037
1503
|
fi
|
|
1038
1504
|
fi
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
status=paused
|
|
1043
|
-
reason=killed
|
|
1044
|
-
detail="the job ended mid-run (its bundle still says running): $url"
|
|
1045
|
-
fi
|
|
1505
|
+
status="$RS_STATE"
|
|
1506
|
+
reason="$RS_PAUSE_REASON"
|
|
1507
|
+
detail="$RS_DETAIL"
|
|
1046
1508
|
[ -n "$detail" ] || detail="synced from $url"
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
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
|
|
1052
1518
|
echo "remote-run.sh: synced run $id of $branch: $status${reason:+ ($reason)}"
|
|
1053
1519
|
return 0
|
|
1054
1520
|
fi
|
|
1055
1521
|
|
|
1056
1522
|
# Case 4 — a newer run with no bundle, while some bundle exists.
|
|
1057
|
-
|
|
1058
|
-
if [ -n "$synced_id" ]; then
|
|
1059
|
-
bundle_exists=1
|
|
1060
|
-
else
|
|
1061
|
-
for older in $(printf '%s' "$runs" | jq -r '.[1:][] | select(.status == "completed") | .databaseId | tostring'); do
|
|
1062
|
-
if has_bundle "$older"; then bundle_exists=1; break; fi
|
|
1063
|
-
done
|
|
1064
|
-
fi
|
|
1065
|
-
if [ "$bundle_exists" -eq 1 ]; then
|
|
1523
|
+
if [ "$RS_STATE" = paused ]; then
|
|
1066
1524
|
set_many_or_fail status paused pause_reason killed remote_run_id "$id" remote_run_url "$url" \
|
|
1067
|
-
remote_detail "
|
|
1068
|
-
remote_synced_at "$now"
|
|
1525
|
+
remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
1069
1526
|
echo "remote-run.sh: run $id of $branch left no bundle; the record is paused (killed), nothing restored"
|
|
1070
1527
|
return 0
|
|
1071
1528
|
fi
|
|
1072
1529
|
|
|
1073
1530
|
# Case 5 — no bundle in any run.
|
|
1074
|
-
set_many_or_fail status failed
|
|
1075
|
-
remote_detail "no run of $branch ever uploaded a state bundle; newest: $url" \
|
|
1076
|
-
remote_synced_at "$now"
|
|
1531
|
+
set_many_or_fail status failed remote_detail "$RS_DETAIL" remote_synced_at "$now"
|
|
1077
1532
|
echo "remote-run.sh: no run of $branch carries a state bundle; the record is failed"
|
|
1078
1533
|
}
|
|
1079
1534
|
|
|
@@ -1140,7 +1595,7 @@ verb_restore() {
|
|
|
1140
1595
|
if [ "$PREV_RUN_STATE" = expired ]; then
|
|
1141
1596
|
[ "$resume" != answer ] \
|
|
1142
1597
|
|| restore_refuse "the state bundle of run $id expired on $BUNDLE_EXPIRES_AT, so its questions can no longer be answered here: resume from the committed ledger with $RESUME_HINT $branch, or re-drop the task; nothing written"
|
|
1143
|
-
echo "::warning::remote-run.sh: the state bundle of run $id expired on $BUNDLE_EXPIRES_AT: the park-loop, auto-resume and stall counts
|
|
1598
|
+
echo "::warning::remote-run.sh: the state bundle of run $id expired on $BUNDLE_EXPIRES_AT: the park-loop, auto-resume and stall counts, the clarification history and any planning drafts not yet committed that it carried are lost; this job continues from the committed ledger"
|
|
1144
1599
|
elif [ -z "$id" ]; then
|
|
1145
1600
|
[ "$resume" != answer ] \
|
|
1146
1601
|
|| restore_refuse "--resume answer, but no finished run of $branch carries a state bundle; nothing written"
|
|
@@ -1163,7 +1618,11 @@ verb_restore() {
|
|
|
1163
1618
|
fi
|
|
1164
1619
|
hr_remote_bundle_restore "$download" "$root" "$branch" job
|
|
1165
1620
|
case $? in
|
|
1166
|
-
0)
|
|
1621
|
+
0)
|
|
1622
|
+
echo "remote-run.sh: restored the bundle of run $id into $root"
|
|
1623
|
+
[ $((HR_REMOTE_PLANNING_PLACED + HR_REMOTE_PLANNING_KEPT)) -eq 0 ] \
|
|
1624
|
+
|| echo "remote-run.sh: placed $HR_REMOTE_PLANNING_PLACED planning file(s) for $branch; kept $HR_REMOTE_PLANNING_KEPT the checkout already carries"
|
|
1625
|
+
;;
|
|
1167
1626
|
2) restore_refuse "the bundle in '$download' is unrecognised for $branch; nothing restored" ;;
|
|
1168
1627
|
*) restore_fail "restoring '$download' into '$root' failed" ;;
|
|
1169
1628
|
esac
|
|
@@ -1304,7 +1763,7 @@ ALL_RUNS_LISTED=0
|
|
|
1304
1763
|
list_all_runs() {
|
|
1305
1764
|
[ "$ALL_RUNS_LISTED" -eq 0 ] || return 0
|
|
1306
1765
|
gh_call run list --workflow "$WORKFLOW_RUN_FILE" \
|
|
1307
|
-
--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
|
|
1308
1767
|
printf '%s' "$GH_OUT" | jq -e 'type == "array"' >/dev/null 2>&1 || {
|
|
1309
1768
|
GH_ERR="its run list is not the expected JSON"
|
|
1310
1769
|
return 1
|
|
@@ -1378,13 +1837,13 @@ RESUME_HINT="/autonomous-sdlc-harness:branch-resume"
|
|
|
1378
1837
|
continue_redispatch() {
|
|
1379
1838
|
local status_file="$1" engine_value
|
|
1380
1839
|
if [ -n "${HARNESS_REMOTE_STOP-}" ]; then
|
|
1381
|
-
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" "")."
|
|
1382
1841
|
return 0
|
|
1383
1842
|
fi
|
|
1384
1843
|
remote_branch_stopped "$branch"
|
|
1385
1844
|
case $? in
|
|
1386
1845
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); not re-dispatched"; return 0 ;;
|
|
1387
|
-
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 ;;
|
|
1388
1847
|
esac
|
|
1389
1848
|
if ! max_chain_var; then
|
|
1390
1849
|
notify failed "$branch" "Not re-dispatched: HARNESS_MAX_CHAIN '$MAX_CHAIN' is not a non-negative integer."
|
|
@@ -1401,19 +1860,19 @@ continue_redispatch() {
|
|
|
1401
1860
|
return 0
|
|
1402
1861
|
fi
|
|
1403
1862
|
redispatch "$engine_value" "$NEXT_CHAIN" \
|
|
1404
|
-
|| 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")."
|
|
1405
1864
|
}
|
|
1406
1865
|
|
|
1407
1866
|
continue_wait_poller() {
|
|
1408
1867
|
remote_branch_stopped "$branch"
|
|
1409
1868
|
case $? in
|
|
1410
1869
|
0) echo "remote-run.sh: $branch is stopped ($STOPPED_LINE); the resume poller is not enabled"; return 0 ;;
|
|
1411
|
-
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 ;;
|
|
1412
1871
|
esac
|
|
1413
1872
|
if gh_call workflow enable "$WORKFLOW_RESUME_FILE"; then
|
|
1414
1873
|
echo "remote-run.sh: enabled $WORKFLOW_RESUME_FILE for $branch"
|
|
1415
1874
|
else
|
|
1416
|
-
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" "")."
|
|
1417
1876
|
fi
|
|
1418
1877
|
}
|
|
1419
1878
|
|
|
@@ -1637,7 +2096,7 @@ poll_branch() {
|
|
|
1637
2096
|
if [ "$failures" -ge "$((10#$POLL_MAX_FAILURES))" ] \
|
|
1638
2097
|
|| [ "$now" -gt "$((10#$at + 10#$POLL_GIVE_UP_MINUTES * 60))" ]; then
|
|
1639
2098
|
poll_state_put "$branch" "$id" "$failures" 1
|
|
1640
|
-
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")."
|
|
1641
2100
|
return 1
|
|
1642
2101
|
fi
|
|
1643
2102
|
poll_state_put "$branch" "$id" "$failures" ""
|
|
@@ -1694,7 +2153,7 @@ poll_recheck() {
|
|
|
1694
2153
|
return 0
|
|
1695
2154
|
fi
|
|
1696
2155
|
for b in $POLL_WAITING; do
|
|
1697
|
-
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" "")."
|
|
1698
2157
|
done
|
|
1699
2158
|
}
|
|
1700
2159
|
|
|
@@ -1767,6 +2226,626 @@ verb_run_created_at() {
|
|
|
1767
2226
|
printf '%s\n' "$epoch"
|
|
1768
2227
|
}
|
|
1769
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
|
+
|
|
1770
2849
|
case "$verb" in
|
|
1771
2850
|
dispatch) verb_dispatch ;;
|
|
1772
2851
|
pause) verb_pause ;;
|
|
@@ -1780,5 +2859,10 @@ case "$verb" in
|
|
|
1780
2859
|
poll) verb_poll ;;
|
|
1781
2860
|
pause-requested) verb_pause_requested ;;
|
|
1782
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 ;;
|
|
1783
2867
|
esac
|
|
1784
2868
|
exit "$EXIT_OK"
|