be066a4b04
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
66 lines
2.3 KiB
Go
66 lines
2.3 KiB
Go
package update
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// Verification is "does this tree build and does it pass its own tests", run
|
|
// before a single byte is written to the install dir.
|
|
//
|
|
// It is `make build` and `make test`, not `go build`: the CGO daemons need the
|
|
// vendored toolchain and the whisper/piper include and library paths wired
|
|
// through the Makefile, and a bare `go build` on them fails in a way that has
|
|
// nothing to do with the change being deployed. `make test` is the -race suite
|
|
// with the CGO env set, and it is the only evidence available on a single box
|
|
// that the new code does what the old code did.
|
|
//
|
|
// This is not a substitute for a second environment. A test suite that passes
|
|
// says the code is self-consistent; it does not say the new build will start
|
|
// against this machine's actual models, sockets and encrypted store. That is
|
|
// what the post-restart health check is for, and it is why the install is
|
|
// reversible rather than merely careful.
|
|
|
|
// Step — one verification or orchestration step and how it went. Kept so the CLI
|
|
// can print a truthful account of what was done, including on the failure path.
|
|
type Step struct {
|
|
Name string
|
|
Argv []string
|
|
Took time.Duration
|
|
Err error
|
|
Output string // combined output, only retained for failures
|
|
}
|
|
|
|
// Verify runs the build and the test suite in SourceDir.
|
|
func (u *Updater) Verify(ctx context.Context) ([]Step, error) {
|
|
ctx, cancel := context.WithTimeout(ctx, u.cfg.verifyTimeout())
|
|
defer cancel()
|
|
var steps []Step
|
|
for _, argv := range [][]string{{"make", "build"}, {"make", "test"}} {
|
|
u.log("verify: %v (this takes a while)", argv)
|
|
start := u.now()
|
|
out, err := u.run(ctx, u.cfg.SourceDir, argv)
|
|
st := Step{Name: argv[len(argv)-1], Argv: argv, Took: u.now().Sub(start), Err: err}
|
|
if err != nil {
|
|
st.Output = tail(out, 4000)
|
|
}
|
|
steps = append(steps, st)
|
|
if err != nil {
|
|
u.log("verify: %v FAILED after %s", argv, st.Took.Round(time.Second))
|
|
return steps, fmt.Errorf("%w: %v: %v", ErrVerifyFailed, argv, err)
|
|
}
|
|
u.log("verify: %v ok in %s", argv, st.Took.Round(time.Second))
|
|
}
|
|
return steps, nil
|
|
}
|
|
|
|
// tail keeps the last n bytes — a failing `make test` prints far more than is
|
|
// useful, and the failure is always at the end.
|
|
func tail(s string, n int) string {
|
|
if len(s) <= n {
|
|
return s
|
|
}
|
|
return "…" + s[len(s)-n:]
|
|
}
|