Repository navigation
Whole-repo review waves W1–W4 and the input-reader integration - #54
Merged
Merged
Conversation
Fees stay in log space. The batch-scoped nonce with a sequencer-filled look-back would make replay state prunable and let the fee move to the batch; record it with its blocking gap (after a recovery rollback, a re-signed op does not exclude the original), and unblock Track 5 pending a sync with Bart.
Four server helpers turned a PermissionDenied bind into an eprintln and None, and 42 callers returned early, so a sandbox that denies bind reported the HTTP, WS and snapshot suites green without running them. The helpers now panic on any bind error and return the runtime.
A manual dispatch checked out the dispatching branch but named every artifact, image and the GitHub release after inputs.tag, and recorded GITHUB_SHA as the commit; a missing tag was then created at the default branch's head. A push now builds the pushed commit, and a dispatch builds the named tag by its fully qualified ref, so a branch or SHA input cannot stand in and a missing tag fails. Manifests and the image record the checked-out commit. Tag pushes were never marked pre-release, so each alpha became the latest release; SemVer pre-release tags (with '-') now are.
download-deps verified the kernel against a .sha512 downloaded from the same release, which catches transfer corruption but not a replaced asset. The kernel is part of the canonical template hash, so it now must match an in-tree pin, like every other downloaded input.
- The 429 test raced a spawned request for the lane queue's one slot and could hang if it lost; the server now starts with the queue full. - The transient-poster-error test asserted after a fixed 250 ms and failed under load; it now waits for the first submission. - Two e2e scenarios slept 7 s for the submitter's tick; they now wait until the submitter's tx is in Anvil's mempool.
- init deleted every checkpoint when config.json was missing, including an agreed head; it now removes only an interrupted init's bootstrap checkpoint and refuses otherwise. - Idempotent init now validates an existing config.json the way tick does, instead of reporting success and failing the next tick. - clear created a missing state directory and then reported that no divergence was latched; it now refuses a missing directory. config.json joins the backup list, since init no longer regenerates it over an agreed head.
With 0, the submitter's idle and transient arm slept for zero and re-ran its frontier and nonce RPCs back to back. The flag now takes the same range(1..) as the other poll intervals.
run and flush-mempool created the data dir and its lock file before finding no database, then exited 30 with a message that named no path. Only setup creates the directory now; run and flush refuse a missing one before taking the lock, and SetupNotComplete names the data dir. An unreadable or non-directory path stays an I/O error (exit 1).
TipAlreadyOpen was classified retry (exit 20), so the next boot would find the Tip and admit. Under the process lock only another writer or an inspection bug can produce it, and admitting would hide that; it now refuses (exit 30), like its postcondition sibling TipMissingAfterOpen. Aging-driven StaleDecision still retries.
The lane checks the shutdown flag only at the top of its loop. When graceful HTTP shutdown dropped the queue's senders after that check, the next turn returned ChannelClosed, which the drain treats as an error, so a clean SIGTERM could exit 1. ChannelClosed with shutdown requested now returns Ok. A lane test drives the race deterministically, and the in-crate server tests assert a clean stop instead of tolerating ChannelClosed.
fold_replay's comment claimed everything <= C was enforced, but only the post-drain empty-fridge check caught out-of-range inputs, and a replay batch above C leaves nothing in the fridge, so it was folded silently. Seeds and replay inputs now assert their block against the stop block; the fridge check stays as a backstop. The only caller already bounds its inputs. Also drops a review codename from a comment.
The header reserves NOT_FOUND and INVALID_DUMP for dump lifecycle calls, but the adapter mapped them to I/O errors from validate, execute and drain too, where the lane treats I/O as retryable: an engine bug became a restart loop instead of a terminal stop. The adapter now maps them to Internal outside from_dump/create_dump, and the reference engine reports them only from those two calls.
Every other client call is bounded, but subscribe() awaited the WS handshake with no deadline and hung on a stalled server; the harness wrapped it in its own timeout. It now fails with HandshakeTimeout after the configured request timeout.
…ding - AGENTS.md told every app to implement all four rejections; apps implement InvalidNonce and InsufficientFeeBalance, and the shared boundary alone produces InvalidMaxFee and NonceExhausted. - The scheduler spec and README now say the canonical machine's compiled sequencer_address must equal the pinned submitter, which nothing checks at runtime. - cockroach.md said the watermark is unavailable because the database is lost; recovery reads the fresh directory even when the old one survives. - A lifecycle comment claimed the completion rule had production callers only on the run side.
The Track 3 plan still had Bart integrating scheduler replay over the historical-L1 API. His client treats cockroach recoveries as out of scope, and a planned shared recovery tool will produce client startup state, so the cross-rebuild projection path receives bug fixes only.
The optimistic model describes a design that was not adopted. It stays as a dev analysis tool behind check-optimistic, out of check-all.
- The testsi cycle-target entry is fixed in the vendored copy (relative target since 1283765); delete it. - The RPC-URL entry also covers transport errors in the reader, poster, flusher and fee oracle, and the watchdog's persisted state. - The SDK entry narrows to HTTPS for ingress; WSS waits for the new sync. - The admission entry names where setup reaches the completion rule. - The fee-observation entry records the per-poll stamp's purpose and cost.
The SDK, the harness and the sequencer's tests pinned reqwest 0.12, so the build compiled two reqwest stacks. reqwest 0.13 puts RequestBuilder::query behind a feature, which the SDK now enables.
testsi pinned thiserror 1 and two members pinned toml 0.8, each keeping a second copy of those stacks in the build; both now use the workspace version (toml moves into [workspace.dependencies]). The info.toml and checkpoint.toml documents are flat integer tables, which toml 1.x reads and writes the same way. The sequencer no longer declares alloy-network-primitives, which nothing used.
The client rejected every https:// endpoint. It now accepts http:// and https://, using reqwest's rustls feature, the TLS stack alloy already builds, with the platform trust store. The WS feed stays plain ws://, so an https client cannot subscribe; the subscribe() doc says so. A test pins that an https request opens with a TLS ClientHello; certificate rejection is recorded as a verification gap.
Thirteen error variants wrapped a #[source] but left it out of their
Display, and every log and abort sink prints Display only, so an exit
read 'snapshot at batch close failed' with no reason. They now end with
': {source}', the convention CommandError and BootstrapError already
use. Three feed and history messages also named the wrong operation.
The variant names what prepare() actually reads.
One helper names the path an I/O error happened at and keeps its kind.
The data-dir and dumps-dir creates, the database probes, the process lock and the orphan sweep surfaced bare OS errors such as 'Not a directory (os error 20)', which name no file. They now say which path failed and keep the error kind, so exit codes do not change. The lock's I/O variant carries its path.
…shes The flusher watched each submitted no-op's hash for inclusion. When the original batch tx won a slot instead, the no-op never landed and the pass waited out its full 10-block timeout before re-checking. It now waits, under the same timeout, until the account's Latest nonce reaches the flush end, whichever tx consumed each slot. Errors still propagate; the outer loop is unchanged.
Every failed command logged its exit line at ERROR, including expected recovery (10) and retryable exits (20), which breaks ERROR-based alerting. Terminal and unclassified exits keep ERROR; the others log at WARN. The line now names the verdict and the supervisor action, and the SIGABRT abort line says it is terminal.
The reader and the submitter logged every retried provider failure at ERROR, once per poll during an L1 outage. They now log at WARN, and the provider error names the failing call (eth_chainId, eth_sendRawTransaction nonce=N, ...). A watermark write failure on the submitter path logs as such instead of as a provider error. A test pins that the mempool-conflict classifier still matches labelled messages.
The CANONICAL DIVERGENCE line named the nonce and input index only; an
operator then had to join tables to find the offending transaction and
compare the two sides. The page query now reads the transaction hash,
and the line carries it, the landed payload's hash and ours ("none" for
a foreign landing at a nonce we never sealed).
Module headers stay within four lines and name their owners instead of restating them. protocol.rs says what the module holds and that its acceptance predicate must agree with the canonical fold (I1); the batch-submitter-as-parameter rationale stays once, on the ProtocolTiming type doc, and leaves scheduler_accepts. fee.rs keeps the encoding, the units it covers and the bit-for-bit porting rule, with fee_to_linear as the conversion owner. SUGGESTED_MAX_FEE_SLACK says what it is and how it differs from batch_policy.log_slack; the FeeResponse type doc already says why wallets get the slack. The wallet snapshot header names format.md as the layout owner and drops an intra-doc link that did not resolve. The canonical-app shell and the golden-case header lose their restated detail; consensus_vectors.rs owns "never regenerated".
Three scheduler comments told how the code got here: inspect_state's visibility "not pub(super)" because the harness is "now" a separate crate, and two tests' notes on what used to be tested only partly. The comments keep the present-tense reason and the property each test pins. Folded into the guest batch so the template hash moves once.
Four doc comments, the schema comment on safe_accepted_batches, and invariants I3 and I12 named Storage::append_safe_inputs, which became a test seed in storage::test_helpers: rustdoc could not resolve the links, and all of them named the wrong writer. They now name append_ingested_safe_inputs_with_timestamp, which holds the monotonicity asserts and maintains safe_accepted_batches in production. The schema comment says a parentless root carries the batch-tree anchor's nonce, 0 at genesis, as its trigger enforces. register.md links the oracle outage policy at its new threat-model subsection, and cartesi-machine.md drops a 'no longer exists' history note.
W4's guest commits trimmed comments in sequencer-core, app-core and canonical-app and moved advance_expected_batch_nonce into the submitter, so the template hash moved again. The cycle count to the first yield is unchanged, as comment edits only shift the line numbers rustc embeds in panic locations. Both moves ship in the same next image, so the existing record gains a W4 row and new final values instead of a second record: devnet 0xa0c7296d..., Sepolia 0x410cc0e1..., cold builds, consensus vectors byte-identical, canonical-test-guest 15/15.
W4's progress line, what it delivered with the AGENTS.md before and after counts, the porter spec, the threat-model restructure, the header cap, the nonce-scan move and the README fixes. It also records what was skipped as already fixed or deferred to the recovery tool, the FS1 and B1 rows as decided and documented, N25 as done, and two items under Not doing: a CI check for the size caps, and bounding archive downloads per connection.
GCdePaula
marked this pull request as ready for review
October 15, 2026 10:17
Pin the v0.22.0 emulator and the kernel its dependencies.lock pairs it with, cartesi/linux v6.18.56-ctsi-1. machine-linux-image stops at v0.21.0, so the kernel now downloads from cartesi/linux, its release pin is renamed CARTESI_LINUX_RELEASE, and the pins check ties the kernel filename to that tag. Guest tools stay at 0.18.0. cartesi/linux tags name the kernel version, so no in-tree check can pair the kernel with the emulator any more, as the old release-equals- version check did. verify-toolchain-pins.sh --upstream, run before each release, compares the kernel and guest tools tags with the emulator's dependencies.lock and the kernel SHA-512 with the one its release publishes. v0.22 refuses machines stored by v0.21 (config archive version 8, marchid 22), so every image and watchdog checkpoint is regenerated. The template hash moves even for identical inputs: the machine identifiers and the microarchitecture's state are part of the root hash. testsi moves to the v0.22 Rust bindings at Dave's v3.0.0-alpha.6 tag, which pass the u32 cmio length the v0.22 header takes. The watchdog's CLI oracle names the renamed input_file_index_* keys, since the v0.22 CLI rejects unknown --cmio-advance-state keys.
…dget v0.22 reads a stopped machine's outcome in the order halt, manual yield, then exhausted budget, so an RX_ACCEPTED or RX_REJECTED yield on the budget's last cycle is an accept or a reject for the CLI and Dave. The watchdog checked the budget first and would have latched a dead canonical machine there instead, and refused a stored head in that state. Check the flags first, in the emulator's order. Reaching the budget end to end takes about 2^48 cycles, so a fake cartesi module pins the order.
v0.22 makes a cmio response the machine cannot take a silent no-op: with no yield pending, an advance at any yield but RX_ACCEPTED, or data larger than the RX buffer. v0.21 refused those sends, and the README's case for running in-process leaned on that refusal. The watchdog never sends in those states, but it now asserts after each send that iflags.Y cleared, which only delivery does, so a broken precondition fails loud instead of reading the old yield as the input's outcome.
v0.22 makes an advance-state response the machine cannot take a silent no-op. testsi already checks for an rx_accepted yield before sending, but an input larger than the 2 MiB RX buffer would be dropped: run would return at the same yield, and advance_state would report an accept with no outputs. Check after sending that iflags.Y cleared, which only delivery does, and fail with InputNotDelivered otherwise. The comments that named v0.21 now state the rules they rely on.
The page holds for the pinned emulator and asks for a re-check on every bump. Against v0.22.0 three statements were false: a response the machine cannot take is a silent no-op, not a refusal; the CLI asserts an RX_ACCEPTED yield before an inspect query; and --no-revert aborts at the first rejected input. Correct those, and add what the page left out: how a version mismatch is detected, why an emulator bump alone moves the template hash, the outcome order at the budget limit, the SHARING_ALL refusals of read-only and hard-linked files, the renamed --cmio-advance-state keys, and --assert-rolling-template's outputs-root check. The kernel now comes from cartesi/linux, and the storage measurements are labeled as v0.21.0's.
v0.22 refuses to map a file for writing in place when it has no write permission, and clone_stored hard-links read-only files, so a bootstrap machine made read-only fails at the first tick: as root with "is read-only and cannot be shared for writing", otherwise at open with "Permission denied". v0.22 also loads nothing an older release stored, and a clone reports that as an internal error, not as the documented operator one. Say to leave the bootstrap machine's permissions as --store sets them (the PMAs file alone read-only), to keep a release per Cartesi Machine version for replay and evidence, and to start a new deployment's watchdog in a new state directory, keeping the old one intact. List the messages in troubleshooting, and tell developers that recipes reuse images stored by another emulator version.
The record keeps one ledger from the deployed baseline to the next image, since every move since then ships in that image. It gains the Cartesi Machine v0.22.0 and kernel 6.18.56-ctsi-1 row and new final values: devnet 0xcd299178... (53967473 cycles), Sepolia 0xeaf87115..., both from two agreeing cold builds. The devnet dapp and rootfs equal W4's, so the template moved through the emulator and kernel alone; the Sepolia dapp also carries the dedicated sequencer address from aa0fc0f, which was not measured on its own. The review record's progress line names the bump.
GCdePaula
force-pushed
the
claude/repo-code-review-103cc5
branch
from
October 20, 2026 15:03
aec6530 to
ee79a8d
Compare
…ecutor The input budget's seams and an advance the machine does not take were pinned only against a fake cartesi module: the test guest cannot reach them, since the budget lasts about 2^48 cycles per input. Build the seam guest of sling's MachineStf tests (dave@dbcca7d4, machine_stf.rs:930-963) in Lua instead: 4 KiB of RAM and five instructions that clear fromhost, yield the tohost word in x19, then yield x20 each time they resume. It needs no image and runs in milliseconds. The rows port sling's pending_input_yield_outranks_halt_and_budget cases (:1072-1102) and its runner test (:1452-1487). An accept on the budget's last cycle takes two inputs, at mcycle 4 and then 6, and ends at the CLI's root hash. A budget that runs out before the yield, and an exception, an unexpected reason or a rejection on the last cycle, each make open refuse the machine. An advance after an exception trips the delivery assert of 7bd5f56 on the real emulator. The seam accepts declare 32 bytes of data, the outputs root libcmt yields, because the CLI asserts that length on every accepted input.
…step does status() classified a pending yield through receive_cmio_request, which also reads the yield's data field and throws when its declared length exceeds the 2 MiB TX buffer (e22 machine.cpp:1907-1911). A guest that bypasses libcmt can declare such a length. The step and sling read only iflags.Y and tohost's device, command and reason, so they take the input, revert it or stop there, while the watchdog raised an internal error on every tick, never passed that input and latched nothing. It also ignored the device field, so a preset template with Y set under another device read as accepted and then tripped the delivery assert. Read the header as sling's MachineStf::manual_yield_reason does (dave@dbcca7d4, machine_stf.rs:120-139; sling since dave ecdc2694), and as the emulator's own delivery check, isYieldedManualWith, does: a manual RX_ACCEPTED, RX_REJECTED or TX_EXCEPTION yield under the yield device, and an unexpected yield for anything else. The exception's message is still read through receive_cmio_request, as evidence only; when the emulator refuses it, the message names the declared length instead. An unexpected yield's description now names the device too. libcmt bounds every length it declares, so no guest built on sdk/guest reaches the oversized case, and only a preset image carries Y under another device or command. The halt-first order is unchanged. A unit test pins the classification against a fake that refuses an oversized length as the emulator does. Five e2e rows build sling's oversized and preset machines (machine_stf.rs:998-1037, 1109-1114). The CLI aborts reading those yields, so the step's rule is their oracle.
Every tick clones the head and loads the clone with SHARING_ALL to run it in place, and v0.22 refuses to map a file without write permission for writing. clone_stored keeps file modes, so a bootstrap machine made read-only (chmod -R a-w, a read-only store, a re-packed tarball) passed init, whose loads are all SHARING_NONE, and every idle tick, including a genesis bootstrap's whole wait. The first tick with inputs to compare then failed, and so did every later one, with no comparison made. Sling refuses such a template at startup (dave@dbcca7d4, storage/open.rs:149-165), and the CLI's --revert-mode=stored fails at its startup SHARING_ALL load (e22 cartesi-machine.lua:2824-2828, 2966-2967). Before it publishes the head, init now opens its clone of the bootstrap machine through machine.open, the first tick's own step: a clone, a SHARING_ALL load and the accepted-yield check. The emulator's own rules decide (any write bit, the link count, running as root), so there is no file list to keep in step with it. A failed load becomes an operator error that names the bootstrap machine and suggests chmod -R u+w, as sling's message does. The operator's bootstrap directory is still loaded only with SHARING_NONE, and the probe costs one more clone at init. The README and the deployment guide say init checks this, and the troubleshooting table gains the init message; the tick's message stays for a head checkpoint that loses its write permission later.
status() checked iflags.H before iflags.Y, the emulator's run break order, which the watchdog has followed since c0bebe8. The step reads only the pending yield: send_cmio_response delivers an advance to any RX_ACCEPTED yield, halted or not, and the uarch reset reverts any RX_REJECTED one. Sling's MachineStf::terminal_fixed (dave@dbcca7d4, machine_stf.rs:287-297) ranks the pending yield first, and its tests deliver an input to a machine preset halted with an accept pending (halted_yield_machine, :989; :1104, :1181). The watchdog refused such a template at init. Classify a pending yield first, then a halt, then an exhausted budget, in the structure of terminal_fixed. Execution never sets both flags (interpret stops while either is set, and delivery clears Y), so the orders differ only on a preset image, which our recipes reject through --assert-rolling-template. On such an image a range-source watchdog now delivers input 0 and latches canonical_machine_dead there, the state sling computes. An inspect source still fails init, because the query cannot run on a halted machine; sling has no inspect path. A halt reports an exit code only when tohost holds the halt device: after delivery to a preset halt, tohost still holds the yield header, whose data field is no exit code. The CLI ends the epoch at boot on that image (cartesi-machine.lua:4577, confirmed by running it), and Dave lists that among the CLI's named exclusions, so the e2e row's oracle is the step's delivery on a fresh load. The host-semantics doc now states the step's precedence and the emulator's break order as two facts.
… driver The host-semantics section named the CLI as the reference loop and said Dave's PRT client implements the same rules, which any host must match exactly. Dave treats the Solidity step as the authority and the release CLI as no independent reference (computation-hash.md:217-219, 383-394), and sling and the CLI differ at the corners the two previous commits pinned. State the step as the authority and name what the watchdog follows: sling's MachineStf (feed, run_big, restore_rejected, terminal_fixed, manual_yield_reason) at dave@dbcca7d4, and the v0.22.0 CLI's epoch driver without a hash builder as the test oracle. Each corner sits at the rule it qualifies. Neither reference checks iflags.Y after a send, and both take the one undeliverable advance at an RX_ACCEPTED yield, data over the 2 MiB RX buffer, as an unchanged accept, which the alpha.10 InputBox's 65,536-byte cap rules out (Dave's LeafTournamentGasFixture.sol:89-92, 444-450). Later inputs after a fixed point reach sling as windows carrying the frozen state (dave ea0318f3; epoch-lifecycle.md:70-78). Epochs change no machine state (DaveConsensus instantiates the next tournament from the final state; sling's roll_epoch), except past the 2^24-input epoch span, which is accepted on cost (dimensioning.md:107-112). The Dave pin joins the re-check rule. The watchdog README points at that section and gains one bullet for its deliberate differences from sling: the safe L1 head, the trusted head, the fail-loud delivery assert (kept), stopping at a fixed point, the trusted non-genesis start, no free-space floor, inspect, and the epoch tail. It now says the watchdog runs the CLI's per-input loop without its two per-input fsyncs (cartesi-machine.lua:3726, 3732), as sling syncs only a batch's final boundary (advance.rs:10-15), and that a scan stopped at a fixed point skips the InputBox count check. The machine.lua comments follow: the module header names both references; deliver() says the assert sees a no-op only where iflags.Y was set, since callers send only at an accepted yield; the run loop says why no other break reason occurs; and the executor no longer claims to be exactly the CLI's stored mode.
A full disk while the working machine runs in place is no internal error: the emulator's write through a shared mapping faults, the in-process emulator installs no SIGBUS handler (only SIGWINCH and SIGPIPE; e22 os.cpp:279, 645), and the tick dies before it writes last_tick.json or status.prom. Sling keeps a 1 GiB floor for this (machine_runner/mod.rs:70-80); the watchdog keeps none. The README's failure classes now say a SIGBUS reaches only the stale-heartbeat alert, the alert response names it, and the disk-usage note no longer calls per-input clones nearly free, since every page a catch-up writes takes real blocks. The trusted-checkpoint bootstrap now says a Dave node seeds mid-history only from the consensus's last settled root, which the watchdog does not read, so a wrong checkpoint costs a false state_mismatch. The troubleshooting row for an incomplete L1 view adds a safe block replaced below the head when the error survives a provider switch. The version facts were overstated. Loading rejects another minor release (archive version 8, marchid, and mimpid = major*1000+minor; e22 machine.cpp:196-208, machine-config.cpp:154-156), but a patch release of the same minor loads stored machines and keeps the root hash by design (CHANGELOG 0.15.2). The Cartesi Machine page and the upgrade guide say so, and that the release the operator runs, not loading, holds the watchdog to the template's exact version.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
Waves W1–W4 of the 2026-10-13 whole-repo review, plus the input-reader review branch (
claude/input-reader-review-fda7ad, adapted), as one stack.Plan, decisions, and what each wave landed: docs/review/2026-10-13-whole-repo-review.md.
test_support, storage fixtures instorage::test_helpers, the benchmark crate trimmed.safe_inputs.aa0fc0f): re-signed with author kept, becausemainrequires signed commits; same tree asaec6530.v6.18.56-ctsi-1); Dave bindings at tagv3.0.0-alpha.6. The watchdog and testsi now check every cmio response reaches the machine (v0.22 drops a refused one silently), and the watchdog ranks a pending yield and a halt above an exhausted budget. Facts re-checked in docs/cartesi-machine.md.initrefuses a bootstrap the tick cannot run in place. Reference and deliberate differences: host semantics, watchdog README.Risk and compatibility
tests/fixtures/consensus_vectors_v1.json. Hashes: guest image record. The next release's notes must state the new hash.replayits checkpoints and evidence (upgrades).rust-srconly on a later run, so the hash depended on build-cache history.just canonical cross-toolchainnow runs before every guest build.initrefuses an image whose files lack write permission; submitter log keystx_nonce→wallet_nonce; a 3xx at setup's chain-id check exits 30; an undecodable batch from our own key abortsrun(terminal); genesis-dump PermissionDenied exits 1; L1 error text is one line, capped, without the RPC URL.scripts/verify-action-pins.sh; runs on PRs and pushes tomain/v*; cargo-deny; runner pinned toubuntu-24.04.ghreplacesaction-gh-release,download-artifact8 /upload-artifact7,build-push-action7. Worth a rehearsal with a throwaway tag on a fork.Verification
cargo test --workspace,clippy -D warningson every W3b commit, fmt, Lua tests and luacheck.just canonical-test-guest, including the golden consensus cases, on cold-built images; two cold v0.22 builds agree.