Docs · Sites
UpdatedDeploy from GitHub Actions
Build in your own pipeline and publish the folder with the subnomic CLI. Every pull request gets an address of its own, every push to main goes live, and going back is one command.
Before you start
You need two things: a site the repository is linked to, and a key the runner can deploy with.
Link the repository to a site, once
Install the CLI on your own machine, sign in, and create the site from the project directory, naming the folder your build writes to:
curl -fsSL https://objects.subnomic.com/subnomic/cli/install.sh | bash subnomic login subnomic sites create blog --dir dist
That prints the site’s address and writes subnomic.json next to your code: the site’s id and the output folder, nothing secret. Commit it. It is how the pipeline knows which site to publish and which folder to upload, so the deploy step needs no arguments. Addresses are one namespace shared by every workspace, first come first served — if blog is taken, pick another.
Create a deploy key
In the console, open the workspace menu → API tokens and create a key with two scopes: sites.read and sites.deploy. That is enough to publish a preview, promote a build and roll back. It cannot create or delete a site or attach a domain. Give it an expiry if you rotate secrets on a schedule; the key is shown once.
In GitHub, add it under Settings → Secrets and variables → Actions as SUBNOMIC_TOKEN.
--prod and rollback read the site before they move its live pointer, so a key with sites.deploy alone publishes previews and then fails with a 403 the first time it promotes one. Give it both scopes. Why it works that way: A deploy key needs two scopes, not one.The workflow
Save this as .github/workflows/site.yml. Change the two build steps to whatever your project runs; everything after them stays the same.
name: Site
on:
push:
branches: [main]
pull_request:
permissions:
contents: read
pull-requests: write # only for the preview comment below
concurrency:
group: site-${{ github.ref }}
# A newer commit on a pull request replaces its preview; production deploys queue.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
deploy:
runs-on: ubuntu-latest
# A pull request from a fork gets no secrets, so it cannot deploy a preview.
if: github.event_name == 'push' || github.event.pull_request.head.repo.full_name == github.repository
env:
SUBNOMIC_TOKEN: ${{ secrets.SUBNOMIC_TOKEN }}
SUBNOMIC_API: https://api.subnomic.com
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: 24
cache: npm
- run: npm ci
- run: npm run build
- name: Install the Subnomic CLI
run: curl -fsSL https://objects.subnomic.com/subnomic/cli/install.sh | bash
- name: Deploy
id: deploy
run: |
if [ "${{ github.event_name }}" = "push" ]; then
url=$(subnomic sites deploy --prod)
echo "Live at $url" >> "$GITHUB_STEP_SUMMARY"
else
url=$(subnomic sites deploy)
echo "Preview at $url" >> "$GITHUB_STEP_SUMMARY"
fi
echo "url=$url" >> "$GITHUB_OUTPUT"
- name: Comment the preview
if: github.event_name == 'pull_request'
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr comment "${{ github.event.pull_request.number }}" --body "Preview: ${{ steps.deploy.outputs.url }}"
Drop the last step, and the pull-requests: write line, if you do not want a comment on the pull request. It posts a new comment for every push to the branch.
What each deploy does
- A pull request runs
subnomic sites deploy. The build is uploaded as a new deployment with an address of its own,https://blog--<id>.subnomic.app/, and nothing goes live. Every push to the branch makes another one. - A push to main runs
subnomic sites deploy --prod: the same upload, then the site’s live pointer moves to the new deployment and the site’s own address is printed. - Only what changed is uploaded. Files are hashed on the runner, and any file an earlier deployment of the site already holds is reused rather than sent again, so the second deploy of a large site uploads a handful of files.
- Only the URL goes to stdout. Progress and the “Live at” line go to stderr, which is why
url=$(…)captures the address and nothing else. - Nothing private is published by accident.
.envfiles,.gitandnode_modulesare never uploaded, and a folder with noindex.htmlis refused before anything is sent. - Any failure fails the step. The CLI exits non-zero, so a refused deploy turns the job red instead of reporting success.
A site keeps its live deployment and the ten newest ones besides it; older ones are retired, previews included. A busy pull request therefore shortens how far back production can roll, and the preview of a long-merged branch eventually stops answering.
Preview addresses are unguessable and served with noindex, but they are not private: anyone with the link can open one. To keep previews behind a sign-in, protect the site — previews are covered by the same setting. See Keeping a site private.
Roll back
Every deployment is kept whole, so rolling back moves the live pointer to one that already exists. Nothing is rebuilt or uploaded.
subnomic sites rollback # to the deployment before the live one subnomic sites rollback --to <deployment id>
Run it from your machine, or keep a button for it in GitHub. Without a terminal, rollback with no --to picks the deployment immediately before the live one:
name: Roll back site
on: workflow_dispatch
jobs:
rollback:
runs-on: ubuntu-latest
env:
SUBNOMIC_TOKEN: ${{ secrets.SUBNOMIC_TOKEN }}
SUBNOMIC_API: https://api.subnomic.com
steps:
- uses: actions/checkout@v7 # for subnomic.json
- run: curl -fsSL https://objects.subnomic.com/subnomic/cli/install.sh | bash
- run: subnomic sites rollback
The same key works: rolling back needs sites.read and sites.deploy, nothing more. Why rollback is a pointer rather than a rebuild is in Rollback should move a pointer.
When it fails
| You see | What it means |
|---|---|
SUBNOMIC_TOKEN is set but SUBNOMIC_API is not | A key needs an endpoint to go with it. Set SUBNOMIC_API: https://api.subnomic.com next to the token. |
Previews work, --prod gets a 403 | The key has sites.deploy but not sites.read. Create one with both. |
| No site is linked | subnomic.json is missing from the checkout. Commit it, or pass --site blog. In CI the CLI never creates a site on its own: a pipeline whose link went missing fails instead of publishing to an address nobody is watching. |
dist has no index.html — is that the build output directory? | The build wrote somewhere else, or did not run. Pass --dir with the right folder — a positional subnomic sites deploy dist is not read as the directory. |
Every flag of sites deploy, rollback and the rest is in CLI; domains, redirects and headers are in Sites.