Show what a deploy is doing while it packs and uploads #21

Merged
geusebio merged 1 commit from feature/sc-deploy-live-progress into main 2026-08-09 09:34:58 +00:00
Owner

sc deploy said almost nothing while it worked: silence until packing had finished, a tumbling spinner for the whole upload, and a bare uuid at the end. All three had the machinery already in place and unused.

This is the local half of giving a deploy real feedback. Following the remote half — build and rollout — needs a platform endpoint and is #293; the ui primitives added here are what that will draw with.

What changed

Packing ran before the header. Walking and gzipping every build context is the longest local step of a deploy, and it happened before the command had printed so much as which application it had settled on. The header now prints first, and bundle.Load takes a callback so the spinner names the context currently costing the wait.

The upload gauge was dead code. ui.Spinner has had an eight-step fill gauge for a measured wait since it was written, and upload.Bundle took a ProgressFunc its own docblock described as never called — the multipart was assembled in one piece, so there was no moment between "nothing sent" and "all sent" to report. It now writes the body through an io.Pipe and counts it past in bounded chunks.

Two things that are easy to get wrong here and are worth a reviewer's eye:

  • The body is measured first, against io.Discard. Not for the gauge's denominator — for Content-Length. The platform refuses an oversized bundle from that header alone, and a chunked upload would turn a courteous 413 into 512 MiB sent and then refused. Both passes share one boundary, and TestBundleSendsEveryPartTheMeasuringPassCounted parses what actually arrived to prove they agree.
  • Counting is chunked at 64 KiB. multipart hands over a whole tarball per Write, so a counter that ticked once per call reported one step per part. I only caught this by running it — the first recording went → done with nothing in between.

The closing line dropped the link. The bundle endpoint has returned an absolute url for the revision's page since #292 closed, and upload.Deployment never had the field, so sc parsed the response and threw it away. docs/agent-cli.md has meanwhile claimed that url "is what sc deploy prints as its closing line". Now it is.

internal/ui grew three primitives, all needed by the live build view in #293 and all useful now: Println (a durable line above a running spinner — erase, write, let the tick redraw, under the mutex draw already holds, which is what will let build output scroll past something still spinning), Fail (the landed die in red brackets, with the colour deliberately an accent so a NO_COLOR terminal and a CI log read identically — hence its summary must say what went wrong in words), and an opt-in elapsed clock. Frames are now erased rather than overdrawn, so a label that shrinks leaves no tail.

What it looks like

Before — nothing at all until packing finished, then a tumble, then a uuid:

Application:  example-app
Endpoint:     http://127.0.0.1:7500
Compose:      demo — 3 service(s), 2 build context(s)
  build api              api  sha256:e6c15e3e7fc1  (11.4 MiB)

Deployment 019fe3f5-d576-7a59-a369-d1b206f27bfe created — seq 12, status pending

After — distinct frames from one recorded run against a throttled stand-in platform, 17.2 MiB over two contexts:

Application:  demo
Deployment:   main
Endpoint:     http://127.0.0.1:7777
[S⠙C] Reading the compose project
[S⠸C] Packing api  (1/2)
[S⠦C] Packing web  (2/2)
[S⠿C] Packed 2 build contexts in 300ms
Compose:      demo — 3 service(s), 2 build context(s)
  build api              api  sha256:e6c15e3e7fc1  (11.4 MiB)
  build web              web  sha256:e7b52b723027  (5.7 MiB)
[S⠙C] Checking demo on http://127.0.0.1:7777
[S⣀C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣄C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣤C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣦C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣶C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣷C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⣿C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⠸C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⠴C] Uploading 17.2 MiB to http://127.0.0.1:7777
[S⠿C] Uploaded 17.2 MiB in 3.5s

Deployment #12 queued — pending
Watch it:     http://127.0.0.1:7777/applications/demo/deployments/019fe3f5

The gauge climbing and then handing back to the tumble is the intended behaviour, not a glitch: once the bytes are gone there is nothing left to measure and the wait belongs to the far end.

Off a terminal it degrades to plain text with no escape codes, which is what a CI log wants:

Application:  example-app
Endpoint:     http://127.0.0.1:7500
Reading the compose project…
[S⠿C] Packed 1 build context in 0s
Compose:      demo — 2 service(s), 1 build context(s)

Verified

  • go build ./..., go vet ./..., gofmt -l internal/ cmd/ clean.
  • go test ./... green; go test -race -count=3 ./internal/ui/ green, since Println and draw contend for the same mutex.
  • Ran end to end against a throttled stand-in platform (the recording above) and as a --dry-run against example-app/, on a pty and redirected.

Not in scope

Following the deploy past pending — the build and rollout status, and streaming kaniko's output — is #293 and needs the platform endpoint first. --detach and --no-build-logs land with it, since there is nothing yet to detach from or suppress.

`sc deploy` said almost nothing while it worked: silence until packing had finished, a tumbling spinner for the whole upload, and a bare uuid at the end. All three had the machinery already in place and unused. This is the local half of giving a deploy real feedback. Following the *remote* half — build and rollout — needs a platform endpoint and is [#293](https://git.grey.ooo/Grey.ooo/someones.computer/issues/293); the `ui` primitives added here are what that will draw with. ## What changed **Packing ran before the header.** Walking and gzipping every build context is the longest local step of a deploy, and it happened before the command had printed so much as which application it had settled on. The header now prints first, and `bundle.Load` takes a callback so the spinner names the context currently costing the wait. **The upload gauge was dead code.** `ui.Spinner` has had an eight-step fill gauge for a measured wait since it was written, and `upload.Bundle` took a `ProgressFunc` its own docblock described as never called — the multipart was assembled in one piece, so there was no moment between "nothing sent" and "all sent" to report. It now writes the body through an `io.Pipe` and counts it past in bounded chunks. Two things that are easy to get wrong here and are worth a reviewer's eye: - **The body is measured first, against `io.Discard`.** Not for the gauge's denominator — for `Content-Length`. The platform refuses an oversized bundle from that header alone, and a chunked upload would turn a courteous 413 into 512 MiB sent and then refused. Both passes share one boundary, and `TestBundleSendsEveryPartTheMeasuringPassCounted` parses what actually arrived to prove they agree. - **Counting is chunked at 64 KiB.** `multipart` hands over a whole tarball per `Write`, so a counter that ticked once per call reported one step per part. I only caught this by running it — the first recording went `⡀` → `⣶` → done with nothing in between. **The closing line dropped the link.** The bundle endpoint has returned an absolute `url` for the revision's page since [#292](https://git.grey.ooo/Grey.ooo/someones.computer/issues/292) closed, and `upload.Deployment` never had the field, so `sc` parsed the response and threw it away. `docs/agent-cli.md` has meanwhile claimed that url "is what `sc deploy` prints as its closing line". Now it is. **`internal/ui` grew three primitives**, all needed by the live build view in #293 and all useful now: `Println` (a durable line above a running spinner — erase, write, let the tick redraw, under the mutex `draw` already holds, which is what will let build output scroll past something still spinning), `Fail` (the landed die in red brackets, with the colour deliberately an accent so a `NO_COLOR` terminal and a CI log read identically — hence its summary must say what went wrong in words), and an opt-in elapsed clock. Frames are now erased rather than overdrawn, so a label that shrinks leaves no tail. ## What it looks like Before — nothing at all until packing finished, then a tumble, then a uuid: ``` Application: example-app Endpoint: http://127.0.0.1:7500 Compose: demo — 3 service(s), 2 build context(s) build api api sha256:e6c15e3e7fc1 (11.4 MiB) Deployment 019fe3f5-d576-7a59-a369-d1b206f27bfe created — seq 12, status pending ``` After — distinct frames from one recorded run against a throttled stand-in platform, 17.2 MiB over two contexts: ``` Application: demo Deployment: main Endpoint: http://127.0.0.1:7777 [S⠙C] Reading the compose project [S⠸C] Packing api (1/2) [S⠦C] Packing web (2/2) [S⠿C] Packed 2 build contexts in 300ms Compose: demo — 3 service(s), 2 build context(s) build api api sha256:e6c15e3e7fc1 (11.4 MiB) build web web sha256:e7b52b723027 (5.7 MiB) [S⠙C] Checking demo on http://127.0.0.1:7777 [S⣀C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣄C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣤C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣦C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣶C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣷C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⣿C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⠸C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⠴C] Uploading 17.2 MiB to http://127.0.0.1:7777 [S⠿C] Uploaded 17.2 MiB in 3.5s Deployment #12 queued — pending Watch it: http://127.0.0.1:7777/applications/demo/deployments/019fe3f5 ``` The gauge climbing and then handing back to the tumble is the intended behaviour, not a glitch: once the bytes are gone there is nothing left to measure and the wait belongs to the far end. Off a terminal it degrades to plain text with no escape codes, which is what a CI log wants: ``` Application: example-app Endpoint: http://127.0.0.1:7500 Reading the compose project… [S⠿C] Packed 1 build context in 0s Compose: demo — 2 service(s), 1 build context(s) ``` ## Verified - `go build ./...`, `go vet ./...`, `gofmt -l internal/ cmd/` clean. - `go test ./...` green; `go test -race -count=3 ./internal/ui/` green, since `Println` and `draw` contend for the same mutex. - Ran end to end against a throttled stand-in platform (the recording above) and as a `--dry-run` against `example-app/`, on a pty and redirected. ## Not in scope Following the deploy past `pending` — the build and rollout status, and streaming kaniko's output — is #293 and needs the platform endpoint first. `--detach` and `--no-build-logs` land with it, since there is nothing yet to detach from or suppress.
Show what a deploy is doing while it packs and uploads
All checks were successful
CI / build (pull_request) Successful in 1m47s
89fbb6b0c2
`sc deploy` printed nothing until packing had finished, then held a
tumbling spinner for the whole upload, then closed on a bare uuid. Three
things were wrong and all three were already paid for.

Packing ran before the header. Walking and gzipping every build context is
the longest local step of a deploy, and it happened before the command had
said so much as which application it had settled on. The header now prints
first, and `bundle.Load` takes a callback so the spinner can name the
context currently costing the wait.

The upload gauge was dead code. `ui.Spinner` has an eight-step fill gauge
for a measured wait, and `upload.Bundle` took a ProgressFunc it documented
as never calling: the multipart was assembled in one piece, so there was no
moment between "nothing sent" and "all sent" to report. It now writes the
body through an io.Pipe and counts it past in bounded chunks. Pipe writes
block until net/http has read them, so the count follows the socket rather
than the buffer; the chunking matters because multipart hands over a whole
tarball per Write, and counting per call gave a gauge that sat at zero and
then jumped to done. The body is measured once against io.Discard first, because the
request has to carry a Content-Length — the platform refuses an oversized
bundle from that header alone, and a chunked upload would turn a courteous
413 into 512 MiB sent and then refused.

The closing line dropped the link. The bundle endpoint has returned an
absolute `url` for the revision's page since #292, and this struct never
had the field. The upload returns before the build has run, so that page is
where the rest of the deploy happens; it is now the last thing printed.

The ui package grows the three primitives the live build view will need:
Println, which writes a durable line above a running spinner so output can
scroll past something still spinning; Fail, a landed die in red brackets,
with the colour deliberately an accent so a CI log reads the same; and an
opt-in elapsed clock. Frames are erased rather than overdrawn, so a label
that shrinks leaves no tail behind.

Groundwork for streaming a build's output to the CLI (#293).
geusebio changed title from WIP: Show what a deploy is doing while it packs and uploads to Show what a deploy is doing while it packs and uploads 2026-08-09 00:55:11 +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!21
No description provided.