GitHub Actions CLI
Previewops ships a standalone CLI (src/cli.ts → dist/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) |