- Go 95.4%
- Shell 4.6%
| Filename | Latest commit message | Latest commit date |
|---|---|---|
| .github | ||
| cmd/sc | ||
| hack | ||
| internal | ||
| scripts | ||
| vendor | ||
| .gitignore | ||
| .golangci.yaml | ||
| .goreleaser.yaml | ||
| coverage.floor | ||
| go.mod | ||
| go.sum | ||
| README.md | ||
sc — someones.computer deploy agent
A single static Go binary you run inside your project. It reads your local
docker-compose.yml, packages every build: context into a compressed tarball,
and uploads the compose file + contexts to
someones.computer as a new immutable
Deployment. The platform builds the contexts into images and reconciles them onto
Docker Swarm.
Status: early.
loginstores a token anddeploypackages the compose project + build contexts and POSTs them to/applications/{slug}/bundle, which the platform now accepts (contexts are cached and apendingDeployment is recorded).apps,logsand thestart/stoptoggles are in — seedocs/agent-cli.mdin the platform repo for the wire contract.
Install
curl -fsSL https://someones.computer/install.sh | sh
The platform serves the binaries, not this repo: every build is mirrored into
someones.computer's object store, and https://someones.computer/install lists
the archives and their checksums for every platform (macOS, Linux and Windows ×
amd64/arm64). The script picks the right one, verifies it against the checksum
published with it, and drops it in /usr/local/bin — or ~/.local/bin when that
needs root, which it will not ask for.
Two channels: stable is the latest v* tag, dev is the last push to
main. ?channel=dev on either URL switches. See
Releasing for what publishes them.
Updating
sc -U # or --update, or `sc update`
sc replaces itself with the newest build on its own channel, and the
channel is not a choice: a tagged build follows stable, a snapshot follows dev,
and the version stamped into the binary says which it is. There is no flag and
no config file for it.
The download is verified against the platform's published sha256 before anything
on disk is touched, and every failure — a bad checksum, a lost connection, an
unpackable archive — leaves the working binary exactly where it was. An install
directory this user cannot write is reported with the command to run by hand;
sc never escalates, for the same reason install.sh never does.
An unreleased local build (go build, version dev) belongs to neither channel
and is refused rather than overwritten.
Build & use
go build -o sc ./cmd/sc
# Store a token for the endpoint (prints the page to mint one at, and offers
# to open it):
./sc login
# In a git checkout, that's all the configuration there is — the application
# is named after the repository directory and the deployment after the branch:
./sc deploy --dry-run # inspect what would be shipped, upload nothing
./sc deploy # upload a Deployment
# Or name them explicitly:
./sc deploy --app my-app --deployment staging
# Who is this endpoint signed in as, and with which token:
./sc whoami
# See what's out there — organizations, applications, deployments and the
# state each one is in:
./sc apps # the whole tree
./sc apps staging # only what matches, with its ancestors
# Switch a deployment off and back on. Nothing is uploaded and nothing is
# rebuilt — the same images go back up:
./sc stop # tear down what this branch has running
./sc start # put it back, following it until it's running
./sc start staging -d # another deployment, without waiting
# Follow a running service's output. Ctrl-C to stop:
./sc logs web
./sc logs web --tail 0 # from now, replaying nothing
Following logs
sc logs <service> streams one service's stdout and stderr from the platform as
it happens. The service is the name it has in your compose file; the application
resolves exactly as deploy resolves it (--app, then .sc.yml, then the
directory), so the two always mean the same application.
Log lines go to stdout and everything else to stderr, so redirecting collects the log and nothing else:
sc logs web > web.log # only log lines land in the file
--tail N replays that much history before following (200 by default; 0
starts from now). The platform caps how many streams it holds open at once — a
full pool answers immediately with how long to wait rather than queueing you
behind somebody else's terminal.
Before it uploads
deploy checks the target first, so the two things a detected name can get
wrong become questions rather than a failed upload:
$ sc deploy
No application "example-app" on https://someones.computer.
Create application "example-app" in "acme"? [Y/n] y
Created example-app — https://someones.computer/applications/019f…
$ sc deploy
"example-app" has no deployment called "feature-ui" yet (it has: main).
Start a new deployment "feature-ui"? [Y/n]
The second one matters because the default shape is one deployment per branch: without it, a branch you didn't mean to deploy from quietly starts a second line of revisions. A first-ever deployment isn't asked about — there is nothing for it to be a departure from.
Answers come from stdin, so printf 'y\n' | sc deploy works the same as typing
it. Nothing to read from (CI, a closed stdin) is not a hang: it's an error that
names the flag which would have answered — --yes accepts every prompt, and
--org picks the organization up front when you belong to several.
Configuration
Every parameter can be set four ways, and they resolve in this order:
flag > environment > .sc.yml > detected default
Which platform it talks to
--endpoint names the platform, and --staging is a shorthand for the hosted
staging one. It is a persistent flag, so it applies to every subcommand:
./sc --staging apps # list what's on staging
./sc deploy --staging # deploy there
./sc login --staging # and its own token — see below
The endpoint resolves highest-first:
| Precedence | Source | Value |
|---|---|---|
| 1 (wins) | --endpoint |
as given |
| 2 | --staging |
https://staging.someones.computer |
| 3 | $SC_ENDPOINT |
as given |
| 4 | endpoint: in .sc.yml |
as given |
| 5 | built-in default | https://someones.computer |
--staging deliberately outranks $SC_ENDPOINT and the project file: a repo
pinned to some other endpoint must not quietly pull a --staging deploy back
off staging. Passing --staging and --endpoint together is an error
rather than a precedence puzzle — for a flag whose job is "not production",
silently picking a winner is the failure mode worth designing out.
Tokens are stored per endpoint, so staging needs its own
sc login --staging. Without one, adeploy --stagingprompts for a token inline rather than reusing the production one.
The deploy summary names the resolved endpoint, so --dry-run answers
"where is this about to go?" before anything uploads.
.sc.yml
The per-project file. sc reads the nearest one, searching upwards from the
working directory, so it works from a subdirectory of the repo; --config <path> points at a specific file instead. .sc.yaml is accepted too (having
both in one directory is an error, not a coin toss). Every key is a flag name,
and unknown keys are rejected rather than ignored — a typo should fail loudly,
not deploy somewhere unexpected.
app: example-app # --app
deployment: staging # --deployment
endpoint: http://127.0.0.1:7500
token: someonescomputer_… # see the warning below
files: # -f/--file; `file:` is accepted as an alias,
- compose.yaml # and a bare string works instead of a list
- compose.override.yaml
dry-run: false # --dry-run
no-browser: false # --no-browser
Relative files entries resolve against the file's own directory, not the
working directory.
A
token:in.sc.ymlsits inside the build context, so it ships with the upload unless.dockerignoreexcludes it.scwarns when it uses one. Prefer$SC_TOKENor thesc loginstore, and keep the file out of the context if you do commit a token to it.
Detected defaults
With nothing configured, sc infers from the checkout — this is what makes a
bare sc deploy work:
| Parameter | Detected from | Example |
|---|---|---|
app |
the git repository's directory name, slugified | ~/code/My Cool App → my-cool-app |
deployment |
the current branch, slugified | feature/New UI → feature-new-ui |
The repository root is used, so running from services/api/ gives the same
answer as running from the root. Outside a checkout the working directory's name
is used and there is no deployment name; on a detached HEAD there is no
deployment name either (a commit id makes a poor one). Git is read directly —
sc is a static binary and doesn't need git on PATH.
| Environment variable | Effect |
|---|---|
SC_ENDPOINT |
default for --endpoint; --staging overrides it |
SC_TOKEN |
token for non-interactive runs (CI) |
SC_CONFIG_HOME / XDG_CONFIG_HOME |
where credentials.json lives |
Authentication
Every upload is attributed to the user the token belongs to — nothing is
anonymous. sc looks for a token in this order:
| Source | Use |
|---|---|
--token |
one-off override |
$SC_TOKEN |
CI |
token: in .sc.yml |
a project pinned to one endpoint (see the warning above) |
sc login store |
interactive machines |
sc login prints the platform's Settings → API tokens page
(<endpoint>/settings/api-tokens?label=<hostname>+(sc)), offers to open it in a
browser, and reads the pasted token without echoing it. The label is a
suggestion the page prefills its form with — this machine's short hostname, which
is the one thing the browser cannot work out — so the token names the box it
lives on rather than being "token 3" when someone comes to revoke it. A hostname
of localhost, or none at all, suggests nothing.
Tokens are stored per endpoint in
$SC_CONFIG_HOME/$XDG_CONFIG_HOME/sc/~/.config/sc/credentials.json, mode
0600, so a local stack and the hosted platform can each have their own. A
deploy with no token configured runs the same prompt inline instead of failing.
./sc login --staging # the hosted staging platform
./sc login --endpoint http://127.0.0.1:7500 # a local stack
./sc login --no-browser # just print the URL
echo "$TOKEN" | ./sc login # non-interactive
deploy auto-discovers compose.yaml/docker-compose.yml (override with -f),
parses it with compose-go, and packs
each build: context into a deterministic, .dockerignore-aware .tar.gz.
While the upload is in flight, sc tumbles a die between a green S and a red
C — the pips are the six dots of a braille cell:
[S⣦C] Uploading 1.4 MiB — 52% bytes moving: an 8-step gauge
[S⠙C] Waiting for someones.computer nothing to measure: ⠙⠸⠴⠦⠇⠋ tumbling
[S·C] Uploading 1.4 MiB — 52% stalled 2s: a resting mark, brackets breathing
[S⠿C] Uploaded 1.4 MiB in 3.2s done: the die landed, and this line stays
It only animates on a terminal; piped or in CI the label prints once and only
the closing summary follows. $NO_COLOR drops the colour and keeps the
animation. The gauge and the resting state need per-byte progress from the
client, which isn't wired yet — see agent-cli.
CI/CD
.github/workflows/ci.yaml— on every push tomainand every PR:go vet,go test, and a static cross-compile acrosslinux,darwin,windows×amd64,arm64(CGO_ENABLED=0)..github/workflows/release.yaml— on av*tag: GoReleaser builds the same matrix as static, stripped binaries (-s -w -trimpath), archives them (.tar.gz,.zipon Windows) with achecksums.txt, and publishes a release to this repo via the Forgejo/Gitea API..github/workflows/release-cadence.yml— cuts thatv*tag itself, nightly, at the newest commit onmainthatci.yamlreported green, sostable(what someones.computer's/installserves by default) cannot silently fall behind the way it has before — see the workflow's own header for why this exists and what it checks before tagging.
The version number is the platform's, not this repo's. someones.computer
is one product with two shipped artifacts — the web app and this CLI — and
they share one version between them: whatever v* is newest on
Grey.ooo/someones.computer
is what gets tagged here too, at whatever commit on this repo's own main is
current when it catches up. A platform-only release cycle still gets a
matching tag here, even with no CLI change behind it — sc --version and the
app's own version should never disagree about what "someones.computer vX.Y.Z"
means.
Releases are cut automatically. To cut one sooner:
scripts/cut-release.sh
scripts/cut-release.sh --dry-run
— or trigger release-cadence.yml's workflow_dispatch from the Forgejo UI. Both
apply the same guards the nightly run does (previous tag actually released, next
tag's commit actually green, forward-only) rather than a bare git tag && git push.
Requirements for the pipeline to run
- A Forgejo Actions runner registered to this repo (or the
Grey.oooorg) exposing theubuntu-latestlabel. - The Actions unit enabled on the repo (Settings → Units).
- The release job uses the auto-injected
GITHUB_TOKEN(repo-scoped) asGITEA_TOKEN; no manual secret needed unless you publish cross-repo.
Releasing
.github/workflows/release.yaml runs goreleaser on every push to main and on
every v* tag, and both ends up somewhere a user can reach:
| Trigger | goreleaser | Forgejo release | someones.computer channel |
|---|---|---|---|
push to main |
--snapshot |
none | dev → 0.0.1-snapshot-<sha> |
v* tag |
release --clean |
yes, on this repo | stable → the tag |
There is deliberately no rolling dev tag: a snapshot version already names
the commit it was built from, so a moving tag would add nothing and force-push
something.
The mirror is scripts/publish-s3.sh — six PUTs signed
with curl --aws-sigv4, no AWS CLI to install. It writes the archives and
goreleaser's checksums.txt under releases/<version>/, then moves
channels/<channel> last, so a half-finished upload leaves the channel on the
previous version instead of on a set of 404s. The platform reads exactly those
keys (docs/agent-cli.md in the platform repo).
The bucket, sc-builder, is this pipeline's alone, and the key below can reach
nothing else on that object store — the platform's own keys have read on it
and nothing more. Garage grants permissions per bucket and no finer, so sharing
the platform's data bucket would have handed this repo's CI write access to every
stored build context, which the build worker feeds to kaniko by object key.
Five settings on this repo, all configured; absent, the release still cuts and the mirror step warns, so a fork can tag one without the platform's credentials:
| Variables | SC_S3_ENDPOINT |
https://s3.grey.ooo |
SC_S3_REGION |
us-east-1 — s3.grey.ooo reports this; signing with the wrong region is rejected outright |
|
SC_S3_BUCKET |
sc-builder |
|
| Secrets | SC_S3_ACCESS_KEY / SC_S3_SECRET_KEY |
the sc-builder-only Garage key |
Layout
cmd/sc/ entry point
internal/cli/ cobra command tree (root, version, login, deploy, apps, logs,
update)
internal/config/ per-endpoint token store (0600 credentials.json) + .sc.yml
internal/detect/ app name from the repo directory, deployment from the branch
internal/bundle/ compose-go loading + deterministic context packing
internal/client/ multipart upload to /applications/{slug}/bundle, plus the
/cli/* JSON API (preflight, create application, list) and the
newline-delimited log stream
internal/ui/ transient terminal output (the [S⠙C] wait spinner)
internal/selfupdate/
sc -U: channel from the embedded version, /install/{channel}.json,
checksum verification, atomic replacement
scripts/ publish-s3.sh — mirrors a build to someones.computer
cut-release.sh — by-hand equivalent of release-cadence.yml
.github/ CI + release workflows
.goreleaser.yaml static multi-arch build matrix
Gates
Same shape as the platform repo's, in Go terms — one definition each, so a laptop and CI cannot drift:
| Check | Command | Is |
|---|---|---|
| Static analysis | hack/lint.sh |
golangci-lint from a pinned image: staticcheck, govet, errcheck, unused, plus bodyclose, errorlint, gosec, gocritic, misspell. Configured in .golangci.yaml, no baseline — the gate is "no findings" |
| Coverage | hack/coverage.sh |
a ratchet: every package's figure is recorded in coverage.floor, and a drop fails. --bump raises them |
| Tests | go test ./... |
run by the coverage gate, so it is not a separate step |
Those three files are duplicated verbatim in someones.computer_scclient and
someones.computer_tray, and are meant to stay identical.
Why a ratchet and not 100%. The platform repo gates PHP at 100% because
everything there is injectable. Go here is not — a main(), an exec path, a
terminal draw. A fixed floor is a number nobody raises, which lets a package
fall from 92% to it unnoticed; a ratchet only turns one way, and lowering a
figure has to be argued for in the commit that does it.