Migrating to v0.49.0
Five surfaces are cut in this release, each a hard cut with no shim: the retired spelling fails naming its replacement. The sections below are independent, so apply the ones you use in any order.
Unified update
Use the top-level update command for both targets:
| Previous command | Replacement |
|---|---|
sparkwing version update --cli | sparkwing update --cli |
sparkwing version update --sdk | sparkwing update --sdk |
Bare sparkwing update selects the CLI. --cli and --sdk conflict;
--force and --override-hold are CLI-only. --version selects a canonical
release tag for either target. Both targets default to the latest published
GitHub release. SDK updates now pass that resolved tag to native go get,
then run go mod tidy, retaining Go's toolchain and module verification rules.
update --check performs HTTP metadata reads and local identity inspection.
It never invokes Go, installs a binary, changes the project pin, or writes
tool caches. Explicit tags must identify published releases. Clean CLI commit
provenance must match its stamped release tag before version comparisons are
reported as known; local builds and SDK replacements produce unknown.
Checks do not verify release asset signatures or guarantee installation.
JSON checks emit one update_check record with tool, target, strategy,
status, installed, and available. Unknown identity fields are omitted;
reason and blocked_reason explain uncertainty or operator holds. Exit 0
means current or ahead, 1 means update_available, and 2 means unknown,
diverged, or lookup failure. Plain checks print the status word.
Successful updates emit a compact update receipt with before and after
identities. CLI artifact metadata is read without executing the new binary;
resolved_release retains the verified release label when embedded version
metadata cannot be established. Progress goes to stderr. JSON is the pipe default, pretty output
is the terminal default, and --output pretty|json|plain overrides it.
Plain updates print the resulting version. The rich version report and
version hold remain available. The version card keeps its layout and adds
cli_status / cli_reason to its JSON report. Its CLI verdict uses the same
comparison facts as update --check; --offline reports not_checked without
network access. The entire metadata lookup chain shares one three-second budget.
The version card describes the invoking process. Update checks and receipts
inspect the destination on disk, which may already contain a replacement build.
SDK module inspection accepts regular files up to 1 MiB and reports unsafe or
oversized inputs as unknown.
SDK receipts report updated whenever native go get and go mod tidy ran,
even if the SDK pin stayed the same, because other dependency files can change.
Unreadable, unsafe, or oversized operator-hold files now report an error and
refuse updates instead of appearing unset. A missing file still means no hold;
a nonempty environment hold keeps precedence without requiring a home path.
Serve command
The local dashboard and API lifecycle now lives under sparkwing serve.
Update shell scripts, shortcuts, and runbooks to use these commands:
| Previous command | Replacement |
|---|---|
sparkwing dashboard start | sparkwing serve start |
sparkwing dashboard status | sparkwing serve status |
sparkwing dashboard kill | sparkwing serve stop |
sparkwing dashboard stop | sparkwing serve stop |
The retired dashboard command fails with a replacement instruction and
performs no service action. serve takes start, stop, restart, logs
and status; there is no serve kill.
serve start leaves a running service alone and reports it, where dashboard start replaced whatever it found. A script that restarted the dashboard by
calling start again calls serve restart. Lifecycle receipts name the owner,
the effective endpoints, readiness, and the artifact the service runs, and
serve logs reads a bounded window of the service log.
The dashboard UI, HTTP routes, JSON service identity dashboard, and
dashboard.pid / dashboard.log state paths keep their names, so serve
addresses the same local service and state.
Lint slots removed
sparkwing.AcquireLintSlot, the LintSlot type, its Configure and
ConfigureIn methods, and SPARKWING_LINT_SLOTS are gone. Give each worktree
its own linter cache with ToolCacheDir instead.
Before:
slot, err := sparkwing.AcquireLintSlot("golangci-lint")
if err != nil {
return err
}
defer slot.Release()
cmd := sparkwing.Bash(ctx, "golangci-lint run --allow-serial-runners ./...")
_, err = slot.Configure(cmd, "GOLANGCI_LINT_CACHE").Run()
After:
_, err := sparkwing.Bash(ctx, "golangci-lint run --allow-serial-runners ./...").
Env("GOLANGCI_LINT_CACHE", sparkwing.ToolCacheDir("golangci-lint")).
Run()
A multi-module repository sets Dir per module and hands each invocation the
same worktree-scoped cache.
Why the slot could not be kept
A slot lent every worktree one alias path -- a symlink repointed at whichever
worktree held the lease -- so that a shared cache's stored absolute paths kept
resolving. git resolves that alias and reports the real worktree as the
repository root, so every finding the linter recorded under the alias sat
outside the diff that a baseline such as golangci-lint's new-from-merge-base
matches against, and the baseline filter dropped all of them. A tree with eight
findings linted clean in three seconds. The alias breaks the filter whether or
not the cache is shared, so keying the cache by tree would not have restored
the findings.
A worktree therefore starts its linter cold, which is the cost of a gate that reports what is in the tree.
Queue exec removed
sparkwing queue exec is gone. It ran one command under a lease from the local
admission daemon, and the daemon kept that lease alive across a lost connection
until the command's process session was proven empty. Nothing in the pipeline
path used it.
Run the work as a pipeline instead. A pipeline run takes admission the same way,
appears in sparkwing queue, and gets the retries, logging, and cancellation a
bare command never had:
# Before
sparkwing queue exec --run-id build-123 --name bootstrap --cores 1 \
--semaphore bootstrap -- make prepare
# After: a pipeline job that runs `make prepare`, with the same charge
sparkwing run bootstrap
Declare the charge and the shared lock in the pipeline's plan:
plan.Resources(sparkwing.Cores(1)) for the charge, and a
sparkwing.NewConcurrencyGroup("bootstrap", ...) enrolled with
plan.Concurrency(group) for the lock. The orchestrator turns the group into
the same admission claim the --semaphore flag used to send, so the daemon
arbitrates the run exactly as it arbitrated the command.
What left the wire
The messages guard_complete and guard_complete_ack, and the
admission_request.guard field, are removed from the protocol. The daemon still
speaks protocol major 3 and still serves every major from 1 up, so a pipeline
binary pinned to any released SDK keeps its admission: no SDK ever sent these.
A sparkwing CLI older than this release that runs queue exec against a newer
daemon is admitted without the guard, runs the command to completion, and then
exits non-zero naming the operation the daemon no longer serves: it sends
guard_complete only once the command has finished. The work is done and the
exit code says otherwise, so replace the call rather than relying on it.
Isolated home removed
sparkwing run --sw-isolated-home DIR is gone. It pointed one run's state and
config at DIR, so the run hosted an admission daemon of its own instead of
joining the machine's. A run that leaves the machine's daemon is arbitrated by
nobody: it does not appear in sparkwing queue or the dashboard, and several
such runs contend on the operating system instead of queueing. A command that
still passes the flag fails naming the replacement.
The flag existed for one case, a pipeline binary whose runs-store schema is newer than the sparkwing hosting the machine's daemon. Admission now refuses that case with a message naming the daemon's version, this binary's version, how to identify the build the daemon runs, and the upgrade:
local admission: the admission daemon cannot read this runs store: daemon
v0.48.1 understands runs-store schema 33, this binary is v0.49.0 at schema 34,
and the store both share is migrated to the newer one. `sparkwing daemon
status` names the build the daemon runs, and the `sparkwing` found on PATH
resolves to ~/.local/bin/sparkwing; upgrade that binary to one that
understands schema 34, with `sparkwing update` for a published release or this
repository's `bin/install.sh` for a build newer than any release, then
`sparkwing daemon restart`, or point SPARKWING_WINGD_BIN at a binary that
understands schema 34 and restart the daemon
Which half of the upgrade applies depends on where the newer schema came from.
sparkwing update installs a published release. A branch whose schema is newer
than every release has no release to install, so build and install that branch
instead, or point SPARKWING_WINGD_BIN at the binary you built and restart the
daemon.
Before:
sparkwing run pre-commit --sw-isolated-home "$(mktemp -d)"
After:
sparkwing update && sparkwing daemon restart # a published release
sparkwing run pre-commit
SPARKWING_HOME still gives a command a home of its own, and keeps that
meaning: deliberate isolation for work that must not touch the operational
runs store, such as the release preview in
Getting started. It carries the
same consequence the flag did, so a run started under it is outside the
machine's admission ledger.
Dashboard lifecycle
serve stop replaces serve kill; the old verb is removed. Repeated serve start preserves every known running instance. Use serve restart for replacement; it preserves the recorded options unless a flag overrides them. serve logs provides a finite 40-line tail and explicit following.
Receipts add action/outcome, ownership, readiness, effective endpoints and artifact comparison. Plain is the state word across all lifecycle actions. Linux stop uses boot/birth identity and a process handle; macOS uses guarded birth checks with a remaining check-to-signal race. Other platforms refuse verified lifecycle actions. Legacy numeric PID files cannot authorize signals. Stop the legacy instance through its existing owner before starting the new supervisor. The admission daemon remains separate.