Publish from GitHub Actions

The goal: every pull request gets a preview URL, and merging to main publishes production. One workflow file, one secret, and the preview machinery does the rest—production never moves until main does.

1. Mint a scoped token

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

surge tokens add --domain example.com -m "github actions"

The token prints once—copy it into your repository's secrets as SURGE_TOKEN (Settings → Secrets and variables → Actions). Because it's scoped with --domain, a leaked secret can publish to this one domain and do nothing else; the -m message is how you'll recognize it in surge tokens list, and surge tokens rem is the kill switch that rotates it.

2. The workflow

name: Publish
on:
  push:
    branches: [main]
  pull_request:

jobs:
  publish:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci && npm run build

      # pull requests: upload a preview - production untouched
      - if: github.event_name == 'pull_request'
        run: npx surge ./dist example.com --preview
        env:
          SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}

      # merges to main: publish production
      - if: github.event_name == 'push'
        run: npx surge ./dist example.com
        env:
          SURGE_TOKEN: ${{ secrets.SURGE_TOKEN }}

The preview step's log ends with the revision's permanent URL—Preview available at …—a complete copy of the built app on the same CDN production uses, safe to link in the PR discussion and point tests at. The production step is a full publish: propagated to every edge node before the job goes green, so a green check means live, not queued.

Notes worth knowing

  • SURGE_TOKEN is picked up from the environment automatically—no login step, no config file. The CLI never prompts when it has what it needs, and aborts rather than hangs if something's missing, so a misconfigured pipeline fails fast and loud.
  • Fork pull requests don't receive secrets on GitHub—preview publishing runs for same-repo branches. That's GitHub's security model, and for a token that can publish to your domain, it's the behavior you want.
  • Rollback is your incident response. A bad merge that went live is one surge example.com rollback from gone—instant and global, from any machine you're logged into. No revert-commit-and-wait-for-CI required (though the revert can follow at leisure).

The same shape works in GitLab CI, CircleCI, or any runner with Node: build, then surge <dir> <domain> with SURGE_TOKEN set. More patterns in CI & Automation.