Deploy a new build with verification and automatic rollback #69

Closed
claude wants to merge 1 commits from overnight/self-update into overnight/model-swap
Contributor

What

internal/update + cmd/mavupdate: deploy a new build of Maven to the box she
runs on, verified before it is committed and rolled back automatically when it
does not come up. Off unless an update block is in mavend.json.

Apply is: health-check the running daemon → snapshot the deployed artifacts →
make buildmake test → install → restart → health-check → restore the
snapshot on any failure.

Why the order is what it is

  • Preflight health check first. If she is already not answering, a failed
    update and an already-broken box are indistinguishable afterwards and the
    rollback has no baseline to prove itself against. Apply refuses to start.
  • Snapshot before the build. make build writes its binaries into the
    working tree, and on the docker deployment the tree is the install dir — so
    snapshotting after the build would snapshot the new artifacts and leave nothing
    to roll back to. This was the one non-obvious ordering bug available here.
  • Verify before deploying anything. make build + make test (not go build — the CGO daemons need the Makefile's toolchain and lib paths). A failed
    verify also restores the tree's artifacts, so a later restart by hand cannot
    deploy code that failed its own tests.
  • The rollback depends on nothing that changed. Byte-for-byte copies out of
    the snapshot dir, sha256-verified against the manifest on the way in, plus the
    same restart command. No build, no toolchain, no migration, no cooperation from
    the code being replaced. It runs on context.WithoutCancel — a rollback
    interrupted halfway is worse than the failure that caused it. A corrupt
    snapshot is refused rather than restored. When the restore or its restart fails
    anyway, it returns ErrRollbackFailed, says she is probably down, and names the
    directory to copy back by hand instead of reporting a tidy rollback.
  • Snapshots are copies, not hardlinks or a git stash. A hardlink into the
    install dir gets clobbered by the very install it exists to undo, and a
    git-based undo needs a clean tree and a rebuild — two things a failed update is
    likely to have taken away.

What I refused to build, and why

The task said to ship the safe subset and say plainly what was left out. All of
this is enforced by the code's shape, not by a note:

  1. No automatic and no periodic anything. No release checker, no update
    channel, no timer, no tick-loop hook. Apply runs when a human runs the CLI.
  2. No IPC method and no web route. The obvious design — MethodApplyUpdate
    plus a button on /tools behind the step-up passkey gate — was considered and
    refused. Step-up protects against the wrong person clicking; it does not change
    the fact that anything reachable over the network becomes, given one mavweb
    bug, a remote code path with a build system attached. The trigger requires
    shell access on the host, a strictly higher bar than the gate guarding the tool
    allowlist. This is the one place I did not follow the "put it behind step-up
    like /tools" instruction, and it is because no-remote-trigger is strictly
    stronger than step-up, not weaker.
  3. Nothing Maven says or routes can reach it. mavend does not import
    internal/update, so there is no act, intent, tool or LLM output that leads
    here. She cannot update herself. She can be updated, by him.
  4. It does not fetch code. No release server, no registry, no GitHub. The new
    version is whatever the owner pulled into the working tree. Downloading code
    and trusting a checksum that arrived in the same download is not a property
    verifiable on one box, and it is the shape most supply-chain compromises take.
  5. No in-process crash-loop supervisor. The plan asked for one. A process
    cannot reliably notice that it keeps dying, and one that believes it can is
    worse than nothing. Restart-on-crash belongs to whatever starts mavend
    (compose restart: unless-stopped). What is guaranteed instead is narrower and
    real: within one Apply, the new build must answer before it is considered
    deployed, and if it does not the old bytes go back and must answer again.
  6. The database is not snapshotted or rolled back. It is encrypted, live, and
    often bigger than the disk headroom, and a store rolled back under a schema
    that already migrated forward loses writes silently — worse than a failed
    update. Schema compatibility stays store.Migrate's job. A snapshot here is
    the deployable artifacts only: binaries and config.

Also: config files are snapshotted but never overwritten by an install. An
update does not get to replace the operator's config.

Health check

Not "the process is up" — mavend can be running with a dead store or a socket it
never bound. It dials the real IPC socket and performs a real Presence read,
which exercises the socket, the dispatch table and the store in one call.
Presence is read-only, so a health check never leaves a trace in her memory.
The config is refused at load without health_socket: an update that cannot check
its own result cannot roll back.

Verified

  • make build — 10 binaries including the new mavupdate, exit 0.
  • make test — full -race suite, 48 packages ok, exit 0.
  • internal/update at 76.6% coverage, 13 cases against a fake box (a temp dir
    for the install tree, an injected Runner for make/git/docker, an injected
    HealthCheck for mavend) so the paths nobody exercises by hand are the ones
    under test: refusal on an already-down daemon, build failure short-circuiting
    the tests, test failure deploying nothing and restoring the tree, unhealthy
    after restart rolling back to the old bytes with the restore proven to have
    happened before the second restart
    , restart failure reported as manual
    recovery, rollback working with the source tree deleted and the toolchain
    failing, config never overwritten, corrupt snapshot refused, manifest-less
    snapshot never offered as a target, prune never dropping the newest.
  • Not run against the real deployment — by construction it restarts mavend, so
    that is the QA step on homesrv, and it is written to be run in the order
    verifyapply → deliberately-broken applyrollback.

deploy/README.md gains the config block and the exact commands. Nothing was
added to deploy/mavend.json: the capability stays off until the owner writes it.

Vikunja #249

## What `internal/update` + `cmd/mavupdate`: deploy a new build of Maven to the box she runs on, verified before it is committed and rolled back automatically when it does not come up. Off unless an `update` block is in `mavend.json`. `Apply` is: health-check the running daemon → snapshot the deployed artifacts → `make build` → `make test` → install → restart → health-check → restore the snapshot on any failure. ## Why the order is what it is - **Preflight health check first.** If she is already not answering, a failed update and an already-broken box are indistinguishable afterwards and the rollback has no baseline to prove itself against. `Apply` refuses to start. - **Snapshot before the build.** `make build` writes its binaries into the working tree, and on the docker deployment the tree *is* the install dir — so snapshotting after the build would snapshot the new artifacts and leave nothing to roll back to. This was the one non-obvious ordering bug available here. - **Verify before deploying anything.** `make build` + `make test` (not `go build` — the CGO daemons need the Makefile's toolchain and lib paths). A failed verify also restores the tree's artifacts, so a later restart by hand cannot deploy code that failed its own tests. - **The rollback depends on nothing that changed.** Byte-for-byte copies out of the snapshot dir, sha256-verified against the manifest on the way in, plus the same restart command. No build, no toolchain, no migration, no cooperation from the code being replaced. It runs on `context.WithoutCancel` — a rollback interrupted halfway is worse than the failure that caused it. A corrupt snapshot is refused rather than restored. When the restore or its restart fails anyway, it returns `ErrRollbackFailed`, says she is probably down, and names the directory to copy back by hand instead of reporting a tidy rollback. - **Snapshots are copies, not hardlinks or a git stash.** A hardlink into the install dir gets clobbered by the very install it exists to undo, and a git-based undo needs a clean tree and a rebuild — two things a failed update is likely to have taken away. ## What I refused to build, and why The task said to ship the safe subset and say plainly what was left out. All of this is enforced by the code's shape, not by a note: 1. **No automatic and no periodic anything.** No release checker, no update channel, no timer, no tick-loop hook. `Apply` runs when a human runs the CLI. 2. **No IPC method and no web route.** The obvious design — `MethodApplyUpdate` plus a button on `/tools` behind the step-up passkey gate — was considered and refused. Step-up protects against the wrong person clicking; it does not change the fact that anything reachable over the network becomes, given one mavweb bug, a remote code path with a build system attached. The trigger requires shell access on the host, a strictly higher bar than the gate guarding the tool allowlist. This is the one place I did not follow the "put it behind step-up like /tools" instruction, and it is because no-remote-trigger is strictly stronger than step-up, not weaker. 3. **Nothing Maven says or routes can reach it.** `mavend` does not import `internal/update`, so there is no act, intent, tool or LLM output that leads here. She cannot update herself. She can be updated, by him. 4. **It does not fetch code.** No release server, no registry, no GitHub. The new version is whatever the owner pulled into the working tree. Downloading code and trusting a checksum that arrived in the same download is not a property verifiable on one box, and it is the shape most supply-chain compromises take. 5. **No in-process crash-loop supervisor.** The plan asked for one. A process cannot reliably notice that it keeps dying, and one that believes it can is worse than nothing. Restart-on-crash belongs to whatever starts mavend (compose `restart: unless-stopped`). What is guaranteed instead is narrower and real: within one `Apply`, the new build must answer before it is considered deployed, and if it does not the old bytes go back and must answer again. 6. **The database is not snapshotted or rolled back.** It is encrypted, live, and often bigger than the disk headroom, and a store rolled back under a schema that already migrated forward loses writes silently — worse than a failed update. Schema compatibility stays `store.Migrate`'s job. A snapshot here is the deployable artifacts only: binaries and config. Also: config files are snapshotted but **never overwritten** by an install. An update does not get to replace the operator's config. ## Health check Not "the process is up" — mavend can be running with a dead store or a socket it never bound. It dials the real IPC socket and performs a real `Presence` read, which exercises the socket, the dispatch table and the store in one call. `Presence` is read-only, so a health check never leaves a trace in her memory. The config is refused at load without `health_socket`: an update that cannot check its own result cannot roll back. ## Verified - `make build` — 10 binaries including the new `mavupdate`, exit 0. - `make test` — full `-race` suite, 48 packages ok, exit 0. - `internal/update` at 76.6% coverage, 13 cases against a fake box (a temp dir for the install tree, an injected `Runner` for make/git/docker, an injected `HealthCheck` for mavend) so the paths nobody exercises by hand are the ones under test: refusal on an already-down daemon, build failure short-circuiting the tests, test failure deploying nothing and restoring the tree, unhealthy after restart rolling back to the old bytes **with the restore proven to have happened before the second restart**, restart failure reported as manual recovery, rollback working with the source tree deleted and the toolchain failing, config never overwritten, corrupt snapshot refused, manifest-less snapshot never offered as a target, prune never dropping the newest. - Not run against the real deployment — by construction it restarts mavend, so that is the QA step on homesrv, and it is written to be run in the order `verify` → `apply` → deliberately-broken `apply` → `rollback`. `deploy/README.md` gains the config block and the exact commands. Nothing was added to `deploy/mavend.json`: the capability stays off until the owner writes it. Vikunja #249
claude added 1 commit 2026-08-01 02:10:05 +02:00
internal/update applies a new build of Maven to the box she runs on and
undoes it when the new build does not come up. cmd/mavupdate is the only
trigger: a CLI the owner runs on the host.

Apply is health-check the running daemon, snapshot the deployed artifacts,
make build, make test, install, restart, health-check — and restore the
snapshot on any failure. The order is load-bearing:

  - The preflight health check refuses to update a daemon that is already
    not answering. Without a working baseline, a failed update and a box
    that was already broken are indistinguishable, and the rollback has
    nothing to prove itself against.
  - The snapshot is taken BEFORE the build, because make build writes its
    binaries into the working tree and on the docker deployment the tree
    is the install dir — snapshotting afterwards would snapshot the new
    artifacts and leave nothing to roll back to.
  - Verification is make build plus make test, before anything is
    deployed, so a broken tree costs time and nothing else. A failed
    verify also puts the tree's artifacts back, so a later restart by
    hand cannot deploy code that failed its own tests.
  - The rollback depends on nothing that just changed: byte-for-byte
    copies out of the snapshot dir, sha256-verified on the way in, and
    the same restart command. No build, no migration, no cooperation from
    the code being replaced. It also runs on an uncancellable context —
    a rollback interrupted halfway is worse than the failure that caused
    it. When the restore itself fails it says so and names the directory
    to copy back by hand rather than reporting a tidy rollback.

Off unless configured, and the refusals are code, not documentation. The
daemon does not import this package: there is no IPC method, no web route,
no timer and no act that can start an update, so nothing Maven says or
routes reaches it. Nothing fetches code — the new version is whatever the
owner pulled into the tree. The plan's release checker, auto-update
channel and in-process crash-loop supervisor are deliberately absent; a
process cannot reliably notice that it keeps dying, and restart-on-crash
belongs to compose or systemd. The database is never snapshotted or rolled
back; schema compatibility stays store.Migrate's job.

The config is refused at load without a health socket, since an update
that cannot check its own result cannot roll back, and refused when the
snapshot dir is inside the install dir, since a restore must not read from
what the install writes.

Vikunja #249
claude reviewed 2026-08-01 11:41:03 +02:00
claude left a comment
Author
Contributor

The refusals in the package comment are the best part of this PR, and they are enforced rather than described. No timer, no IPC method, no web route, no fetch of code from anywhere. Snapshotting before the build, with the reason spelled out, is the non-obvious ordering and it is the correct one. The preflight health check is right, and for the reason given. Without a baseline, a failed update and an already broken box are the same picture. context.WithoutCancel around the rollback is the detail that makes a Ctrl-C during the health wait safe. Copies rather than hardlinks or a git stash, re-hashed on the way back out. A restore is checked, not hoped for.

Four things.

1. On the documented deployment the rollback restores bytes that nothing reads. The README config sets source_dir and install_dir to the repo and restart_cmd to docker compose up -d --build. Follow it through. install is a no-op because the two dirs match. The restart rebuilds the image, and this repo's Dockerfile copies cmd/ and internal/ and runs go build inside the builder stage. It never copies a host binary. .dockerignore excludes the built binaries by name, with the comment "rebuilt inside the image". So what gets deployed is the source tree, which Apply never touches and Restore never reverts.

The failure case is the one this package exists for. He pulls a bad commit and runs apply -yes. Build and test pass, the restart builds an image from the bad source, she does not answer, waitHealthy burns 120s. The rollback copies the old binaries into the tree and runs the same restart command. That rebuilds the same image from the same bad source. She does not answer again. It returns ErrRollbackFailed with "SHE IS PROBABLY DOWN" and an instruction to copy files back by hand. Copying them back by hand would not have helped either. The box stays down for two health timeouts plus two image builds. The only recovery is a git checkout the operator has to work out himself.

The one part of the restore that does reach the running system is deploy/mavend.json, because compose bind-mounts it read-only from the tree. That is the file the Config comment says an update never replaces.

The snapshot needs to cover whatever the restart command deploys. For an image built from source that means the commit, and gitHead is already recorded. So either record and restore the tree state for that deployment shape, or refuse that config outright. A README that sends him to a two-stage build-then-copy layout would be the honest alternative.

2. The health socket in the README cannot be opened by the account the README tells him to use. The documented path is /var/lib/docker/volumes/maven_sockets/_data/mavend.sock. On this box /var/lib/docker is drwx--x--- root root, so kami gets EACCES before reaching the socket. The socket itself is 0600 owned by uid 10001, from the Dockerfile's useradd -r -u 10001 maven. So every apply stops at the preflight with ErrUnhealthyBefore. The message blames the daemon for not answering, when the cause is a permission error on the dial.

Running it as root does work, and that is the worse outcome. Verify runs make build and make test in SourceDir as root, which leaves root-owned binaries, object files and a root-owned build cache in his working tree. The next non-root make fails, so a single root apply breaks the ordinary build. Bind-mount the socket to a host path he owns and document that, or drop privileges for the verify step. At minimum, separate the dial error from the read error at preflight and say "cannot open the socket".

3. A cold-start locked daemon makes a good update look like the manual-recovery case. main.go documents the locked mode: with a passkey enrolled and no env key, mavend starts locked and every CoreAPI method returns errLocked until an assertion arrives. DialHealth calls Presence, which is a CoreAPI method. Preflight passes because the running daemon is already unlocked. After the restart she comes up locked, waitHealthy fails for 90s, the rollback restores, restarts, and she comes up locked again. The result is ErrRollbackFailed and "SHE IS PROBABLY DOWN" for an update that was fine. He now has a daemon waiting for a passkey, and a tool telling him to copy files by hand.

The deployed config uses db_key.env today, so this is latent. It fires on the day he removes the env key, which the main.go comment describes as the intended end state. The health check needs a liveness signal a locked daemon can answer. Otherwise Apply has to refuse a deployment that boots locked.

4. mavend does not import internal/update is no longer true. It is stated three times: in the package comment, in the cmd/mavupdate comment, and beside the Update field. This PR adds "github.com/kami/maven/internal/update" to the import block of internal/config/config.go and calls c.Update.Validate() from validate(). mavend imports internal/config, so every mavend build links the package. The property that matters still holds, since there is no method, route, timer or caller. The proof offered for it does not. Validating the block from config is worth keeping. Reword the claim to what is enforced: mavend never constructs an Updater, and nothing in the daemon can call Apply.

Smaller notes:

  • The verify-failure path sets res.RolledBack = true when nothing was installed and nothing was restarted. summarize then prints rolled_back=true for a plain compile error, and cmdApply falls to the default branch, so he sees a rollback flag with no rollback message. Leave the flag false and let the log line carry it.
  • The package comment gives the order as health-check, build, test, snapshot, install. Apply snapshots before Verify, and the comment in apply.go explains at length why it must. The two comments contradict each other, and the one that is wrong is the one someone reads first.
  • Rollback restores ConfigFiles over InstallDir. The Config doc says config is "Snapshotted, never overwritten by an install", which is true of install and not of the restore. A rollback silently reverts any config edit made since the last apply. That includes a phraser.model_path change, which is how the resident model gets swapped.
  • mavupdate is now built by make build and is absent from the README's binaries list. The updater is the one artifact never snapshotted and never installed. A rollback leaves the new mavupdate in place against restored binaries.
  • Validate checks SnapshotDir against InstallDir but not against SourceDir. With the split layout, a snapshot dir under the source tree lands inside the docker build context. It also lands inside whatever make and git do there.
  • waitHealthy tests the deadline only after an attempt, and each attempt gets its own 10s budget. With a 90s timeout the last attempt can start at 89s and run to 99s. Cap the attempt at the remaining time.
  • Nothing quiesces the daemon before the restart. PR 68 added a whole drain for a model swap. An update kills the same process mid-turn with no drain at all. One line in the package comment saying an update is not turn-safe would settle it.
  • cmdRollback dies with the bare error on ErrRollbackFailed. cmdApply prints the loud "SHE IS PROBABLY DOWN" paragraph for the same condition. The standalone rollback is the path he reaches for when something is already wrong, so it needs that text more.
  • tail slices bytes, so a truncated make test log can start with half a rune. Russian test names and fixture strings will show it.
  • The Config comment shows restart_cmd ending in "mavend" and the README example omits it, so the documented command restarts every service in the compose file.
The refusals in the package comment are the best part of this PR, and they are enforced rather than described. No timer, no IPC method, no web route, no fetch of code from anywhere. Snapshotting before the build, with the reason spelled out, is the non-obvious ordering and it is the correct one. The preflight health check is right, and for the reason given. Without a baseline, a failed update and an already broken box are the same picture. `context.WithoutCancel` around the rollback is the detail that makes a Ctrl-C during the health wait safe. Copies rather than hardlinks or a git stash, re-hashed on the way back out. A restore is checked, not hoped for. Four things. **1. On the documented deployment the rollback restores bytes that nothing reads.** The README config sets `source_dir` and `install_dir` to the repo and `restart_cmd` to `docker compose up -d --build`. Follow it through. `install` is a no-op because the two dirs match. The restart rebuilds the image, and this repo's `Dockerfile` copies `cmd/` and `internal/` and runs `go build` inside the builder stage. It never copies a host binary. `.dockerignore` excludes the built binaries by name, with the comment "rebuilt inside the image". So what gets deployed is the source tree, which `Apply` never touches and `Restore` never reverts. The failure case is the one this package exists for. He pulls a bad commit and runs `apply -yes`. Build and test pass, the restart builds an image from the bad source, she does not answer, `waitHealthy` burns 120s. The rollback copies the old binaries into the tree and runs the same restart command. That rebuilds the same image from the same bad source. She does not answer again. It returns `ErrRollbackFailed` with "SHE IS PROBABLY DOWN" and an instruction to copy files back by hand. Copying them back by hand would not have helped either. The box stays down for two health timeouts plus two image builds. The only recovery is a `git checkout` the operator has to work out himself. The one part of the restore that does reach the running system is `deploy/mavend.json`, because compose bind-mounts it read-only from the tree. That is the file the `Config` comment says an update never replaces. The snapshot needs to cover whatever the restart command deploys. For an image built from source that means the commit, and `gitHead` is already recorded. So either record and restore the tree state for that deployment shape, or refuse that config outright. A README that sends him to a two-stage build-then-copy layout would be the honest alternative. **2. The health socket in the README cannot be opened by the account the README tells him to use.** The documented path is `/var/lib/docker/volumes/maven_sockets/_data/mavend.sock`. On this box `/var/lib/docker` is `drwx--x--- root root`, so `kami` gets EACCES before reaching the socket. The socket itself is 0600 owned by uid 10001, from the Dockerfile's `useradd -r -u 10001 maven`. So every `apply` stops at the preflight with `ErrUnhealthyBefore`. The message blames the daemon for not answering, when the cause is a permission error on the dial. Running it as root does work, and that is the worse outcome. `Verify` runs `make build` and `make test` in `SourceDir` as root, which leaves root-owned binaries, object files and a root-owned build cache in his working tree. The next non-root `make` fails, so a single root `apply` breaks the ordinary build. Bind-mount the socket to a host path he owns and document that, or drop privileges for the verify step. At minimum, separate the dial error from the read error at preflight and say "cannot open the socket". **3. A cold-start locked daemon makes a good update look like the manual-recovery case.** `main.go` documents the locked mode: with a passkey enrolled and no env key, mavend starts locked and every CoreAPI method returns `errLocked` until an assertion arrives. `DialHealth` calls `Presence`, which is a CoreAPI method. Preflight passes because the running daemon is already unlocked. After the restart she comes up locked, `waitHealthy` fails for 90s, the rollback restores, restarts, and she comes up locked again. The result is `ErrRollbackFailed` and "SHE IS PROBABLY DOWN" for an update that was fine. He now has a daemon waiting for a passkey, and a tool telling him to copy files by hand. The deployed config uses `db_key.env` today, so this is latent. It fires on the day he removes the env key, which the `main.go` comment describes as the intended end state. The health check needs a liveness signal a locked daemon can answer. Otherwise `Apply` has to refuse a deployment that boots locked. **4. `mavend does not import internal/update` is no longer true.** It is stated three times: in the package comment, in the `cmd/mavupdate` comment, and beside the `Update` field. This PR adds `"github.com/kami/maven/internal/update"` to the import block of `internal/config/config.go` and calls `c.Update.Validate()` from `validate()`. mavend imports `internal/config`, so every mavend build links the package. The property that matters still holds, since there is no method, route, timer or caller. The proof offered for it does not. Validating the block from config is worth keeping. Reword the claim to what is enforced: mavend never constructs an `Updater`, and nothing in the daemon can call `Apply`. Smaller notes: - The verify-failure path sets `res.RolledBack = true` when nothing was installed and nothing was restarted. `summarize` then prints `rolled_back=true` for a plain compile error, and `cmdApply` falls to the `default` branch, so he sees a rollback flag with no rollback message. Leave the flag false and let the log line carry it. - The package comment gives the order as health-check, build, test, snapshot, install. `Apply` snapshots before `Verify`, and the comment in `apply.go` explains at length why it must. The two comments contradict each other, and the one that is wrong is the one someone reads first. - `Rollback` restores `ConfigFiles` over `InstallDir`. The `Config` doc says config is "Snapshotted, never overwritten by an install", which is true of `install` and not of the restore. A rollback silently reverts any config edit made since the last apply. That includes a `phraser.model_path` change, which is how the resident model gets swapped. - `mavupdate` is now built by `make build` and is absent from the README's `binaries` list. The updater is the one artifact never snapshotted and never installed. A rollback leaves the new `mavupdate` in place against restored binaries. - `Validate` checks `SnapshotDir` against `InstallDir` but not against `SourceDir`. With the split layout, a snapshot dir under the source tree lands inside the docker build context. It also lands inside whatever `make` and git do there. - `waitHealthy` tests the deadline only after an attempt, and each attempt gets its own 10s budget. With a 90s timeout the last attempt can start at 89s and run to 99s. Cap the attempt at the remaining time. - Nothing quiesces the daemon before the restart. PR 68 added a whole drain for a model swap. An update kills the same process mid-turn with no drain at all. One line in the package comment saying an update is not turn-safe would settle it. - `cmdRollback` dies with the bare error on `ErrRollbackFailed`. `cmdApply` prints the loud "SHE IS PROBABLY DOWN" paragraph for the same condition. The standalone rollback is the path he reaches for when something is already wrong, so it needs that text more. - `tail` slices bytes, so a truncated `make test` log can start with half a rune. Russian test names and fixture strings will show it. - The `Config` comment shows `restart_cmd` ending in `"mavend"` and the README example omits it, so the documented command restarts every service in the compose file.
kami closed this pull request 2026-08-01 14:51:54 +02:00
Owner

Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.

Landed on master. The stack was one linear chain, so #84 carried every commit from #50 up, and master now contains this branch in full. Merging this PR on its own is an empty diff, so it is closed rather than merged. The review findings for it were fixed in the 2026-08-01 pass and are on master as commits on the stack tip, not on this branch.

Pull request closed

Sign in to join this conversation.
No Reviewers
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: kami/Maven#69