CI/CD: Deploying with Neo from GitHub Actions

Neo needs SSH access and a license key — that's the whole CI setup. No server-side webhook receiver, no agent to install.

6 min read

What CI needs

Neo deploys the same way from CI as from your laptop: SSH access to the server, and an activated license. Nothing runs on the server waiting for a webhook — the CI job connects out to the server, the same direction a developer's laptop would.

Secrets to configure

In your repository's GitHub Actions secrets:

  • NEO_SSH_PRIVATE_KEY — a deploy key with access to the target server (see Team SSH Key Management for adding it there)
  • NEO_ENV_KEY — if the project uses encrypted env files, the decryption key, so the workflow never prompts

A minimal workflow

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Install Neo
        run: curl -fsSL https://neo.vxero.dev/neo | sh

      - name: Configure SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.NEO_SSH_PRIVATE_KEY }}" > ~/.ssh/id_ed25519
          chmod 600 ~/.ssh/id_ed25519
          ssh-keyscan your-server.example.com >> ~/.ssh/known_hosts

      - name: Deploy
        env:
          NEO_DEV_PLUS: "true"       # skip interactive license activation in CI
          NEO_ENV_KEY: ${{ secrets.NEO_ENV_KEY }}
        run: neo deploy --to production

Why the commit tracking matters here specifically

Since deploy records the git commit that produced each build, a CI-triggered deploy is exactly where that pays off — the commit captured is the exact one GitHub Actions checked out, not an approximation from a developer's local branch state days later:

neo status my-app --json | jq -r .deployment.commit

This should always equal $GITHUB_SHA for the deploy that just ran; a CI job can assert it as a post-deploy check.

Shallow checkouts

GitHub Actions' default checkout is often shallow, which can leave git unable to fully describe the commit locally. Neo falls back to GITHUB_SHA automatically in that case — no extra configuration needed for the commit to still be captured correctly.

Deploying on tag push instead of every commit to main

on:
  push:
    tags: ['v*']

Pairs naturally with neo status's version display, which shows the git tag (v1.4.2) when the deployed commit is tagged — so neo list reads as your actual release version, not just a commit hash.

Try it yourself — Neo is free, with every feature unlocked.

Get started free