CollabOps

Hosting

Deployment templates for Vercel, Firebase, Fly.io, Convex, and SSH

collabops/vercel-deploy@v1

On-Premise: ❌ — requires Vercel connectivity

Builds and deploys a Vercel project. Supports Preview and Production environments.

Prerequisites

1. Create a Vercel API Token

Go to Vercel Dashboard > Settings > Tokens and create a new token.

2. Create a Vercel Project

You need to create the project via the Vercel API without connecting a Git provider.

curl -X POST https://api.vercel.com/v10/projects \
  -H "Authorization: Bearer {VERCEL_TOKEN}" \
  -H "Content-Type: application/json" \
  -d '{"name": "my-project", "framework": "nextjs"}'

name: Project name

framework: Set according to your project (nextjs, vite, nuxtjs, svelte, etc.)

Use id from the response as VERCEL_PROJECT_ID

3. Find Your Organization ID

Go to Vercel Dashboard > Settings > General and find the Vercel ID (= Organization ID).

4. Register Secrets

Register the following secrets in your CollabOps project settings.

SecretDescription
VERCEL_TOKENVercel API token
VERCEL_ORG_IDOrganization / Team ID
VERCEL_PROJECT_IDProject ID

Inputs

InputRequiredDefaultDescription
vercel-tokenYES-Vercel API token. $\{\{ secrets.VERCEL_TOKEN \}\} recommended
vercel-org-idYES-Vercel Organization ID
vercel-project-idYES-Vercel Project ID
productionNO"false"Production deployment (false = Preview)
working-directoryNO"/workspace/source"Project root path

Usage Example

steps:
  - name: Checkout
    uses: collabops/checkout@v2
    with:
      repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      ref: ${{ collabops.ref_name }}
      sha: ${{ collabops.sha }}

  - name: Deploy to Vercel
    uses: collabops/vercel-deploy@v1
    with:
      vercel-token: ${{ secrets.VERCEL_TOKEN }}
      vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
      vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
      production: "false"

Production vs Preview routing

# Triggered by push and change_request. Pick production vs preview per Job using if.
triggers:
  push:
    branches: [main]
  change_request:
    branches: [main]

jobs:
  deploy-production:
    # Production only on main push — gate with a Job-level if.
    if: "collabops.event_name == 'push'"
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: vercel-prod
        uses: "collabops/vercel-deploy@v1"
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          production: "true"

  deploy-preview:
    # Change Request events deploy a preview.
    if: "collabops.event_name == 'change_request'"
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: vercel-preview
        uses: "collabops/vercel-deploy@v1"
        with:
          vercel-token: ${{ secrets.VERCEL_TOKEN }}
          vercel-org-id: ${{ secrets.VERCEL_ORG_ID }}
          vercel-project-id: ${{ secrets.VERCEL_PROJECT_ID }}
          production: "false"

Key points — Branch production using triggers plus a per-Job if: "collabops.event_name == ...". Use working-directory to scope multi-project monorepos.

collabops/firebase-deploy@v1

On-Premise: ❌ — requires Firebase connectivity

Deploys Firebase resources. Supports selective deployment of Functions, Hosting, Firestore Rules, and more.

InputRequiredDefaultDescription
service-account-keyYES-GCP service account key JSON. $\{\{ secrets.FIREBASE_SA_KEY \}\} recommended
project-idYES-Firebase project ID
deploy-targetsNO""Deploy targets (comma-separated: functions, hosting, firestore:rules, storage:rules). Deploys all if empty
working-directoryNO"/workspace/source"Directory containing firebase.json

Examples

Basic — deploy the full firebase.json

jobs:
  deploy:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: install-and-build
        run: |
          npm ci
          npm run build
        image: node:22-alpine
      # Without deploy-targets, the entire firebase.json is deployed.
      - name: firebase-deploy
        uses: "collabops/firebase-deploy@v1"
        with:
          service-account-key: ${{ secrets.FIREBASE_SA_KEY }}
          project-id: my-firebase-project

Targeted deploy — specific resources only

jobs:
  deploy-hosting:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: install-and-build
        run: |
          npm ci
          npm run build
        image: node:22-alpine
      - name: firebase-deploy-targeted
        uses: "collabops/firebase-deploy@v1"
        with:
          service-account-key: ${{ secrets.FIREBASE_SA_KEY }}
          project-id: my-firebase-project
          # Comma-separated — hosting, functions, firestore, storage, …
          deploy-targets: "hosting,functions"

Key pointsservice-account-key is the complete JSON key downloaded from the GCP console. Use deploy-targets to ship only the hot path quickly; a full deploy is much slower. Point working-directory at the directory that contains firebase.json for monorepo setups.

collabops/fly-deploy@v1

On-Premise: ❌ — requires Fly.io connectivity

Deploys an app to Fly.io using flyctl with remote build.

InputRequiredDefaultDescription
api-tokenYES-Fly.io API token. $\{\{ secrets.FLY_API_TOKEN \}\} recommended
app-nameNO""Fly.io app name (reads from fly.toml if empty)
remote-onlyNO"true"Use remote builder
working-directoryNO"/workspace/source"Directory containing fly.toml

Examples

Basic — pick app name from fly.toml

jobs:
  deploy:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      # Omitting app-name uses the [app] value from the workspace's fly.toml.
      - name: fly-deploy
        uses: "collabops/fly-deploy@v1"
        with:
          api-token: ${{ secrets.FLY_API_TOKEN }}

Monorepo — env-specific app + local build

jobs:
  deploy-staging:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: fly-deploy-staging
        uses: "collabops/fly-deploy@v1"
        with:
          api-token: ${{ secrets.FLY_API_TOKEN }}
          # Override when the deploy targets a different app than fly.toml's [app].
          app-name: web-staging
          # Build inside the Job container instead of Fly's remote builder — needs BuildKit.
          remote-only: "false"
          working-directory: /workspace/source/apps/web

Key points — Only set app-name when the deploy targets an app different from [app] in fly.toml. remote-only defaults to true (uses Fly's remote builder) — set "false" only when you provide BuildKit yourself. Use the official flyio/flyctl image for the Job container.

collabops/convex-deploy@v1

On-Premise: ❌ — requires Convex connectivity

Deploys Convex Functions. Optionally runs a build command.

InputRequiredDefaultDescription
deploy-keyYES-Convex Deploy Key. $\{\{ secrets.CONVEX_DEPLOY_KEY \}\} recommended
cmdNO""Build command to run after deploy
working-directoryNO"/workspace/source"Project root path

Examples

Basic — deploy Convex functions

jobs:
  deploy:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: npm-install
        run: npm ci
        image: node:22-alpine
      # deploy-key is the Convex Production / Preview deploy key from the dashboard.
      - name: convex-deploy
        uses: "collabops/convex-deploy@v1"
        with:
          deploy-key: ${{ secrets.CONVEX_DEPLOY_KEY }}

Run a build command after deploy

jobs:
  deploy:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: npm-install
        run: npm ci
        image: node:22-alpine
      - name: convex-deploy-with-cmd
        uses: "collabops/convex-deploy@v1"
        with:
          deploy-key: ${{ secrets.CONVEX_DEPLOY_KEY }}
          # Trigger downstream builds (e.g. Next.js prerender) immediately after the deploy.
          cmd: "npm run build:next"

Key points — Store a separate deploy-key secret per environment (production / preview). cmd runs after Convex schema + codegen finishes, so it is the right place to trigger builds that depend on Convex types (Next.js prerender, etc.).

collabops/ssh-exec@v1

On-Premise: ✅ — works in air-gapped environments

Connects to a remote host over SSH and executes a shell script. Use this for deploy scripts, service restarts, migrations, or any ad-hoc remote command.

The runtime image is pinned to alpine/git:2.43.0 and performs no extra package installation at runtime (no apk/apt), so it works in air-gapped environments out of the box.

Prerequisites

You need an SSH private key for the target host, plus a known_hosts value obtained beforehand via ssh-keyscan -p &lt;port&gt; &lt;host&gt;. Store both as CollabOps secrets and inject them at runtime.

known-hosts is the host-key verification value that prevents MITM (man-in-the-middle) attacks, so it is required. Leaving it empty would let SSH connect even when the host key changes.

SecretDescription
DEPLOY_SSH_PRIVATE_KEYOpenSSH private key for the remote host
DEPLOY_KNOWN_HOSTSOutput of ssh-keyscan -p &lt;port&gt; &lt;host&gt;

Inputs

InputRequiredDefaultDescription
hostYES-Remote host (IP or domain)
usernameYES-SSH username
portNO"22"SSH port
ssh-keyYES-SSH private key contents (OpenSSH format). $\{\{ secrets.DEPLOY_SSH_PRIVATE_KEY \}\} recommended
known-hostsYES-known_hosts contents (output of ssh-keyscan -p &lt;port&gt; &lt;host&gt;). Required to prevent MITM (man-in-the-middle) attacks
scriptYES-Shell script to run on the remote host (multi-line supported). set -eu is automatically prepended and the script is executed via bash -s to prevent silent failures

Usage

steps:
  - name: Restart service over SSH
    uses: collabops/ssh-exec@v1
    with:
      host: ${{ vars.DEPLOY_HOST }}
      username: ${{ vars.DEPLOY_USER }}
      ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
      known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
      script: |
        cd /opt/myapp
        docker compose pull
        docker compose up -d --remove-orphans

Fan out the same script to multiple hosts

# strategy.matrix is unsupported. Declare a Job per host — Jobs run in parallel by default.
jobs:
  restart-web1:
    steps:
      - name: ssh-restart
        uses: "collabops/ssh-exec@v1"
        with:
          host: web1.prod
          username: deploy
          ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
          known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          script: |
            sudo systemctl restart api
            sudo systemctl status api --no-pager

  restart-web2:
    steps:
      - name: ssh-restart
        uses: "collabops/ssh-exec@v1"
        with:
          host: web2.prod
          username: deploy
          ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
          known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          script: |
            sudo systemctl restart api
            sudo systemctl status api --no-pager

  restart-web3:
    steps:
      - name: ssh-restart
        uses: "collabops/ssh-exec@v1"
        with:
          host: web3.prod
          username: deploy
          ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
          known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          script: |
            sudo systemctl restart api
            sudo systemctl status api --no-pager

Key pointsstrategy.matrix is unsupported, so declare a Job per host. Jobs without needs run in parallel by default. The known-hosts secret must include ssh-keyscan output for every host.

collabops/scp-upload@v1

On-Premise: ✅ — works in air-gapped environments

Uploads a file or directory to a remote host over SCP. Use this to ship compose files, static assets, or build artifacts to a deploy server.

The runtime image is pinned to alpine/git:2.43.0 with no extra package installation at runtime, so it works in air-gapped environments out of the box.

known-hosts is required for the same reason as ssh-exec: it prevents MITM (man-in-the-middle) attacks via host-key verification. Storing both values as CollabOps secrets and injecting them via inputs is recommended.

SecretDescription
DEPLOY_SSH_PRIVATE_KEYOpenSSH private key for remote access
DEPLOY_KNOWN_HOSTSOutput of ssh-keyscan -p &lt;port&gt; &lt;host&gt;

Inputs

InputRequiredDefaultDescription
hostYES-Remote host (IP or domain)
usernameYES-SSH username
portNO"22"SSH port
ssh-keyYES-SSH private key contents (OpenSSH format). $\{\{ secrets.DEPLOY_SSH_PRIVATE_KEY \}\} recommended
known-hostsYES-known_hosts contents (output of ssh-keyscan -p &lt;port&gt; &lt;host&gt;). Required to prevent MITM (man-in-the-middle) attacks
sourceYES-Local path to upload (file or directory). Absolute path under /workspace/source recommended
targetYES-Remote target path. Trailing slash recommended for directories. Paths containing spaces are not supported due to scp user@host:path syntax limits
recursiveNO"false"Recursively upload a directory (true/false). Set to true when source is a directory

Usage

steps:
  - name: Upload compose file
    uses: collabops/scp-upload@v1
    with:
      host: ${{ vars.DEPLOY_HOST }}
      username: ${{ vars.DEPLOY_USER }}
      ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
      known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
      source: "/workspace/source/docker-compose.yml"
      target: "/opt/myapp/docker-compose.yml"

Upload a directory and trigger a remote reload

jobs:
  release:
    steps:
      - name: checkout
        uses: "collabops/checkout@v2"
        with:
          repo-url: "https://<collabops-host>/<workspace>/<repository>.git"
      - name: build-dist
        run: |
          npm ci && npm run build       # writes dist/
        image: node:22-alpine
      # 1) Push build output to the remote host.
      - name: upload-dist
        uses: "collabops/scp-upload@v1"
        with:
          host: web1.prod
          username: deploy
          ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
          known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          source: dist
          target: /var/www/app/current
      # 2) Reload nginx remotely.
      - name: reload-nginx
        uses: "collabops/ssh-exec@v1"
        with:
          host: web1.prod
          username: deploy
          ssh-key: ${{ secrets.DEPLOY_SSH_PRIVATE_KEY }}
          known-hosts: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
          script: |
            sudo nginx -t
            sudo systemctl reload nginx

Key points — Pairing scp-upload with an immediate ssh-exec reload/restart is the common deploy pattern. Both steps share the same SSH secrets, so prepare credentials once at the Job level.

Table of Contents