# 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](https://git.grey.ooo/Someones.Computer) as a new immutable
Deployment. The platform builds the contexts into images and reconciles them onto
Docker Swarm.

> Status: **early.** `login` stores a token and `deploy` packages the compose
> project + build contexts and POSTs them to `/applications/{slug}/bundle`, which
> the platform now accepts (contexts are cached and a `pending` Deployment is
> recorded). The build worker is still being built — see `docs/agent-cli.md` in
> the platform repo for the wire contract. Log-streaming lands next.

## Install

```sh
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](#releasing) for what publishes them.

## Build & use

```sh
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

# See what's out there — organizations, applications, deployments:
./sc apps                 # the whole tree
./sc apps staging         # only what matches, with its ancestors

# 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:

```sh
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
```

### `.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.

```yaml
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.yml` sits inside the build context**, so it ships with
> the upload unless `.dockerignore` excludes it. `sc` warns when it uses one.
> Prefer `$SC_TOKEN` or the `sc login` store, 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` |
| `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`), offers to open it in a browser, and reads the
pasted token without echoing it. 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.

```sh
./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](https://github.com/compose-spec/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](https://git.grey.ooo/Grey.ooo/someones.computer/src/branch/main/docs/agent-cli.md).

## CI/CD

- **`.github/workflows/ci.yaml`** — on every push to `main` and every PR: `go vet`,
  `go test`, and a static cross-compile across `linux`, `darwin`, `windows` ×
  `amd64`, `arm64` (`CGO_ENABLED=0`).
- **`.github/workflows/release.yaml`** — on a `v*` tag: GoReleaser builds the same
  matrix as static, stripped binaries (`-s -w -trimpath`), archives them
  (`.tar.gz`, `.zip` on Windows) with a `checksums.txt`, and publishes a release to
  this repo via the Forgejo/Gitea API.

Cut a release:

```sh
git tag v0.1.0
git push origin v0.1.0
```

### Requirements for the pipeline to run

- A **Forgejo Actions runner** registered to this repo (or the `Grey.ooo` org)
  exposing the `ubuntu-latest` label.
- The **Actions** unit enabled on the repo (Settings → Units).
- The release job uses the auto-injected `GITHUB_TOKEN` (repo-scoped) as
  `GITEA_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`](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)
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)
scripts/          publish-s3.sh — mirrors a build to someones.computer
.github/          CI + release workflows
.goreleaser.yaml  static multi-arch build matrix
```
