ADR 0270: the forge's ancestry decides covered, and say what covered means and does not (review of #241)
Merge order on a protected trunk is kept only as the first filter; the forge's compare, through a new read-only tool, decides whether a later merge contains a held one. A merge held without a passing check is covered only by a later delivery whose own check passed. Covered says a later merge that contains it was delivered, not that the change runs: a revert in between stays possible. A delivery that only adds a module is delivered whatever its build does, and covered wins over go when a person's walk fails between two readings. The glossary gains covered beside superseded and delivered. ADR 0271 names the bus upgrade as the mesh call and says S17 clears once the step sent the new bus.
This commit is contained in:
@@ -319,6 +319,11 @@ planner, the walk). **Downstream of** Module, Core, Health and repair and Data.
|
||||
`held`, and the final four). Not *change* (a diff). *Pipeline* is the predecessor's build-and-deploy
|
||||
mechanism, which the as-is designs describe; it is never a word for a delivery.
|
||||
*Not:* ~~deployment~~ (hq)
|
||||
- **covered** — the end of a delivery that a later merge containing it delivered: every module it moves was
|
||||
sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own
|
||||
passing check, whose merge commit the forge says contains this one. Not *superseded*, which is a delivery
|
||||
whose walk a newer delivery took over before it ended; nor *delivered*, which its own walk or an empty plan
|
||||
ends in ([ADR 0270](../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md), proposed).
|
||||
- **delivery group** — two or more deliveries sharing a pull request head branch name across the mesh's
|
||||
repositories, delivered as one unit in an order declared (`after:`) or inferred by the planner. One
|
||||
level: a group holds deliveries, never groups. Its state is derived from its members, never set.
|
||||
|
||||
+71
-36
@@ -7,7 +7,7 @@ reconstructed: false
|
||||
extends: 02-DECISIONS/0239-a-delivery-is-owned-by-the-mesh-delivery-module-and-runs-from-commit-to-delivered.md
|
||||
---
|
||||
|
||||
# 270. A held delivery whose change already runs, or has nothing to run, ends by itself, and a hold whose reason is gone lifts
|
||||
# 270. A held delivery that moves nothing, or that a later delivered merge contains, ends by itself, and a hold whose reason is gone lifts
|
||||
|
||||
## Context
|
||||
|
||||
@@ -42,7 +42,7 @@ person could answer correctly:
|
||||
|
||||
`release` would walk an old commit over a newer one, or walk nothing. `retire-history` refuses each one,
|
||||
because their heads were heard. `stop` is the only way out that works, and it is the wrong record: it sets
|
||||
`mesh/delivery` to `error` on a pull request whose change runs on every machine. Four were stopped that way on
|
||||
`mesh/delivery` to `error` on a pull request whose change a later delivered merge carried. Four were stopped that way on
|
||||
2026-10-09 before the issue was found, and their pull requests now say "stopped: covered: …" as an error. A
|
||||
condition that only a wrong act can clear teaches people to ignore the list it is in.
|
||||
|
||||
@@ -51,17 +51,21 @@ condition that only a wrong act can clear teaches people to ignore the list it i
|
||||
1. **Stop them with why.** Rejected: it is what was done, and it records a delivered change as an error on
|
||||
its pull request. Issue 313 ruled it out for the same reason.
|
||||
2. **A person's act, `supersede`, that ends a held delivery as superseded.** Rejected as the main path. Every
|
||||
fact the person would check is already in mesh-delivery's own records: the plan, the forge's merge times
|
||||
and the walks' records. A person asked to confirm a fact the mesh can read is a person asked for nothing.
|
||||
A person keeps `stop` and `release` for what the facts do not settle.
|
||||
3. **Ask the forge, or the controller, whether each running build's commit contains the merge** (git
|
||||
ancestry through a new forge tool, or through the controller's records of what each machine runs).
|
||||
Rejected for now. The forge module has no such tool, and adding one means changing a TypeScript module.
|
||||
The trunks are protected: they take merges only through pull requests, and no one can push to them
|
||||
(`gitea.gitea_branch_protection_get` on each trunk says `push: false`). So a commit the forge merged later
|
||||
into the same trunk contains every commit merged into it before. The merge times say that order, and
|
||||
mesh-delivery already holds them. Ancestry would answer the same question with more parts.
|
||||
4. **Read the facts, and end the delivery by observation** — chosen, below.
|
||||
fact the person would check can be read by the mesh: the plan, the walks' records and the forge's history.
|
||||
A person asked to confirm a fact the mesh can read is a person asked for nothing. A person keeps `stop` and
|
||||
`release` for what the facts do not settle.
|
||||
3. **Trust merge order alone.** Rejected after review. On a trunk that takes no push, a commit the forge
|
||||
merged later contains every commit merged before it, and mesh-delivery already holds the merge times. But
|
||||
that rests on the trunk's protection, which is the forge's setting and which nothing in mesh-delivery
|
||||
checks. A protection lifted once, or a merge time read wrongly, would end a delivery as covered when its
|
||||
commit is not in the later one. Merge order stays, as the cheap first filter.
|
||||
4. **Ask the forge whether the later merge commit contains this one** — chosen. The forge module gains a
|
||||
read-only tool, `gitea_contains`, over the forge's compare of the two commits: none of this commit's history
|
||||
missing from the later one means it is contained. mesh-delivery is not given the forge's general API tool,
|
||||
which can write. The tool is TypeScript, as the rest of the forge module is; no Go route to the forge's API
|
||||
exists for a module to call. Asking the controller what each machine runs was weighed and left: the
|
||||
controller holds no clones, and what runs is already what the later walk's record says it sent.
|
||||
5. **Read the facts, and end the delivery by observation**, with point 4 deciding containment — below.
|
||||
|
||||
For issue 316 the alternative was to require `mesh/delivery-group` on the trunks, so that no member can merge
|
||||
before its composed check passed. That stays the operator's setting, as ADR 0239 left it. It does not settle
|
||||
@@ -74,26 +78,47 @@ a check that answers after the merge.
|
||||
that only adds a module the controller builds and sends nowhere. This holds whatever its check said: from
|
||||
`proposed`, `checked` and `rejected`, the row is tried before *merged unchecked → held*, and from `held`
|
||||
it is tried on every tick. `ready → published → delivered` already did this for a merge that passed.
|
||||
2. **A new final state, `covered`.** A merged delivery is covered when every module its plan moves was sent
|
||||
by a finished walk of another delivery that:
|
||||
- is of the same repository and was merged into the same trunk;
|
||||
- was merged after it, by the forge's merge times, so its merge commit contains this one;
|
||||
- is `delivered`.
|
||||
**A delivery that only adds a module is reported delivered whatever its build's outcome.** The build is
|
||||
the controller's, registered when it lands and sent nowhere. A failed build of it is the build seat's
|
||||
condition (S6) and is seen when the module is assigned, not this delivery's.
|
||||
2. **A new final state, `covered`: a later merge that contains it was delivered.** A merged delivery is
|
||||
covered when, for every module its plan moves, another delivery:
|
||||
- is of the same repository and was merged into the same trunk, after it by the forge's merge times (the
|
||||
cheap filter);
|
||||
- is `delivered`, and its own check passed;
|
||||
- was delivered by its own walk: the walk's commit is its merge commit, on that trunk;
|
||||
- sent the module to every machine running it, as its machine steps say (sent to all at once, or to the
|
||||
rest after its first machines passed), with no step of it failed or put back;
|
||||
- has a merge commit that contains this one's, as the forge says through `gitea_contains`.
|
||||
|
||||
The modules a walk sent are read from the walk's own record, or from the machine steps kept for that
|
||||
delivery. A delivery whose own walk is still open is never covered: its walk delivers it. The row is
|
||||
observed. It is tried from `held`, and from `ready` once the ten minutes for a walk to open have passed,
|
||||
before the no-walk hold. The transition's why names each covering delivery, the commit it merged as and its
|
||||
walk. On the pull request, `mesh/delivery` says `covered: …` as a **success**, never an error. A group
|
||||
counts a covered member as delivered, so the member after it gets its turn and is not stopped.
|
||||
3. **A group member is not held for a composed check that has not answered.** "Did not pass for the heads that
|
||||
The forge is asked only once every module has a candidate, once per pair, outside the holder's lock, and its
|
||||
answer is kept on the delivery. A forge that cannot answer means "not yet covered": the delivery stays where
|
||||
it is and is asked about again a minute later. A delivery whose own walk is still open is never covered: its
|
||||
walk delivers it.
|
||||
|
||||
The row is observed. It is tried from `held`, and from `ready` once the ten minutes for a walk to open have
|
||||
passed, before the no-walk hold. From `held` it is tried before `go`. If a walk a person started fails
|
||||
between two readings of a covered delivery, the delivery ends `covered`, not `delivering` then `failed`: the
|
||||
later merge that contains it was delivered whatever its own walk did. The transition's why names each
|
||||
covering delivery, the commit it merged as and its walk. On the pull request, `mesh/delivery` says
|
||||
`covered: a later merge that contains it was delivered: …` as a **success**, never an error. A group counts
|
||||
a covered member as delivered, so the member after it gets its turn and is not stopped.
|
||||
|
||||
`covered` says what was delivered, not what runs now: a revert merged after the covering merge is possible,
|
||||
and is that revert's own delivery.
|
||||
3. **A merge held without a passing check is covered without a person** when a later delivery whose own check
|
||||
passed carries it, and only then. A later delivery's walk runs only on a passing check, so its check has
|
||||
passed in every case this matters for. What a person would have been asked, "does this unchecked change go
|
||||
on?", was answered by the later change, which was checked with this one's change in its history and
|
||||
delivered.
|
||||
4. **A group member is not held for a composed check that has not answered.** "Did not pass for the heads that
|
||||
merged" means the check failed, ran for other heads, or was never asked. A member published while its
|
||||
group's check is asked and not answered waits in `published`, within that state's bound.
|
||||
4. **A hold whose reason is gone lifts.** A member held because its group's composed check had not passed is
|
||||
5. **A hold whose reason is gone lifts.** A member held because its group's composed check had not passed is
|
||||
taken back to `published` when that check passes or warns for the heads that merged, and its own walk is
|
||||
still open. The event is *lifted*, observed, and the member then takes its turn in the group's order. Every
|
||||
other hold stays a person's.
|
||||
5. **Read again on every tick.** The holder reads the facts of every held delivery on each tick, not only when
|
||||
6. **Read again on every tick.** The holder reads the facts of every held delivery on each tick, not only when
|
||||
an event arrives. A delivery that was held before these rows existed, or read back after a restart, ends by
|
||||
itself. Once it is final, nothing moves it again.
|
||||
|
||||
@@ -102,7 +127,7 @@ The table of ADR 0239 decision 2 gains these rows:
|
||||
| From | Event | To | Guard |
|
||||
|---|---|---|---|
|
||||
| proposed, checked, rejected, held | done | delivered | merged, no walk, and its plan moves no module the mesh holds |
|
||||
| ready, held | covered | covered | merged, no walk of its own open, and every module it moves sent by a finished walk of a later-merged, delivered delivery to the same trunk |
|
||||
| ready, held | covered | covered | a later merge that contains it was delivered: merged, no walk of its own open, and every module it moves sent to every machine running it by the walk of a later merge into the same trunk, delivered on its own passing check, whose merge commit the forge says contains this one |
|
||||
| held | lifted | published | held only for its group's composed check, which has since passed or warned for the heads that merged, and its own walk still open |
|
||||
|
||||
`delivered`, `failed`, `superseded`, `stopped`, `retired` and `covered` are final.
|
||||
@@ -112,11 +137,13 @@ The table of ADR 0239 decision 2 gains these rows:
|
||||
- **The held deliveries end without a person.** When mesh-delivery with these rows is delivered, its first
|
||||
ticks deliver the hq merges and the module-adding merges, and cover the controller and catalogue merges
|
||||
that a later walk carried. Their D14 conditions clear at the self-check's next reading of `stalled`.
|
||||
- **A pull request no longer says an error for a change that runs.** The four stopped on 2026-10-09 keep their
|
||||
- **A pull request no longer says an error for a change a later merge delivered.** The four stopped on 2026-10-09 keep their
|
||||
error: a final state is not undone. Their commit notes say why.
|
||||
- **The trunk's protection is now a premise of a delivery's end.** If a trunk ever accepted a push, a later
|
||||
merge might not contain an earlier one, and `covered` could be wrong. The protection is the forge's setting
|
||||
on each trunk. mesh-delivery does not check it itself, and this is a known gap.
|
||||
- **Containment is the forge's answer, not a premise.** The trunk's protection is no longer relied on: the
|
||||
forge's compare decides. mesh-delivery gains one forge tool, which reads only.
|
||||
- **The forge's tool must be delivered before `covered` can be reached.** Until `gitea_contains` answers, every
|
||||
candidate is "not yet covered", and a held delivery stays held as before. Nothing ends wrongly while the forge
|
||||
module is behind.
|
||||
- **`covered` is only as complete as the walks' records.** A module the later walk held back for a person is
|
||||
not sent, so it does not count. A delivery whose modules were only partly carried stays held for a person.
|
||||
- **A group member waits longer, rather than being held too soon.** A composed check that never answers leaves
|
||||
@@ -129,12 +156,20 @@ The table of ADR 0239 decision 2 gains these rows:
|
||||
before its check answered, on a failed check, or on a passing one. Each is delivered, and its status is a
|
||||
success.
|
||||
- `TestAMergeALaterDeliveredWalkCarriedIsCovered`: covered whether it was held first or not. It stays held
|
||||
when the later walk carried only one of its modules, merged before it, went into another branch, or failed.
|
||||
Its status is `success covered` and names the commit that covered it, never an error.
|
||||
when the later walk carried only one of its modules, merged before it, went into another branch, or failed,
|
||||
and when the forge says the later merge does not contain it. With the forge away it stays held, and is
|
||||
covered once the forge answers. The forge is asked once per pair, and never for a pair merge order rules
|
||||
out. Its status is `success covered: a later merge that contains it was delivered` and names the commit
|
||||
that covered it, never an error.
|
||||
- `TestACoveredGroupMemberLetsTheNextOneGo`: the member after a covered one is let go, and the group ends
|
||||
delivered.
|
||||
- `TestHeldDeliveriesReadBackCloseByThemselvesOnce`: deliveries held before these rows existed are read back
|
||||
from the store and end delivered or covered, or stay held when not covered. A second restart moves nothing.
|
||||
from the store and end delivered or covered, or stay held when not covered. A merge held without a passing
|
||||
check is covered by a later delivery whose own check passed, and stays held when that check did not pass. It
|
||||
stays held when the later delivery's walk was of another commit or another branch, or sent its module to its
|
||||
first machine only. A second restart moves nothing.
|
||||
- The forge module's `test/checks.test.ts`: a compare with no commits ahead is contained, one with some is
|
||||
not, an answer without a count is no answer, and only a sha reaches the compare's path.
|
||||
- `TestAGroupMemberWaitsForItsComposedCheckToAnswer` and `TestAMemberHeldForItsComposedCheckIsLiftedWhenItPasses`:
|
||||
a pass or a warning lifts the member; a failure, or a pass for other heads, keeps it held.
|
||||
|
||||
@@ -154,4 +189,4 @@ stopped or failed.
|
||||
module opens no walk.
|
||||
- [Design 47](../03-DESIGN/01-to-be/47-delivery-from-commit-to-delivered.md), *The state table* and *A
|
||||
delivery group*, which take these rows once this record is accepted.
|
||||
- mesh-catalog #150, which builds it.
|
||||
- mesh-catalog #150, which builds it, and the forge module's `gitea_contains` with it.
|
||||
+9
-5
@@ -60,18 +60,22 @@ Three things were missing, read from the controller's code
|
||||
- each walk that waits, with its repository, commit and the modules of its tier;
|
||||
- the bus build, from and to;
|
||||
- since when: the first refusal this controller saw, or the save that kept the refusal after a restart;
|
||||
- the verb `mesh-controller.bus upgrade --why … --reversible|--irreversible`.
|
||||
- the call that ends it, as the operator makes it through the mesh MCP server:
|
||||
`mesh_call mesh-controller.bus {"upgrade": "true", "why": "…", "reversible" or "irreversible": "true"}`.
|
||||
|
||||
Its plain words say that sends to the machine wait for a bus upgrade, what waits, for how long, and that the
|
||||
operator starts the upgrade from the mesh MCP server. It clears once the bus's machine runs the build the
|
||||
mesh holds, that is, once the step was taken, whatever the walks' notes still say. A walk whose note names
|
||||
operator starts the upgrade from the mesh MCP server. It clears once the bus's machine has been sent the build the
|
||||
mesh holds, that is, once the step was taken, whatever the walks' notes still say. From then on no send is
|
||||
refused for the bus, so nothing waits for a person; whether the new bus came up healthy is the step's own
|
||||
condition, `bus-maintenance` and then `bus-upgrade-failed` (probe DB), not this one. A walk whose note names
|
||||
an older bus build than the one held now does not count.
|
||||
2. **S3 leaves out a walk held only by the bus's step**, as it leaves out a walk under a paused build seat. The
|
||||
wait is said once, as itself, never as lateness.
|
||||
3. **The delivery plan says it before the merge.** When a change builds the module that provides the bus and
|
||||
that module runs somewhere, its plan has the step: "merging this needs a person's bus upgrade: nats is never
|
||||
sent by an ordinary send, and until `mesh-controller.bus upgrade --why … --reversible|--irreversible` runs,
|
||||
nothing else is sent to <its machines> either". Its summary says "merging this needs a person's bus
|
||||
sent by an ordinary send, and until the bus upgrade runs, nothing else is sent to <its machines> either —
|
||||
unless the new build turns out the same as the one running there (issue 280)". The plan cannot know that
|
||||
before the build, so it says so rather than staying silent. Its summary says "merging this needs a person's bus
|
||||
upgrade". The forge module writes that summary into the `mesh/merge-gate` status and the plan into the pull
|
||||
request's comment. It is said, not enforced: the gate's verdict does not change.
|
||||
|
||||
|
||||
+1
-1
@@ -50,7 +50,7 @@ The group's order put it second. Open question 4 in the report asks whether that
|
||||
|
||||
## 2026-10-09 — the fix, proposed
|
||||
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-whose-change-already-runs-or-has-nothing-to-run-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
(proposed) answers open questions 1 and 2, and mesh-catalog #150 builds it:
|
||||
|
||||
- **Asked and not answered is a reason to wait, not to hold.** `groupTurns` leaves a member in `published` while
|
||||
|
||||
+14
-10
@@ -110,7 +110,7 @@ later walk.
|
||||
|
||||
## 2026-10-09 — the fix, proposed
|
||||
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-whose-change-already-runs-or-has-nothing-to-run-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
[ADR 0270](../../02-DECISIONS/0270-a-held-delivery-that-moves-nothing-or-that-a-later-delivered-merge-contains-ends-by-itself-and-a-hold-whose-reason-is-gone-lifts.md)
|
||||
(proposed) gives both ends this issue asks for, by observation, and mesh-catalog #150 builds them in
|
||||
mesh-delivery:
|
||||
|
||||
@@ -118,15 +118,17 @@ mesh-delivery:
|
||||
the controller builds and sends nowhere opens no walk, so there is nothing to wait for. A merge whose plan moves
|
||||
nothing goes from `proposed`, `checked`, `rejected` or `held` to `delivered`, before the row *merged unchecked
|
||||
→ held*.
|
||||
- **`covered`, a new final state.** For every module its plan moves, a delivered delivery of the same repository
|
||||
and trunk, merged after it, whose finished walk sent that module. The later merge contains this one because
|
||||
the trunks take no push. The modules a walk sent are read from its record (`sent`, kept from now on) or from
|
||||
the machine steps already kept, which every delivered controller delivery holds. `mesh/delivery` says
|
||||
`covered: delivered with … (merged as …, walk …)` as a success.
|
||||
- **`covered`, a new final state: a later merge that contains it was delivered.** For every module its plan
|
||||
moves, a delivered delivery of the same repository and trunk, merged after it, delivered on its own passing
|
||||
check by the walk of its own merge commit, whose machine steps say the module reached every machine running
|
||||
it. The forge then says, through a new read-only tool `gitea_contains` (its compare of the two commits),
|
||||
whether the later merge commit contains this one; merge order is only the first filter. A forge that cannot
|
||||
answer leaves it held. `mesh/delivery` says `covered: a later merge that contains it was delivered: …` as a
|
||||
success.
|
||||
- **Read on every tick**, so what is held now ends without an event.
|
||||
|
||||
This answers open question 1: mesh-delivery reads it from its own records, and asks neither the controller nor
|
||||
the forge. Open question 3, why the controller's own merges opened no walk, is not answered by this fix; it no
|
||||
This answers open question 1: mesh-delivery reads it from its own records and asks the forge for ancestry; the
|
||||
controller is not asked. Open question 3, why the controller's own merges opened no walk, is not answered by this fix; it no
|
||||
longer leaves anything held.
|
||||
|
||||
**How to verify after mesh-delivery is delivered with it.** Within a minute of its start:
|
||||
@@ -137,7 +139,9 @@ longer leaves anything held.
|
||||
2. `mesh-delivery.show` of one of them, for example `novox/mesh-controller@b98fd0f396c8`, ends with a
|
||||
transition `held → covered (covered)` whose why names the later delivery and its walk.
|
||||
3. `mesh-delivery.checks` for its pull request shows `mesh/delivery` as `success covered: …`.
|
||||
4. At the self-check's next reading, `mesh-controller.conditions` no longer lists their
|
||||
4. The forge module is delivered in the same pull request. Until its `gitea_contains` answers, nothing is
|
||||
covered, and the held controller merges stay held.
|
||||
5. At the self-check's next reading, `mesh-controller.conditions` no longer lists their
|
||||
`delivery.<id>.stalled`.
|
||||
5. The two catalogue merges held for their group (issue 316) are covered only if a later delivered walk sent
|
||||
6. The two catalogue merges held for their group (issue 316) are covered only if a later delivered walk sent
|
||||
their module. One that stays held is a real question: its change may not run anywhere.
|
||||
|
||||
@@ -67,8 +67,8 @@ person, and the person must be told.
|
||||
- **Row S17**, `bus.<bus module>.step-waiting`: raised on the first watchdog tick after a walk's send was refused
|
||||
for the bus's planned step, while the new bus build would still replace the bus on its machine. It is a
|
||||
warning, for the operator. It names the machines, each waiting walk and the modules of its tier, the bus build
|
||||
from and to, since when, and `mesh-controller.bus upgrade --why … --reversible|--irreversible`. It clears once
|
||||
the bus's machine runs the build the mesh holds.
|
||||
from and to, since when, and the `mesh-controller.bus` call with `upgrade`. It clears once the bus's machine has
|
||||
been sent the build the mesh holds.
|
||||
- **S3 leaves out a walk held only by it.**
|
||||
- **The delivery plan says it before the merge**: "merging this needs a person's bus upgrade: … nothing else is
|
||||
sent to <machine> either", and its summary, which `mesh/merge-gate` carries, says the same.
|
||||
|
||||
Reference in New Issue
Block a user