Let sc replace itself with the newest build on its own channel #18

Merged
geusebio merged 1 commit from feature/self-update into main 2026-08-07 22:41:41 +00:00
Owner

No UI surface — this is a CLI command, so there is nothing to screenshot.

$ sc -U
Updating sc 0.0.1-snapshot-1a2b3c4 → 0.0.1-snapshot-ffffff (dev channel)
Installed sc 0.0.1-snapshot-ffffff to /home/you/.local/bin/sc

$ sc -U
sc 0.0.1-snapshot-ffffff is already the newest build on the dev channel.

What changed

sc -U — also --update, also sc update — reads the platform's manifest for
the channel it came from, downloads the archive for its own os/arch, verifies it
against the published sha256, and swaps itself out.

The channel needs no flag and no config file, because the build already
answers it. .goreleaser.yaml's snapshot version_template stamps -snapshot-
into every untagged build and never into a tagged one, so a version containing it
follows dev and anything else follows stable. The alternative was a fourth
-X ldflag carrying the channel, which both publish paths would have to set
correctly and which is empty on every binary already in the wild — a fallback to
guess at anyway.

Case Behaviour
Already current Success with a clear message, not a no-op and not an error
Checksum mismatch Hard failure, no retry, nothing replaced
Unreleased local build (version == "dev") Refused — it belongs to neither channel and has nothing to go back to
No build for this os/arch Error naming the platform
Install path not writable ErrNotWritable naming the path and printing the install.sh one-liner. Never sudo
Channel never published The server's own 404 sentence, surfaced as-is

Nothing unverified reaches the filesystem: the archive is held in memory
until its sha256 matches. The replacement stages the new binary in the install
directory, renames the old one aside, renames the new one in, and puts the old
one back if that second rename fails — so every failure path leaves a working
sc where it was. That is also the shape that works on Windows, where the
running image is locked but its directory entry can still be moved; a plain
rename onto the target would be fine on Unix and refused there.

-U is on the root command rather than persistent, so sc deploy -U is an error
rather than two requests in one line with no obvious order.

Depends on

GET /install/{channel}.json on the platform —
Someones.Computer#371.
This cannot work until that merges. The catalogue's other surfaces are a web
page, a shell script and a stream of bytes that never names its own version.

That endpoint deliberately does not fall back from an unpublished stable to
dev, and neither does this: a stable binary handed a dev snapshot is a silent
downgrade onto an unreleased build. There is a test for exactly that.

Verified

go vet ./..., go test ./... and the CI cross-compile matrix
(linux/darwin/windows × amd64/arm64) all green locally. 14 new tests drive the
whole path — resolve, fetch, verify, replace — against an httptest server and a
temp file, including the mismatched-checksum refusal, the unwritable directory,
the Windows zip, and the stable-is-not-offered-dev property.

What's left

Nothing here, but this stays draft until
Someones.Computer#371
merges — until then the endpoint it reads does not exist in production, so the
command is correct but unusable.

One thing is deliberately undecided and recorded on the issue rather than guessed
at here: whether -U should support Windows at all. The replacement code is
written to work there and archives for it are published, but no one has run it on
Windows.

Part of
Someones.Computer#237.

No UI surface — this is a CLI command, so there is nothing to screenshot. ```console $ sc -U Updating sc 0.0.1-snapshot-1a2b3c4 → 0.0.1-snapshot-ffffff (dev channel) Installed sc 0.0.1-snapshot-ffffff to /home/you/.local/bin/sc $ sc -U sc 0.0.1-snapshot-ffffff is already the newest build on the dev channel. ``` ## What changed `sc -U` — also `--update`, also `sc update` — reads the platform's manifest for the channel it came from, downloads the archive for its own os/arch, verifies it against the published sha256, and swaps itself out. **The channel needs no flag and no config file**, because the build already answers it. `.goreleaser.yaml`'s snapshot `version_template` stamps `-snapshot-` into every untagged build and never into a tagged one, so a version containing it follows `dev` and anything else follows `stable`. The alternative was a fourth `-X` ldflag carrying the channel, which both publish paths would have to set correctly and which is empty on every binary already in the wild — a fallback to guess at anyway. | Case | Behaviour | |---|---| | Already current | Success with a clear message, not a no-op and not an error | | Checksum mismatch | Hard failure, no retry, nothing replaced | | Unreleased local build (`version == "dev"`) | Refused — it belongs to neither channel and has nothing to go back to | | No build for this os/arch | Error naming the platform | | Install path not writable | `ErrNotWritable` naming the path and printing the `install.sh` one-liner. **Never `sudo`** | | Channel never published | The server's own 404 sentence, surfaced as-is | **Nothing unverified reaches the filesystem**: the archive is held in memory until its sha256 matches. The replacement stages the new binary in the install directory, renames the old one aside, renames the new one in, and puts the old one back if that second rename fails — so every failure path leaves a working `sc` where it was. That is also the shape that works on Windows, where the running image is locked but its directory entry can still be moved; a plain rename onto the target would be fine on Unix and refused there. `-U` is on the root command rather than persistent, so `sc deploy -U` is an error rather than two requests in one line with no obvious order. ## Depends on `GET /install/{channel}.json` on the platform — [Someones.Computer#371](https://git.grey.ooo/Grey.ooo/Someones.Computer/pulls/371). **This cannot work until that merges.** The catalogue's other surfaces are a web page, a shell script and a stream of bytes that never names its own version. That endpoint deliberately does not fall back from an unpublished `stable` to `dev`, and neither does this: a stable binary handed a dev snapshot is a silent downgrade onto an unreleased build. There is a test for exactly that. ## Verified `go vet ./...`, `go test ./...` and the CI cross-compile matrix (linux/darwin/windows × amd64/arm64) all green locally. 14 new tests drive the whole path — resolve, fetch, verify, replace — against an `httptest` server and a temp file, including the mismatched-checksum refusal, the unwritable directory, the Windows zip, and the stable-is-not-offered-dev property. ## What's left Nothing here, but this stays draft until [Someones.Computer#371](https://git.grey.ooo/Grey.ooo/Someones.Computer/pulls/371) merges — until then the endpoint it reads does not exist in production, so the command is correct but unusable. One thing is deliberately undecided and recorded on the issue rather than guessed at here: whether `-U` should support **Windows** at all. The replacement code is written to work there and archives for it are published, but no one has run it on Windows. Part of [Someones.Computer#237](https://git.grey.ooo/Grey.ooo/Someones.Computer/issues/237).
Let sc replace itself with the newest build on its own channel
All checks were successful
CI / build (pull_request) Successful in 5m4s
f6c49f1ea3
sc -U (also --update, also `sc update`) reads the platform's manifest for the
channel it came from, downloads the archive for its own os/arch, verifies it
against the published sha256 and swaps itself out.

The channel needs no flag and no config file, because the build already answers
it. .goreleaser.yaml's snapshot version_template stamps -snapshot- into every
untagged build and never into a tagged one, so a version containing it followed
dev and anything else followed stable. The alternative was a fourth -X ldflag
carrying the channel, which both publish paths would have to set correctly and
which is empty on every binary already in the wild — a fallback to guess at
anyway.

An unreleased local build reports version "dev", belongs to neither channel, and
is refused. That binary is somebody's own go build, possibly of uncommitted
work, with no version to compare against and nothing to go back to.

Nothing unverified reaches the filesystem: the archive is held in memory until
its sha256 matches, and a mismatch is a hard failure with no retry, because the
two things it can mean are corruption and substitution and retrying is the wrong
answer to both. The replacement stages the new binary in the install directory,
renames the old one aside, renames the new one in, and puts the old one back if
that second rename fails — so every failure path leaves a working sc where it
was. That is also the shape that works on Windows, where the running image is
locked but its directory entry can still be moved; a plain rename onto the
target would be fine on Unix and refused there.

An install directory this user cannot write is an ErrNotWritable naming the path
and printing the install.sh one-liner. sc does not escalate, for the same reason
install.sh does not: it picks ~/.local/bin over sudo, and a self-updater that
quietly acquired root would be a much larger promotion of privilege than the
original install ever asked for.

-U is on the root command rather than persistent, so `sc deploy -U` is an error
rather than two requests in one line with no obvious order.

Depends on GET /install/{channel}.json, added platform-side in the someones.
computer repo: the catalogue's other surfaces are a web page, a shell script and
a stream of bytes that never names its own version. That endpoint deliberately
does not fall back from an unpublished stable to dev, and neither does this — a
stable binary handed a dev snapshot is a silent downgrade onto an unreleased
build.

Part of Someones.Computer#237.
geusebio changed title from WIP: Let sc replace itself with the newest build on its own channel to Let sc replace itself with the newest build on its own channel 2026-08-07 22:41:36 +00:00
Sign in to join this conversation.
No reviewers
No labels
in-progress
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Grey.ooo/someones.computer_agent!18
No description provided.