CI & Automation

Everything the CLI does interactively it also does non-interactively: pass the project and domain as arguments, supply a token instead of logging in, and surge runs prompt-free in any pipeline.

Mint a token

On your own machine, where you're logged in:

surge tokens add --domain example.com -m "ci"

Copy the token it prints into your CI provider's secret store as SURGE_TOKEN. The CLI picks that environment variable up automatically—no login step in the pipeline:

SURGE_TOKEN=… surge ./dist example.com

A --token <token> flag is also accepted where an environment variable is awkward.

Tokens never face a two-factor challenge: turning two-factor auth on for your account guards the ceremonies that create credentials, and leaves every pipeline holding a token running untouched.

Scoping the token with --domain means a leaked CI secret can publish to that one domain and nothing else; the -m message is how you'll recognize it later. Rotate or kill a pipeline's access any time with surge tokens rem <id>—see tokens.

For a team, add the token to the org instead: surge orgs acme tokens add -m "ci". It publishes as the org, so the pipeline doesn't stop when the person who set it up leaves or resets their password.

The non-interactive rule

surge only prompts for information it doesn't have. Give it everything up front and it never asks:

surge ./dist example.com          # path + domain as arguments…
surge ./dist publish              # …or the CNAME file a previous publish wrote

If a required value is missing and there's no terminal to ask on, the command aborts rather than hangs—so a misconfigured pipeline fails fast. With no token and no stored login, that's:

Aborted - not logged in and no terminal to ask. Run `surge login` in a terminal, or set SURGE_TOKEN.

printed to stderr, with exit code 1. An unset or misspelled SURGE_TOKEN fails the job instead of passing it.

To check what a pipeline is running as before it acts, run surge whoami: with a token it prints the token's id and scope, and it exits 1 when the credentials don't work.

Behind a proxy

The CLI honors the standard proxy variables—HTTPS_PROXY, HTTP_PROXY, ALL_PROXY (or their lowercase spellings)—and NO_PROXY for hosts that should bypass it. Nothing else to configure.

npm scripts

Make publishing part of the project by installing surge as a dev dependency:

npm install --save-dev surge
{
  "scripts": {
    "build": "vite build",
    "deploy": "npm run build && surge ./dist example.com"
  }
}

Now npm run deploy works for everyone on the team (each using their own account, through an org), and in CI with SURGE_TOKEN set.

GitHub Actions and other CI

Publish from GitHub Actions has the complete workflow—a preview for every pull request, production on every merge. The same shape works in GitLab CI, CircleCI, or any runner with Node available: build, then surge <path> <domain> with SURGE_TOKEN in the environment.

Scheduled jobs and loops should keep each domain under 200 publishes a day: publish every 10 minutes or less often, or only when the build output changed. See publishing limits.

Previews per branch

The preview flow maps naturally onto pull requests—publish every branch build as a preview, and cut over on merge:

# on pull request builds
surge ./dist example.com --preview -m "$COMMIT_MESSAGE"

# on merge to main
surge ./dist example.com

Each PR build gets a stable revision URL to link in the review, and production only moves when main does.

Git hooks

For a lighter-weight setup—no CI service at all—publish from a local git hook. Git has no hook that runs after a push, so use pre-push, which runs on every git push (a failed publish stops the push):

printf '#!/bin/sh\nsurge ./dist example.com\n' > .git/hooks/pre-push
chmod +x .git/hooks/pre-push

Or use husky/simple-git-hooks to version the hook with the project.