# CLI & GitHub integration

Source: https://versodocs.com/docs/cli

Engineers build from files in the repo, not from a browser tab. The `versodocs` CLI pulls the approved (Current) version of a spec into the repo as Markdown, records the exact version in `versodocs.lock`, and tells CI when that version has been superseded.

## Install

Node.js 20 or later, no dependencies. Run it with npx or add it to the project:

```bash
npx -y @versodocs/cli@0 --help
# or
npm install --save-dev versodocs
```

The CLI uses the [REST API](https://versodocs.com/docs/api.md) and works on every plan. Reading past versions (`pull --version`, `pull --frozen`) and `diff` need Team; on Free, `check` still runs and says which plan adds change summaries.

## Log in

```bash
versodocs login --org <org-slug> [--api-url https://versodocs.com]
```

Create an API key in VersoDocs under **Settings → Developer** (scope `docs:read`). `login` asks for it with hidden input, or reads it from `--token` or stdin (`echo "$TOKEN" | versodocs login --org acme`). It is stored in `~/.config/versodocs/config.json` (`$XDG_CONFIG_HOME` respected; `%APPDATA%\versodocs\config.json` on Windows) with file mode 600. `versodocs logout` deletes it.

| Setting | Flag | Env | Default |
| --- | --- | --- | --- |
| Token | `login --token` | `VERSODOCS_TOKEN` | — |
| Org | `--org` | `VERSODOCS_ORG` | from `versodocs.lock`, then login |
| API URL | `--api-url` | `VERSODOCS_API_URL` | `https://versodocs.com` |

> `VERSODOCS_TOKEN` always wins over the stored token, so use it in CI. A stored token is only ever sent to the API URL it was saved for.

## Commands

### pull

```bash
# One spec (path = <workspace>/<folder…>/<slug>)
versodocs pull eng/specs/checkout-api [--out docs/checkout-api.md] [--version 12]

# Every spec in a folder; subfolders are mirrored under --out-dir
versodocs pull --folder eng/specs --out-dir docs/specs

# Everything in versodocs.lock, updated to the latest Current version
versodocs pull

# Everything in versodocs.lock at exactly the locked versions (restore)
versodocs pull --frozen
```

Each file is the published Markdown with YAML front-matter:

docs/specs/checkout-api.md:

```markdown
---
title: "Checkout API"
versodocs_id: "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e"
version: 13
published_at: "2026-10-01T09:12:00.000Z"
url: "https://versodocs.com/org/acme/eng/specs/checkout-api"
reference: "versodocs:acme/0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e@v13"
---

# Checkout API
…
```

Without `--out`, a single spec is written to `<slug>.md` in the current directory; `--out-dir` defaults to `.`. `--summary-file <file>` writes a Markdown table of what changed (doc, old → new version, link); the auto-sync workflow uses it as the pull request body.

### status

```text
FILE                        LOCKED  CURRENT  STATUS
--------------------------  ------  -------  ----------
docs/specs/checkout-api.md  v12     v13      outdated
docs/specs/payments.md      v4      v4       up to date
```

Statuses: `up to date`, `outdated` (a newer version is Current), `missing` (file deleted), `modified` (file edited locally since the pull), `not found` (document deleted or unpublished). `--json` prints machine-readable rows.

### check

Same table; **exits 1** when any spec is outdated, missing or not found, and prints a summary of what changed for each outdated spec (Team+).

### diff

```bash
versodocs diff docs/specs/checkout-api.md   # or the doc path: eng/specs/checkout-api
```

Summary and Markdown diff between the locked version and Current (or `--version N`). (Team+)

### API endpoints used

| Command | Endpoint | Plan |
| --- | --- | --- |
| `pull <path>` | `GET docs/by-path?format=md` | All plans |
| `pull` | `GET docs?folder=…, GET docs/{id}?format=md` | All plans |
| `pull --version / --frozen` | `GET docs/{id}?version=N` | Team+ |
| `status, check` | `GET docs/{id}/current` (falls back to `GET docs/{id}` on Free) | All plans |
| `diff, check summary` | `GET docs/{id}/diff` | Team+ |

## versodocs.lock

Commit it. It records the org, API URL, folders pulled with `--folder`, and for every spec its `id`, `path`, `version`, `file` and the `sha256` of the file as written. Entries are sorted and the JSON is formatted deterministically, so it diffs cleanly in pull requests.

versodocs.lock:

```json
{
  "lockfile_version": 1,
  "api_url": "https://versodocs.com",
  "org": "acme",
  "folders": [{ "folder": "eng/specs", "out_dir": "docs/specs" }],
  "docs": [
    {
      "id": "0b6c2d8e-1f2a-4b3c-9d4e-5f6a7b8c9d0e",
      "path": "eng/specs/checkout-api",
      "version": 13,
      "file": "docs/specs/checkout-api.md",
      "sha256": "…"
    }
  ]
}
```

## CI check

Fail a build when the repo's specs fall behind:

.github/workflows/ci.yml (step):

```yaml
- run: npx -y @versodocs/cli@0 check
  env:
    VERSODOCS_TOKEN: ${{ secrets.VERSODOCS_TOKEN }}
```

## GitHub auto-sync

A ready-to-copy workflow keeps the specs in your repo on their Current version. Every hour, or on demand, it runs `versodocs pull` against `versodocs.lock` and, when a spec has a new published version, opens (or updates) one pull request titled **Update specs from VersoDocs**. Nothing changes in your repo until someone merges it. Works on every plan.

1. Pull your specs once, locally, so the repo has a lockfile:

   ```bash
   npx -y @versodocs/cli@0 login --org <your-org>
   npx -y @versodocs/cli@0 pull --folder <workspace>/<folder> --out-dir docs/specs
   git add versodocs.lock docs/specs && git commit -m "docs: add specs from VersoDocs"
   ```
2. Add an API key (`docs:read`) as the repository secret `VERSODOCS_TOKEN`.
3. Copy `integrations/github-sync/versodocs-sync.yml` to `.github/workflows/`.
4. Settings → Actions → General → Workflow permissions → enable *Allow GitHub Actions to create and approve pull requests*.

.github/workflows/versodocs-sync.yml (core):

```yaml
on:
  schedule:
    - cron: '17 * * * *'   # hourly
  workflow_dispatch:
  repository_dispatch:
    types: [versodocs.published]

permissions:
  contents: write
  pull-requests: write

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20 }
      - run: npx -y @versodocs/cli@0 pull --summary-file "$RUNNER_TEMP/versodocs-changes.md"
        env:
          VERSODOCS_TOKEN: ${{ secrets.VERSODOCS_TOKEN }}
      - uses: peter-evans/create-pull-request@5f6978faf089d4d20b00c7766989d076bb2fc7f1 # v8.1.1
        with:
          branch: versodocs/sync
          commit-message: 'docs: update specs from VersoDocs'
          title: Update specs from VersoDocs
          body-path: ${{ runner.temp }}/versodocs-changes.md
```

- The workflow reuses one branch (`versodocs/sync`), so you get one rolling PR.
- Folders pulled with `--folder` are re-listed on every sync, so documents added later arrive in the next PR.
- PRs opened with the default `GITHUB_TOKEN` don't trigger other workflows (GitHub's rule); pass a PAT or GitHub App token via the action's `token:` input if CI must run on them.

## Spec drift check (pr-check)

Catch pull requests built from an outdated spec. `versodocs pr-check` scans the PR title and description for VersoDocs references, asks which version is Current, and fails (or warns) when the PR points at an older one. (Team+)

```text
"Payments API": PR references Spec v12 but Current is v13: https://versodocs.com/org/acme/eng/proposals/…
```

Reference a spec by pasting **Copy reference for PRs** from any document page (`versodocs:<org>/<documentId>@v<N>`), or a document URL with `?v=<N>`. On GitHub, the [GitHub App](https://versodocs.com/docs/github.md#pr-check) runs the same check with no workflow file.

.github/workflows/spec-drift.yml:

```yaml
name: Spec drift check
on: { pull_request: { types: [opened, edited, synchronize, reopened] } }
jobs:
  spec-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/setup-node@v4
        with: { node-version: 22 }
      - run: npx -y @versodocs/cli@0 pr-check
        env:
          VERSODOCS_TOKEN: ${{ secrets.VERSODOCS_TOKEN }}
```

| Option | Default | Description |
| --- | --- | --- |
| `VERSODOCS_TOKEN` (env) | required | API key (`docs:read`). Only ever sent to `--api-url`. |
| `--api-url` | `https://versodocs.com` | VersoDocs origin. URL references on other hosts are ignored. |
| `--mode` | `fail` | `fail`: drift or an unknown reference fails the job. `warn`: report only, always exit 0. |
| `--require-reference` | off | Treat a PR without any spec reference as a problem. |
| `--text` / `--file` | PR title + body | Scan this text / file instead. |

On GitHub Actions it adds `::error` / `::warning` annotations, a job-summary table and the `references` / `outdated` step outputs. Network errors and VersoDocs 5xx responses only warn, so an outage never blocks merges; a rejected token (401), a plan without the CI check (403) or an unknown document (404) is reported as a problem. Include `edited` in the triggers so fixing the reference re-runs the check. Pull requests from forks get no secrets, so skip them with `if: github.event.pull_request.head.repo.full_name == github.repository`.

.gitlab-ci.yml:

```yaml
spec-check:
  image: node:22
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
  script:
    - npx -y @versodocs/cli@0 pr-check
  # VERSODOCS_TOKEN: masked CI/CD variable
```

## Errors & exit codes

| HTTP | What the CLI tells you |
| --- | --- |
| 401 | Run `versodocs login --org <slug>` or set `VERSODOCS_TOKEN`. |
| 403 `plan_feature` | Which plan unlocks the feature, and the upgrade URL. |
| 404 | Check the org slug / path and that the document has a published version. |
| 429 | Retried automatically (honours `Retry-After` up to 30 s), then reports the wait. |

Exit codes: `0` ok, `1` failure (`check` found outdated specs, `pr-check` found drift in fail mode), `2` usage error (bad flags, no token, nothing to scan).
