Desde

Viewer guide

Builds

How the Viewer clones, installs and builds your repository in its own process, how to watch the log live, and every way a build can fail.

A build turns a commit in your GitHub repository into a deployment the Viewer can serve. It runs inside the Viewer process, on the Viewer's own machine. There is no build service, no container, and no queue beyond one build at a time per project.

This page assumes the project already has a repository connected. If not, see projects.

What a build does#

Five steps, in order:

  1. Mint an installation token. The GitHub App produces a short-lived token scoped to the installation you connected.
  2. Clone. A shallow clone (--depth 1) of the configured branch into a temporary directory. The token travels in an HTTP header (-c http.extraHeader=…), never in the clone URL, so it does not get written into .git/config. Every step also scrubs it out of the build log.
  3. Check out a specific commit, if one was requested. Otherwise the branch tip is used and the resolved SHA is recorded.
  4. Install, then build. Your installCommand runs first, then your buildCommand, both through a shell in the checkout root.
  5. Publish. The contents of outputDir are copied into the Viewer's asset store and the deployment becomes the project's active one.

The temporary checkout is deleted on every exit path: success, failure, timeout, or crash.

The environment your commands see#

Your commands do not inherit the Viewer's environment. They get an allowlist:

  • PATH: so git, node and npm resolve.
  • HOME and TMPDIR: both pointed at the build's own scratch directory, not the real home.
  • CI=true.
  • NODE_ENV=production, on the build step only.

NODE_ENV is deliberately absent during install. With npm ci, setting it to production omits devDependencies (which is where vite, typescript and the rest of the build toolchain live), and the build then dies with a missing-package error. (pnpm and Yarn Berry do not behave this way, but the default install command is npm ci.)

Warning The build runner executes whatever code is in the repository you connected, as the Viewer process, on the Viewer's host. It is not a sandbox. This is the same trust model as self-hosted CI: connect repositories you trust, and keep the deployment off the open internet.

There is one consequence worth planning for: secrets are not available. Nothing from the Viewer's own environment reaches your build, and there is no place to configure build-time environment variables. A prototype that needs an API key has to ship it in the repository or mock the call.

Starting a build#

From the dashboard#

Open /review/{slug}, click Repo, and use Build now in the Build section at the bottom of the panel. The status pill and the log pane below it update live.

If the button is disabled, the panel says why, in these words:

Message Meaning
Building needs a GitHub App, which isn't set up on this viewer No GitHub App is configured on the server.
Connect a GitHub repository first The project has no repository configuration.
Only editors and admins can start a build You can read the project, but your role is Viewer.
A build is already running One build per project at a time.

From the API#

curl -X POST http://localhost:3100/api/v1/projects/PROJECT_ID/deployments/build \
  -H "Authorization: Bearer dsv_your_token_here"

A 202 comes back with {"deploymentId": "...", "status": "building"}. The request returns as soon as the build is running, not when it finishes.

To build a specific commit, pass one. It must be 7 to 40 hex characters:

curl -X POST http://localhost:3100/api/v1/projects/PROJECT_ID/deployments/build \
  -H "Authorization: Bearer dsv_your_token_here" \
  -H "Content-Type: application/json" \
  -d '{"commitSha":"a1b2c3d"}'

Other responses: 400 if no repository is connected, 503 if the deployment has no GitHub App, and 409 if a build is already running. The 409 body includes the in-flight deploymentId so you can watch that one instead.

On push (auto-deploy)#

The Viewer accepts GitHub's push webhook at POST /api/v1/webhooks/github. To use it:

  1. Set VIEWER_GITHUB_APP_WEBHOOK_SECRET on the server. Without it the endpoint answers 503 rather than processing unverified payloads.
  2. In your GitHub App's settings, set the webhook URL to {your public URL}/api/v1/webhooks/github, set the same secret, and subscribe to Push events.
  3. Turn on Auto-deploy on push in the project's Repo panel (or send "autoDeploy": true when connecting).

A push then triggers a build if (and only if) the project's autoDeploy is on, the repository's owner/name matches, and the pushed branch matches the configured branch exactly.

Some deliberate quiet cases: a ping event is acknowledged, every non-push event is acknowledged and ignored, a tag push is ignored, and a branch deletion (a push whose after is all zeroes) is ignored. The endpoint always answers 200, even when nothing matched, so a repository nobody wired up does not show as a failed delivery in GitHub.

If a push arrives while a build is already running, the new one is dropped silently. The running build produces the same branch tip anyway.

Watching the log#

The log streams over server-sent events at GET /api/v1/deployments/{deploymentId}/log/stream. Two event types:

  • log: new output since the last event. These are deltas, not snapshots; append them.
  • done: the build finished. Carries the final status (deployed or failed) and the resolved commitSha, then the stream closes.

A comment heartbeat goes out every 25 seconds so an idle stream is not mistaken for a dead one by a proxy in between.

Reading the log is gated on the project, not the deployment, because build output can contain repository content. An unreadable project's log is a 404, the same as one that does not exist.

The stored log is capped at 512 KB, and the browser keeps at most 200,000 characters on screen, trimming from the front. The end of a failing build is the part you want, so that is the part that survives.

You can also read the whole log at once from GET /api/v1/projects/{id}/deployments, which returns every deployment with its status, commitSha and buildLog.

Limits#

Limit Value What happens at the edge
Per-step timeout 10 minutes The step is killed and the build fails with "Clone timed out" / "Install timed out" / "Build timed out".
Total published output 200 MB The publish throws with "Build output exceeds the 200MB limit" and the build fails.
Stored build log 512 KB Older output is dropped.
Concurrent builds per project 1 A second request gets 409 with the running deployment's id.

The output cap is not a soft warning. It is checked while files are being copied, and exceeding it fails the build rather than publishing a partial deployment. An index.html with missing chunks looks like a working deploy and is worse than a failed one.

Failure modes#

Each of these ends the build with status: "failed" and a one-line reason at the end of the log.

Clone failed: the branch does not exist, or the installation token cannot read the repository. Re-check that the App still has access to that repo.

Install failed / Build failed: your own command exited non-zero. The log has the real error. The most common cause on a first build is a lockfile that npm ci refuses, since npm ci requires package-lock.json to match package.json.

Commit <sha> not found: you asked for a commit that is not reachable in the repository.

Build output directory "dist" does not exist: the build succeeded but wrote nothing where you said it would. Check outputDir against what your build tool actually emits.

Build output path "dist" is not a directory: outputDir resolved to a file.

Build output directory "dist" resolves outside the repository checkout: something in the repo (usually a committed symlink) points out of the checkout. Refused; the resolved path is deliberately not reported back.

Build output contains a symlink ("..."): symlinks in the output tree are refused rather than skipped. Skipping would produce a prototype missing files with nothing in the log to explain it.

Build output has no index.html at its root: the Viewer serves /p/{slug}/ from index.html. Without one there is nothing to serve.

What a failure does not do#

A failed build does not disturb what is already being served. activeDeploymentId is only moved when a build succeeds, so the previous deployment stays live.

If the Viewer restarts mid-build, the shutdown path aborts the build and marks the deployment failed. Without that it would sit at "building" forever, showing a spinner that can never resolve.

Next#

  • Reviewing: what to do once a deployment is live.
  • Serving: how the built files reach the browser, and the one build-output shape that breaks under the default URL scheme.