GitHub Actions CLI

Previewops ships a standalone CLI (src/cli.tsdist/cli.js) that provides the same deploy, delete, validate, and cleanup commands as the GitHub App — but driven entirely by GitHub Actions and repository secrets. No third-party app installation is required.


When to use the CLI instead of the App

Scenario Recommendation
You can install GitHub Apps on your org GitHub App (easier, zero per-repo config)
Your org blocks third-party app installations CLI + reusable workflow
You want PR comments but no external webhook receiver CLI + reusable workflow
You want to trigger deploys from a custom workflow step CLI directly

Quick start

1. Allow your repo to call the reusable workflow

The reusable workflow lives at .github/workflows/previewops-deploy.yml in the previewops repository. To call it from your own repo, create .github/workflows/preview.yml:

name: Preview Environments

on:
  issue_comment:
    types: [created]
  pull_request:
    types: [closed]

jobs:
  # Step 1: parse the PR comment to determine the command
  parse-command:
    if: github.event_name == 'issue_comment' && github.event.issue.pull_request != null
    runs-on: ubuntu-latest
    outputs:
      command: ${{ steps.parse.outputs.command }}
      pr_number: ${{ steps.parse.outputs.pr_number }}
    steps:
      - id: parse
        run: |
          BODY="${{ github.event.comment.body }}"
          if echo "$BODY" | grep -qE '^/deploy-previewops'; then
            echo "command=deploy" >> $GITHUB_OUTPUT
          elif echo "$BODY" | grep -qE '^/delete-previewops'; then
            echo "command=delete" >> $GITHUB_OUTPUT
          elif echo "$BODY" | grep -qE '^/validate-previewops'; then
            echo "command=validate" >> $GITHUB_OUTPUT
          elif echo "$BODY" | grep -qE '^/perf-previewops'; then
            echo "command=perf" >> $GITHUB_OUTPUT
          fi
          echo "pr_number=${{ github.event.issue.number }}" >> $GITHUB_OUTPUT

  # Step 2: call the reusable workflow
  deploy:
    needs: parse-command
    if: needs.parse-command.outputs.command != '' && github.event_name == 'issue_comment'
    uses: YOUR_ORG/previewops/.github/workflows/previewops-deploy.yml@main
    with:
      command: ${{ needs.parse-command.outputs.command }}
      pr_number: ${{ fromJson(needs.parse-command.outputs.pr_number) }}
      owner: ${{ github.repository_owner }}
      repo: ${{ github.event.repository.name }}
      provider: cloud-run
      provider_config: '{}'   # non-secret providerConfig fields
    secrets:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}

  # Step 3: delete preview when PR is closed
  cleanup-on-close:
    if: github.event_name == 'pull_request' && github.event.action == 'closed'
    uses: YOUR_ORG/previewops/.github/workflows/previewops-deploy.yml@main
    with:
      command: delete
      pr_number: ${{ github.event.pull_request.number }}
      owner: ${{ github.repository_owner }}
      repo: ${{ github.event.repository.name }}
      provider: cloud-run
      provider_config: '{}'
    secrets:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}

2. Add scheduled TTL cleanup

Create a separate .github/workflows/preview-cleanup.yml:

name: Preview Cleanup

on:
  schedule:
    - cron: '0 * * * *'   # every hour
  workflow_dispatch:        # allow manual runs

jobs:
  cleanup:
    uses: YOUR_ORG/previewops/.github/workflows/previewops-deploy.yml@main
    with:
      command: cleanup
      owner: ${{ github.repository_owner }}
      repo: ${{ github.event.repository.name }}
      provider: cloud-run
      provider_config: '{}'
    secrets:
      GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      GCP_PROJECT_ID: ${{ secrets.GCP_PROJECT_ID }}

Reusable workflow inputs

Required for all commands

Input Type Description
command string One of deploy, delete, validate, cleanup, perf.
owner string GitHub repository owner / org.
repo string Repository name.
provider string Provider key (default: cloud-run).
provider_config string JSON-encoded non-secret providerConfig fields (default: {}).

Required for deploy

Input Type Default Description
pr_number number 0 PR number to deploy.
sha string ${{ github.sha }} Commit SHA to build.
branch string ${{ github.head_ref }} Branch name.
clone_url string auto HTTPS clone URL.
ttl_hours number 168 Preview TTL in hours.
memory string 512Mi Container memory.
cpu string 0.1 Container CPU.
port number 8080 Container port.
dockerfile string Dockerfile Dockerfile path.
build_context string . Docker build context.

Secrets

Secrets are forwarded to the CLI as environment variables. Pass exactly the secrets your provider needs:

Secret Provider Description
GITHUB_TOKEN all Used to post PR comments. Optional — omit to skip comments.
GCP_PROJECT_ID cloud-run Also accepted as a workflow input.
FLY_API_TOKEN fly
HETZNER_API_TOKEN hetzner
HETZNER_SSH_KEY hetzner
SSH_PRIVATE_KEY docker-ssh
RENDER_API_KEY render
RAILWAY_API_TOKEN railway
DIGITALOCEAN_TOKEN digitalocean
DO_SSH_PRIVATE_KEY digitalocean
AWS_ACCESS_KEY_ID aws-lightsail, aws-ecs
AWS_SECRET_ACCESS_KEY aws-lightsail, aws-ecs
AWS_SSH_PRIVATE_KEY aws-lightsail
AZURE_CLIENT_ID azure-container-apps
AZURE_CLIENT_SECRET azure-container-apps
AZURE_TENANT_ID azure-container-apps
AZURE_SUBSCRIPTION_ID azure-container-apps

CLI commands

After building (npm run build), the CLI is at dist/cli.js.

deploy

Builds and deploys a preview environment for a PR.

PREVIEW_OWNER=acme \
PREVIEW_REPO=website \
PREVIEW_PR_NUMBER=42 \
PREVIEW_SHA=abc123def456 \
PREVIEW_BRANCH=feat/my-feature \
PREVIEW_CLONE_URL=https://github.com/acme/website \
PREVIEW_PROVIDER=cloud-run \
GCP_PROJECT_ID=my-project-123 \
GITHUB_TOKEN=ghp_... \
node dist/cli.js deploy

On success, prints the preview URL and TTL to stdout and updates the PR comment.

delete

Tears down all preview services for a PR.

PREVIEW_OWNER=acme \
PREVIEW_REPO=website \
PREVIEW_PR_NUMBER=42 \
PREVIEW_PROVIDER=cloud-run \
GCP_PROJECT_ID=my-project-123 \
GITHUB_TOKEN=ghp_... \
node dist/cli.js delete

validate

Checks that credentials and providerConfig are valid without deploying anything.

PREVIEW_OWNER=acme \
PREVIEW_REPO=website \
PREVIEW_PR_NUMBER=42 \
PREVIEW_PROVIDER=fly \
FLY_API_TOKEN=fo1_... \
PREVIEW_PROVIDER_CONFIG='{"orgSlug":"my-org"}' \
GITHUB_TOKEN=ghp_... \
node dist/cli.js validate

Posts a pass/fail comment on the PR. Exits with code 1 if validation fails.

cleanup

Scans all active previews for PREVIEW_PROVIDER and deletes any that have exceeded their TTL.

PREVIEW_PROVIDER=cloud-run \
GCP_PROJECT_ID=my-project-123 \
node dist/cli.js cleanup

Exits with code 1 if any deletions failed.


perf

Runs a k6 load test against an active preview URL and posts results to the PR.

PREVIEW_OWNER=acme \
PREVIEW_REPO=website \
PREVIEW_PR_NUMBER=42 \
PREVIEW_PROVIDER=cloud-run \
GCP_PROJECT_ID=my-project-123 \
GITHUB_TOKEN=ghp_... \
node dist/cli.js perf

Optional flags (set as env vars):

Variable Default Description
PREVIEW_PERF_VUS 10 Number of virtual users. Clamped to per-plan cap: Premium BYOC = 10, Pro = 50, Enterprise = 100, Custom = 150.
PREVIEW_PERF_DURATION 30s Test duration (e.g. 2m, 90s). Clamped to plan cap: 30 min standard, 60 min with the Advanced Perf add-on.
PREVIEW_PERF_SET_BASELINE Set to true to save this run as the baseline for future comparisons. Requires the Advanced Perf add-on (Pro+).

Posts a results comment with latency percentiles, throughput, error rate, and SLO evaluation. On paid plans, AI-generated insights and baseline comparison are included.


All CLI environment variables

Variable Command Required Default Description
PREVIEW_OWNER all except cleanup GitHub org/user.
PREVIEW_REPO all except cleanup Repository name.
PREVIEW_PR_NUMBER deploy, delete, validate, perf Pull-request number.
PREVIEW_PROVIDER all cloud-run Provider key.
PREVIEW_PROVIDER_CONFIG all {} JSON-encoded providerConfig.
GITHUB_TOKEN all GitHub PAT or Actions token.
PREVIEW_SHA deploy Full commit SHA.
PREVIEW_BRANCH deploy Branch name.
PREVIEW_CLONE_URL deploy HTTPS clone URL.
PREVIEW_TTL_HOURS deploy 168 TTL in hours.
PREVIEW_MEMORY deploy 512Mi Container memory.
PREVIEW_CPU deploy 0.1 Container CPU.
PREVIEW_PORT deploy 8080 Container port.
PREVIEW_DOCKERFILE deploy Dockerfile Dockerfile path.
PREVIEW_BUILD_CONTEXT deploy . Docker build context.
GITHUB_ACTOR deploy github-actions Actor name shown in comments.
PREVIEW_PERF_VUS perf 10 Virtual users for the load test. Clamped to per-plan cap.
PREVIEW_PERF_DURATION perf 30s Test duration (e.g. 2m, 90s). Clamped to 30 min (60 min with Advanced Perf add-on).
PREVIEW_PERF_SET_BASELINE perf Set to true to save as baseline. Requires the Advanced Perf add-on (Pro+).
PREVIEWOPS_TOKEN all CLI token generated in the Previewops dashboard (Pro / Enterprise / Custom). When set, the CLI verifies the token against the SaaS API before proceeding. Omit for self-hosted setups.
PREVIEWOPS_API_URL all https://previewops.io Override the API base URL for token verification. Only needed for self-hosted deployments.

AWS ECS example

uses: YOUR_ORG/previewops/.github/workflows/previewops-deploy.yml@main
with:
  command: ${{ needs.parse-command.outputs.command }}
  pr_number: ${{ fromJson(needs.parse-command.outputs.pr_number) }}
  owner: ${{ github.repository_owner }}
  repo: ${{ github.event.repository.name }}
  provider: aws-ecs
  provider_config: >-
    {
      "cluster": "my-preview-cluster",
      "vpc": "vpc-0abc123",
      "subnets": ["subnet-0abc123", "subnet-0def456"],
      "securityGroups": ["sg-0abc123"],
      "albListenerArn": "arn:aws:elasticloadbalancing:us-east-1:...",
      "executionRoleArn": "arn:aws:iam::123456789012:role/ecsTaskExecutionRole",
      "baseDomain": "previews.example.com"
    }
secrets:
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
  AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
  AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Differences from the GitHub App

Feature GitHub App CLI
PR comments ✅ (when GITHUB_TOKEN is set)
Automatic PR-close cleanup ✅ (via pull_request: closed trigger)
alwaysOn branch deploys Manual (trigger via push event)
Capacity eviction
Performance testing (/perf-previewops) ✅ (perf command)
SaaS plan enforcement (TTL clamp, deploy cap) ❌ (no control-plane lookup)
Multiple concurrent deployments ✅ (in-process lock) ✅ (GitHub Actions concurrency groups)
Monorepo services array ❌ (single service per run)