@unotest/protocol 0.32.0 → 0.35.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/CHANGELOG.md +336 -0
- package/dist/index.d.ts +483 -50
- package/dist/index.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,341 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.35.0] - 2026-09-15
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 79c99ab: A box can now say "I cannot judge this token yet" instead of "your token is
|
|
8
|
+
wrong".
|
|
9
|
+
|
|
10
|
+
A box rented from the fleet accepts tokens the cloud signed for it, and it
|
|
11
|
+
checks every one against the operator's revocation list. Until that list has
|
|
12
|
+
reached the box — the minutes between it coming up and its first update tick
|
|
13
|
+
— there is nothing to check against, so such a token is refused. That is not
|
|
14
|
+
the same refusal as a bad credential: the answer is to send the SAME token
|
|
15
|
+
again shortly, never to mint another.
|
|
16
|
+
|
|
17
|
+
`BoxReadErrorCode` gains `revocation-list-unavailable` for it, and a refusal
|
|
18
|
+
body may carry `retryAfterSeconds` (the box sends `Retry-After` with the same
|
|
19
|
+
number). The push and run answers gain the same code, plus `ambiguous-project`
|
|
20
|
+
for a cloud-minted push token that names no project on a box that serves
|
|
21
|
+
several. Nothing changes for the tokens a person or a CI job mints on the box
|
|
22
|
+
itself.
|
|
23
|
+
|
|
24
|
+
- dee96a1: A box can now say "this token has expired" instead of "your token is wrong".
|
|
25
|
+
|
|
26
|
+
Tokens the cloud signs for a rented box live thirty days. Until now a box
|
|
27
|
+
refused an expired one with the same blanket `unauthorized` it gives a
|
|
28
|
+
forgery, so the CLI could not tell "ask the cloud for another pair and
|
|
29
|
+
retry" from "this credential will never work", and an expired pair looked
|
|
30
|
+
revoked.
|
|
31
|
+
|
|
32
|
+
The read, push, run and values answers gain the code `expired` for it. A box
|
|
33
|
+
sends it only for a token its operator key really signed, of the right kind,
|
|
34
|
+
for that box — the signature, the kind and the box are all checked before
|
|
35
|
+
the expiry is — so nothing a guesser sends can produce it. It is also
|
|
36
|
+
decided before the revocation list is consulted: a box that has not received
|
|
37
|
+
a list yet still answers `expired` rather than asking for the same dead
|
|
38
|
+
token again.
|
|
39
|
+
|
|
40
|
+
Two codes that a box could already send are now accepted when parsing a run
|
|
41
|
+
or values answer, which had been left out of those two readers:
|
|
42
|
+
`revocation-list-unavailable` and `ambiguous-project`.
|
|
43
|
+
|
|
44
|
+
## [0.34.0] - 2026-09-07
|
|
45
|
+
|
|
46
|
+
### Patch Changes
|
|
47
|
+
|
|
48
|
+
- 32a495e: Failure artifacts survive a run that is isolated from its own artifact root.
|
|
49
|
+
|
|
50
|
+
A run executed inside a container sees only what is mounted into it. The
|
|
51
|
+
failure bundle's directory is derived from the artifact root, which on such
|
|
52
|
+
a run is not one of those mounts — so it could not be created, and a failed
|
|
53
|
+
test produced no screenshot, no trace and no page HTML at all. Worse, that
|
|
54
|
+
one error took the always-on copies in the run directory down with it: both
|
|
55
|
+
sinks shared a single try, so the artifact that does not depend on the
|
|
56
|
+
bundle was lost too.
|
|
57
|
+
|
|
58
|
+
Two changes. `UNOTEST_FAILURES_ROOT` now says where bundles live, for
|
|
59
|
+
whoever spawns the run to point at a directory it can actually write
|
|
60
|
+
(unset, everything behaves exactly as before). And the two sinks are
|
|
61
|
+
independent: an unusable bundle root no longer costs you the run
|
|
62
|
+
directory's screenshot and page HTML.
|
|
63
|
+
|
|
64
|
+
The viewer serves failure assets from either layout, so a bundle written
|
|
65
|
+
inside the environment's runs tree renders like a local one.
|
|
66
|
+
|
|
67
|
+
- c6b91bb: From `init` to a paid box without copying a token: `login`, `box create`,
|
|
68
|
+
and the tokens that keep themselves fresh.
|
|
69
|
+
|
|
70
|
+
Three new commands sign this machine in to unotest cloud — the account
|
|
71
|
+
that rents boxes — and rent one:
|
|
72
|
+
|
|
73
|
+
- `login` runs the device flow (RFC 8628): it prints a link and a code,
|
|
74
|
+
opens the browser when there is one, and waits for the approval. The
|
|
75
|
+
key is kept in `~/.config/unotest/credentials.json` (mode 0600,
|
|
76
|
+
`XDG_CONFIG_HOME` honoured) and is never printed, not even with
|
|
77
|
+
`--json`. `UNOTEST_CLOUD_TOKEN` in the environment wins over the file,
|
|
78
|
+
which is how a CI job signs in. `logout` revokes the key on the server
|
|
79
|
+
when the cloud can be reached and removes it locally always; `whoami`
|
|
80
|
+
says who, where the key came from, and the balance (exit 77 when not
|
|
81
|
+
signed in).
|
|
82
|
+
- `box create [name] [--size s|m|l] [--topup <eur>]` creates a box named
|
|
83
|
+
after the project (its package name, as a slug), with the environments
|
|
84
|
+
the project runs locally (`unotest/.env` and every `unotest/.env.<name>`,
|
|
85
|
+
their `APP_BASE_URL` or the config's `baseUrl` as the target). When the
|
|
86
|
+
balance will not cover the first hour it prints a Stripe Checkout link,
|
|
87
|
+
opens it when it can, and waits for the payment. Then it waits for the
|
|
88
|
+
box, mints its cloud-signed push and read tokens, writes
|
|
89
|
+
`UNOTEST_BOX_URL` to `unotest/.env` and `UNOTEST_BOX_TOKEN` +
|
|
90
|
+
`UNOTEST_BOX_READ_TOKEN` to `unotest/.secrets` (through the same
|
|
91
|
+
structure-preserving writer the viewer's Variables panel uses, with
|
|
92
|
+
`.gitignore` guarded the way `init` guards it), pushes the suite —
|
|
93
|
+
retrying with backoff while a fresh box has no revocation list yet —
|
|
94
|
+
and prints the viewer's address. Exit codes for an agent driving it
|
|
95
|
+
without a terminal: 75 the payment was not confirmed in time (nothing
|
|
96
|
+
created, nothing charged), 76 the terms of service are not accepted
|
|
97
|
+
(a person must, in a browser), 77 not signed in. No prompt is ever
|
|
98
|
+
shown without a TTY.
|
|
99
|
+
`--picker` rents the box with the Picker; `UNOTEST_GROUNDER_MODE=remote`
|
|
100
|
+
and `UNOTEST_GROUNDER_REMOTE_URL` then go to `unotest/.env` and
|
|
101
|
+
`UNOTEST_GROUNDER_REMOTE_TOKEN` to `unotest/.secrets` — the names the MCP
|
|
102
|
+
server reads — so intents ground on the box with no further setup. A
|
|
103
|
+
box without the Picker leaves those keys alone. The last line of
|
|
104
|
+
`create` is the next step: the complete `bundle push --run --env …
|
|
105
|
+
--collection …` when the project has exactly one environment and one
|
|
106
|
+
collection, the viewer's address plus the command with placeholders
|
|
107
|
+
otherwise.
|
|
108
|
+
- `box access [name]` re-mints the pair and rewrites the two files;
|
|
109
|
+
`box status` and `box destroy` (asks on a terminal, `--yes` elsewhere)
|
|
110
|
+
are thin wrappers. The name defaults to the box `UNOTEST_BOX_URL`
|
|
111
|
+
points at (`acme.box.unotest.com` → `acme`).
|
|
112
|
+
|
|
113
|
+
Cloud-signed box tokens live thirty days. When a box answers that one
|
|
114
|
+
has expired, `bundle push` and the `box …` read commands re-mint the
|
|
115
|
+
pair through the cloud and retry once, provided a cloud login is at
|
|
116
|
+
hand; without one they say to run `login && box access`. The box a
|
|
117
|
+
re-mint is for is the one the project points at, named from its address
|
|
118
|
+
or, when the address spells no name (a stand behind an IP), by the same
|
|
119
|
+
default `create` used — so a lab box re-mints like any other. A `--box`
|
|
120
|
+
naming somewhere else is said to be somewhere else rather than blamed on
|
|
121
|
+
a missing login. A box that has
|
|
122
|
+
not yet received its revocation list is told apart from a real refusal
|
|
123
|
+
and retried; a token the box calls revoked is never re-minted quietly.
|
|
124
|
+
`bundle push` now also reads the push token from `unotest/.secrets`,
|
|
125
|
+
where `box create` puts it (then `unotest/.env`, for suites set up by
|
|
126
|
+
hand), so nothing has to be exported after the setup.
|
|
127
|
+
|
|
128
|
+
The MCP server knows the state too. While the project points at no box,
|
|
129
|
+
`box_envs`, `box_runs`, `box_run` and the other box tools answer
|
|
130
|
+
`{state: "no-box", next, why}` instead of an error, and `run_test`
|
|
131
|
+
carries the same `boxSuggestion` exactly once per server — after the
|
|
132
|
+
first local run that passes — so an agent offers the box at the moment
|
|
133
|
+
it is worth something and never nags. The box tools re-mint an expired
|
|
134
|
+
cloud-signed read token the way the CLI does, through the composition
|
|
135
|
+
root, and `unotest/.secrets` is read on every call: a pair written by
|
|
136
|
+
`box create` while the server runs is used by the next call. On a
|
|
137
|
+
Picker box, `ground_element` (and intent locators) whose picker token
|
|
138
|
+
the grounder refuses re-mint it once through the cloud and hand the new
|
|
139
|
+
token to the running client; a refusal that cannot be mended answers
|
|
140
|
+
`{state: "picker-token-refused", next: "npx @unotest/web box access",
|
|
141
|
+
why}`, and a grounder that has not read its revocation list yet answers
|
|
142
|
+
`{state: "retry-shortly", retryAfterSeconds}`. Box tokens are checked
|
|
143
|
+
before the network: cloud-signed ones by their whole shape (`unos_…`), a
|
|
144
|
+
push token in the read variable (and the reverse) is named as the wrong
|
|
145
|
+
kind, anything else as not a token.
|
|
146
|
+
|
|
147
|
+
A renewal the cloud itself refuses is its own answer, not the cloud's.
|
|
148
|
+
When a box or the grounder says a cloud-signed token is stale and the
|
|
149
|
+
`/access` call that would replace it is refused in turn — no such box,
|
|
150
|
+
the box not ready, too many live pairs, the cloud unreachable — every
|
|
151
|
+
surface says that the credentials could not be renewed and keeps the
|
|
152
|
+
cloud's refusal as the cause. The CLI prints one line and exits with the
|
|
153
|
+
code that refusal earns; the grounding tools answer
|
|
154
|
+
`{state: "picker-token-refused", next, why}` as before, so an intent is
|
|
155
|
+
never answered with a sentence about boxes.
|
|
156
|
+
|
|
157
|
+
`init` ends with one line saying how to run the suite on a box; the
|
|
158
|
+
"mint a read token in the guard" advice in the box commands' refusals
|
|
159
|
+
now points at `login` and `box access` instead. `UNOTEST_CLOUD_URL`
|
|
160
|
+
points the CLI at a stand or a fake cloud; nobody sets it otherwise.
|
|
161
|
+
|
|
162
|
+
`@unotest/protocol`: `replaceEnvVar` (an in-place value replacement the
|
|
163
|
+
new upsert builds on), `BOX_PUSH_TOKEN_PREFIX` / `BOX_SIGNED_TOKEN_PREFIX`
|
|
164
|
+
/ `BOX_SIGNED_TOKEN`, and `UNOTEST_BOX_URL`, `UNOTEST_GROUNDER_MODE`,
|
|
165
|
+
`UNOTEST_GROUNDER_REMOTE_URL` listed among the runner-config keys so they
|
|
166
|
+
file under the Runner section of `unotest/.env`.
|
|
167
|
+
|
|
168
|
+
`@unotest/grounder-client`: `GrounderHttpClientOptions.token` may be a
|
|
169
|
+
function yielding the current bearer; `GrounderRemoteError` carries the
|
|
170
|
+
grounder's `retryAfterSeconds`, and the error codes gain
|
|
171
|
+
`revocation-list-unavailable`.
|
|
172
|
+
|
|
173
|
+
## [0.33.0] - 2026-09-06
|
|
174
|
+
|
|
175
|
+
### Minor Changes
|
|
176
|
+
|
|
177
|
+
- 2abfd4d: Box: every run is announced — Slack, Telegram, or a webhook of your own — and the channels are managed from the guard.
|
|
178
|
+
|
|
179
|
+
A box now tells people how its runs went, whoever started them: the
|
|
180
|
+
schedule's ticks, a pull request's check, somebody pressing Run in the
|
|
181
|
+
viewer, a terminal inside the container. The daemon reads each run's own
|
|
182
|
+
journal, so the three producers are one source; the run's manifest says
|
|
183
|
+
what set it off — `trigger: { kind: "schedule" | "ci" | "manual" | "cli",
|
|
184
|
+
by? }`, written by the runner from `UNOTEST_RUN_TRIGGER` /
|
|
185
|
+
`UNOTEST_RUN_ACTOR` (a box sets them for its scheduled and CI runs, the
|
|
186
|
+
viewer for a run ordered from its UI; a terminal sets nothing and reads as
|
|
187
|
+
`cli`).
|
|
188
|
+
|
|
189
|
+
A scheduled series is announced on a **change of state**: `failed` after
|
|
190
|
+
`afterFailures` red ticks in a row, `recovered`, `still-failing` at a rule's
|
|
191
|
+
`repeatEvery`, `not-run` when the suite could not run at all (no bundle, a
|
|
192
|
+
bundle that does not install) — daily by default. A run somebody ordered is
|
|
193
|
+
announced on its own, `failed` or `passed`, every time. A withdrawn or
|
|
194
|
+
aborted run says nothing; a run whose journal stops and whose heartbeat goes
|
|
195
|
+
cold is `interrupted`.
|
|
196
|
+
|
|
197
|
+
Channels, rules, mutes and quiet hours live on the box and are managed from
|
|
198
|
+
the guard's `/_guard/notifications` page (or `boxd notify …`): a channel's
|
|
199
|
+
secret is set once and never shown again, a rule picks environments,
|
|
200
|
+
triggers, events and channels, a series or the whole project can be muted
|
|
201
|
+
until a time, and quiet hours hold reminders and `passed` back until the
|
|
202
|
+
morning. A generic webhook receives the event as JSON (`version: 1`, the
|
|
203
|
+
box named, the `trigger`) with `X-Unotest-Event` and, when the channel has
|
|
204
|
+
a secret, `X-Unotest-Signature: sha256=<hex>` — HMAC-SHA256 over the raw
|
|
205
|
+
body, the scheme GitHub uses — plus any headers the channel declares.
|
|
206
|
+
|
|
207
|
+
`@unotest/protocol`: `RunManifest.trigger` and the trigger helpers
|
|
208
|
+
(`runTriggerFromEnv`, `runTriggerEnv`, `manifestTrigger`); the session-door
|
|
209
|
+
paths, views and parsers of the notifications API (`boxNotifyProjectPath`,
|
|
210
|
+
`boxNotifyChannelPath`, `boxNotifyRulePath`, `boxNotifySeriesPath`,
|
|
211
|
+
`boxNotifyMutePath`, `boxNotifyQuietHoursPath`, `boxNotifyPreviewPath`,
|
|
212
|
+
`BoxNotifyProjectView`, …). The first draft of this feature (channels
|
|
213
|
+
declared in the box config, secrets by field) never shipped; this replaces
|
|
214
|
+
it whole.
|
|
215
|
+
|
|
216
|
+
- 5d1bde3: Run isolation, stage 1: a box no longer executes a test bundle inside its
|
|
217
|
+
own daemon.
|
|
218
|
+
|
|
219
|
+
box-runner (new, private): the run sidecar — the only service on a box
|
|
220
|
+
holding the docker socket, and the only one that starts a container. Three
|
|
221
|
+
authenticated routes on the `box` network (`POST /runs`, `GET
|
|
222
|
+
/runs/:id/stream` NDJSON, `DELETE /runs/:id`), a shared secret compared in
|
|
223
|
+
constant time, and an API that cannot be told anything about how a
|
|
224
|
+
container is built: no path, image, network, user, mount or flag. A request
|
|
225
|
+
names a project, an environment, a bundle, a scenario and the environment's
|
|
226
|
+
values; the bind sources are derived from the first three, resolved with
|
|
227
|
+
`realpath` and re-checked against the projects root, and must already
|
|
228
|
+
exist. The container is created over the Docker Engine API (pinned
|
|
229
|
+
`v1.43`) straight over the socket — no `docker` binary in the image and no
|
|
230
|
+
client dependency, and a container that is a JSON document has no place
|
|
231
|
+
for a value to become a flag. A daemon that refuses or is not there comes
|
|
232
|
+
back as `502 {reason}` before the run is accepted, or as an `error` event
|
|
233
|
+
on the stream after — never as a run that "exited with code null". An
|
|
234
|
+
environment variable the box does not forward is named in the sidecar's
|
|
235
|
+
log rather than dropped silently. A run gets the bundle tree read-only as its working directory, the
|
|
236
|
+
environment's `unotest/.runs.<env>` read-write, a `noexec` tmpfs `/tmp`,
|
|
237
|
+
a read-only rootfs, all capabilities dropped, `no-new-privileges`, its own
|
|
238
|
+
uid in boxd's group, memory/pids/cpu limits clamped to both the operator's ceilings and the
|
|
239
|
+
host's own size (docker refuses a container bigger than the machine
|
|
240
|
+
instead of clamping it), a 512 MB `/dev/shm`
|
|
241
|
+
(docker's default 64 MB kills Chromium mid-page; the host's IPC namespace
|
|
242
|
+
is deliberately not borrowed), and the `runs` network only. The container engine is an interface, so every rule is asserted on
|
|
243
|
+
the container that would have reached docker — including the cases where
|
|
244
|
+
the answer is no container at all.
|
|
245
|
+
|
|
246
|
+
boxd: running the SUITE is its own contract (`IScenarioRunner`), separate
|
|
247
|
+
from running a command (`ICommandRunner`, still the daemon's own `npm ci`
|
|
248
|
+
and viewers). `ContainerScenarioRunner` talks to the sidecar,
|
|
249
|
+
`ProcessScenarioRunner` keeps the old child-process shape for a box without
|
|
250
|
+
docker; `BOXD_RUNNER_KIND=container|process` chooses, and half a container
|
|
251
|
+
configuration refuses to start. Reading a bundle's schedules
|
|
252
|
+
(`unotest-web schedules --json`) executes the project's config module, so it
|
|
253
|
+
goes the same way. A run's environment is built from named parts
|
|
254
|
+
(`run-environment.ts`) instead of inheriting `process.env`, and its debug
|
|
255
|
+
tree is pointed at the run's own directory rather than the read-only
|
|
256
|
+
sources. New metrics
|
|
257
|
+
`boxd_run_container_total{outcome}` and `boxd_run_container_start_seconds`.
|
|
258
|
+
An environment's runs directory is created by the daemon with `2775`, so
|
|
259
|
+
the run's uid may write into it and the daemon's group may read it back.
|
|
260
|
+
|
|
261
|
+
box-kit: `secretsMatch` (constant-time secret comparison, moved out of
|
|
262
|
+
dist-service), `splitLines`, and `envNumber` / `envPositiveNumber` — the
|
|
263
|
+
env-reading rule the box-side services share.
|
|
264
|
+
|
|
265
|
+
`npm ci` of a bundle is a container of its own (`kind: "install"`): the
|
|
266
|
+
bundle tree is its only mount and it is writable, there is no environment
|
|
267
|
+
and no artifacts directory in reach, and a dependency's install scripts
|
|
268
|
+
RUN — a native module builds or fetches its binary as usual. That is what
|
|
269
|
+
running them inside the daemon could never allow. The container's last
|
|
270
|
+
steps, only on success, check the tree against the box's size cap, set
|
|
271
|
+
the final modes on what the install created and write the install marker
|
|
272
|
+
— each with an exit code of its own, so the daemon's log names the step
|
|
273
|
+
that failed — so a killed or oversized
|
|
274
|
+
install leaves a tree the box will not mount; a failed install takes the
|
|
275
|
+
tree with it. The daemon no longer passes `--ignore-scripts` and no
|
|
276
|
+
longer walks the tree afterwards.
|
|
277
|
+
|
|
278
|
+
The daemon runs with `umask 002` and clears `node_modules` before each
|
|
279
|
+
install, so a tree it created is one the install container (another uid
|
|
280
|
+
in its group) can actually write.
|
|
281
|
+
|
|
282
|
+
An install that fails KEEPS the tree, unmarked: nothing mounts it and the
|
|
283
|
+
next attempt reinstalls in place (`npm ci` wipes `node_modules` itself).
|
|
284
|
+
And a bundle directory holding a manifest with neither an archive nor a
|
|
285
|
+
tree behind it no longer counts as "this box has it" — a push of the same
|
|
286
|
+
content brings it back instead of being answered `already had this exact
|
|
287
|
+
bundle`.
|
|
288
|
+
|
|
289
|
+
web: `bundle push` and the docs say that a dependency's install scripts
|
|
290
|
+
run on a box, in isolation — the earlier advice to vendor them is gone.
|
|
291
|
+
|
|
292
|
+
A run ordered in the viewer now goes through the daemon's queue instead
|
|
293
|
+
of being spawned by the viewer.
|
|
294
|
+
|
|
295
|
+
`POST/GET/DELETE /api/box/envs/<project>/<env>/runs[/<runId>]` (paths and
|
|
296
|
+
wire types in `@unotest/protocol`) takes a `manual` ticket like every
|
|
297
|
+
other producer. Two callers may use it, and neither is taken on its word:
|
|
298
|
+
the guard, proving it is the guard with a shared secret from `box-init`
|
|
299
|
+
(the actor header is read only next to it — a viewer container could
|
|
300
|
+
otherwise claim to be any admin), and a viewer, with a per-environment
|
|
301
|
+
token the daemon minted when it started that viewer plus a short-lived
|
|
302
|
+
single-use ticket the guard signed over WHO clicked. The guard gates the
|
|
303
|
+
route under `admin` and requires an `Origin` on a mutation; the daemon
|
|
304
|
+
checks the token's environment, the ticket's environment and its `jti`
|
|
305
|
+
against the path.
|
|
306
|
+
|
|
307
|
+
`@unotest/viewer` gets `BoxdRunner`: on a box the Run button orders and
|
|
308
|
+
the Stop button asks the daemon, and the progress still comes from the
|
|
309
|
+
run's journal on disk. A local viewer spawns as it always did — the
|
|
310
|
+
composition root picks by whether a box handed it credentials.
|
|
311
|
+
|
|
312
|
+
### Patch Changes
|
|
313
|
+
|
|
314
|
+
- b3e1220: Box notifications: a webhook channel's header values are credentials.
|
|
315
|
+
|
|
316
|
+
The notifications API and the guard's page now see a webhook's headers
|
|
317
|
+
as names with `{ set: true }` — the values (the receiver's
|
|
318
|
+
`Authorization`, most of the time) are never read back, like the signing
|
|
319
|
+
secret. The edit form lists the names; typing a value after one replaces
|
|
320
|
+
the set, names alone keep it. `/api/box/notify` is the guard's own route:
|
|
321
|
+
it is no longer reachable through the session proxy for any role — a
|
|
322
|
+
readonly session could previously list every project's channels with
|
|
323
|
+
their headers.
|
|
324
|
+
|
|
325
|
+
- b3e1220: Box notifications: the webhook signature now covers a timestamp.
|
|
326
|
+
|
|
327
|
+
Every webhook delivery carries `X-Unotest-Timestamp` (Unix milliseconds,
|
|
328
|
+
when it was sent), and `X-Unotest-Signature` is HMAC-SHA256 over
|
|
329
|
+
`<timestamp>.<raw body>` rather than the body alone — GitHub's scheme
|
|
330
|
+
with a timestamp in front, so a delivery captured on the wire cannot be
|
|
331
|
+
replayed to the receiver once its window (five minutes, documented) has
|
|
332
|
+
passed. The GitHub webhook the box receives is unchanged: that is
|
|
333
|
+
GitHub's contract.
|
|
334
|
+
|
|
335
|
+
The `by` of a CI-triggered run (`push a1b2c3d (branch)`) is cut at 200
|
|
336
|
+
characters — a branch name is the pusher's input, and it must not balloon
|
|
337
|
+
the run's manifest and every notification the run produces.
|
|
338
|
+
|
|
3
339
|
## [0.32.0] - 2026-09-05
|
|
4
340
|
|
|
5
341
|
### Minor Changes
|