# GitHub Actions

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

[`one9x/pages-deploy`](https://github.com/marketplace/actions/deploy-to-one9x-pages)
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](https://github.com/one9x/template-vite) ·
[nextjs](https://github.com/one9x/template-nextjs) ·
[astro](https://github.com/one9x/template-astro) ·
[nuxt](https://github.com/one9x/template-nuxt) ·
[sveltekit](https://github.com/one9x/template-sveltekit) ·
[create-react-app](https://github.com/one9x/template-create-react-app) ·
[hugo](https://github.com/one9x/template-hugo) ·
[eleventy](https://github.com/one9x/template-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](/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.

```console title="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:

```yaml
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](/docs/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:

```yaml
  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:

```console title="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](/docs/install) and run the command directly.