GitHub Actions
Deploy on every push with one9x/pages-deploy — preview URLs on pull requests, publish on merge.
read as markdownone9x/pages-deploy
is a composite action that installs the CLI, runs one pages release and hands
back the URL. Pull requests get a preview; merges go live.
Skip this page
If you are starting something new, there is a template repository per framework with this workflow already in it — press Use this template, set two values, and you are deploying:
vite · nextjs · astro · nuxt · sveltekit · create-react-app · hugo · eleventy
Each one deploys itself to a live demo on every push, so what you copy is something that is currently working rather than something that once did.
Want a whole site rather than a framework? Templates also has a blog, a docs site, a landing page and a portfolio, wired the same way.
The rest of this page is for adding the action to a repo you already have.
Setup, once
Two things: a site to deploy to, and a token for CI.
$ one9x pages create mysite✓ created mysite → https://mysite.one9x.app/$ one9x tokens create "github actions"one9x_tok_7Kd… (shown once)Both can also be done from the dashboard. Store the printed token as a
repository secret named ONE9X_TOKEN (Settings → Secrets and variables →
Actions), or as an organization secret if several repos deploy.
The action deploys to a site that already exists — it will not create one. A
typo in site should fail, not quietly publish your build to a brand-new
address.
Tokens expire after 90 days.
one9x tokens extend <id>pushes that out without changing the secret, so the stored value keeps working. A silently expired token is the most likely way a pipeline that has run for months starts failing.
Preview on every PR, publish on merge
One workflow covers both, because deploy is just a string the caller computes:
name: Pages
on:
pull_request:
push:
branches: [main]
permissions:
contents: read
pull-requests: write # only the PR comment needs this
concurrency:
group: pages-${{ github.ref }}
# A superseded preview is worth cancelling; a live deploy is not.
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
pages:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 22, cache: npm }
- run: npm ci
- run: npm run build
- id: pages
uses: one9x/pages-deploy@v1
with:
site: mysite
dir: dist
token: ${{ secrets.ONE9X_TOKEN }}
# THE ONE LINE that decides preview vs live.
deploy: ${{ github.ref == 'refs/heads/main' }}
comment: true
- if: steps.pages.outputs.live == 'true'
run: echo "live at ${{ steps.pages.outputs.url }}"
A pull request stages: the release uploads but nothing goes live, and the
action comments the preview URL on the PR, editing the same comment on every
later push rather than adding another. A push to main publishes.
Staging costs nothing — blobs are already uploaded and publishing is a pointer flip, so the preview a reviewer approved is byte-identical to what goes live.
Your build folder is whatever your framework produces; Frameworks has the table.
Requiring an approval before it goes live
If “merged to main” is not enough and you want a human to press a button, put
the deploy in its own job with a GitHub environment that has required
reviewers:
publish:
needs: pages
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
environment: production # required reviewers live on the environment
steps:
- uses: actions/checkout@v5
- uses: actions/setup-node@v5
with: { node-version: 22, cache: npm }
- run: npm ci && npm run build
- uses: one9x/pages-deploy@v1
with:
site: mysite
dir: dist
token: ${{ secrets.ONE9X_TOKEN }}
deploy: true
Don’t try to express this as a conditional environment: on one job — an empty
environment name is an error, not a skip.
Inputs
| Input | What it maps to | Default |
|---|---|---|
site (required) | --site | — |
dir (required) | the folder to deploy | — |
token (required) | ONE9X_TOKEN | — |
deploy | --deploy | false |
spa | --spa | false |
index | --index | — |
error-pages | one <code>:<path> per line | — |
exclude | one glob per line | — |
tag | --tag | — |
cli-version | which CLI to install | pinned per action release |
portal | ONE9X_PORTAL | https://portal.one9x.com |
comment | post/update the PR comment | false |
comment-template | markdown body for that comment | built-in |
github-token | token used to post the comment | ${{ github.token }} |
Booleans are checked, not coerced. deploy: yes and deploy: 1 are the
obvious things to write and both fail the run with a message naming what you
passed. They are not read as false — a misconfiguration whose only symptom is
“the site didn’t change” is worse than a red run.
Outputs
| Output | |
|---|---|
url | The live URL. Empty unless this run published. |
preview-url | The preview URL. Empty unless this run staged. |
version | The release version — a content hash. |
seq | The release number, as in v6. |
live | "true" if this release is the one being served. |
url and preview-url are mutually exclusive by design: a run either published
or staged, and the one that didn’t happen is empty rather than stale.
The PR comment
comment: true posts one comment and then edits it in place on every later
push — a twenty-commit PR gets one comment, not twenty. The marker it looks for
is keyed on the site name, so a monorepo deploying several sites to the same PR
keeps one comment each.
Override the body with comment-template. The placeholders are {site},
{url}, {preview_url}, {version}, {seq} and {commit} — single braces,
because ${{ }} belongs to GitHub’s own templating.
It needs permissions: pull-requests: write in the workflow. A pull request
from a fork is the exception: GitHub gives fork PRs a read-only token by
design, so commenting cannot work there. The action skips it rather than failing
a contributor’s PR.
Pinning
Use one9x/pages-deploy@v1. The v1 tag moves as fixes land, so you get them
without editing workflows; the action carries a pinned, tested CLI version, and
bumping v1 is the gated way a CLI bump reaches you.
Override the CLI with cli-version: v0.3.0 when you need a specific one.
Other CI systems
There is no wrapper for GitLab CI, CircleCI or Jenkins, and none is needed — the action is three lines of shell in a trench coat:
$ curl -fsSL https://get.one9x.com | sh$ export ONE9X_TOKEN=…$ one9x pages release ./dist --site mysite --deploy --json✓ v8 liveWindows runners are not supported by the action itself, because the installer it calls is a POSIX shell script. On Windows, install the CLI with the PowerShell one-liner from Install & sign in and run the command directly.