documentation menu

GitHub Actions

Deploy on every push with one9x/pages-deploy — preview URLs on pull requests, publish on merge.

read as markdown

one9x/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.

setup
$ 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

InputWhat it maps toDefault
site (required)--site—
dir (required)the folder to deploy—
token (required)ONE9X_TOKEN—
deploy--deployfalse
spa--spafalse
index--index—
error-pagesone <code>:<path> per line—
excludeone glob per line—
tag--tag—
cli-versionwhich CLI to installpinned per action release
portalONE9X_PORTALhttps://portal.one9x.com
commentpost/update the PR commentfalse
comment-templatemarkdown body for that commentbuilt-in
github-tokentoken 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
urlThe live URL. Empty unless this run published.
preview-urlThe preview URL. Empty unless this run staged.
versionThe release version — a content hash.
seqThe 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:

any CI
$ curl -fsSL https://get.one9x.com | sh$ export ONE9X_TOKEN=…$ one9x pages release ./dist --site mysite --deploy --json✓ v8 live

Windows 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.