Deployment
Automated Deployments with GitHub Actions

Automate your code deployments with Carpathian Cloud. Easily push code to your development and production spark servers.

10 min read
394 views
Automated Deployments with GitHub Actions

GitHub Actions Deployment

Deploy code to your Carpathian server automatically when you push to GitHub. Uses GitHub Actions to trigger deployments via the Carpathian API.

What You Need

  • A running Carpathian Spark Server
  • A GitHub repository with your code
  • An API key with Deployment scope

How It Works

When you push to a configured branch, GitHub Actions sends a request to the Carpathian API. Carpathian then SSHs into your server and runs your configured commands in order:

  1. Before Deploy hook (optional) — e.g. enable maintenance mode
  2. Git pull — fetches and checks out the latest code
  3. Command groups — each group runs its build and restart commands in order
  4. After Deploy hook (optional) — e.g. disable maintenance mode

Step 1: Configure Deployment

  1. Go to Cloud Servers in the dashboard
  2. Click Actions on your server card
  3. Select Configure Deployment
  4. Fill in the settings:

Repository Settings

  • Repository URL (optional) — your GitHub repo URL. If blank, uses the URL from the GitHub webhook.
  • Branch (required) — the branch to deploy from (e.g. main)

Lifecycle Hooks

These run once per deployment, wrapping all command groups.

  • Before Deploy — runs first, before git pull. Use for things like enabling maintenance mode (php artisan down) or stopping workers (systemctl stop worker).
  • After Deploy — runs last, after all command groups finish. Use for disabling maintenance mode (php artisan up) or restarting workers.

Command Groups

Command groups are the core of the deployment. Each group represents one application stack (backend or frontend) with its own directory, build, and restart commands.

Click Add Backend or Add Frontend to create a group. Each group has:

  • Type — Backend or Frontend. This filters the framework templates shown.
  • Framework — pre-configured templates (Django, Flask, Next.js, React, etc.) that auto-fill build and restart commands. Select "Custom" for manual configuration.
  • Label (optional) — a display name for the group (e.g. "API Server", "Admin Panel")
  • Deploy Directory — absolute path where the code lives on your server (e.g. /var/www/app)
  • Build Command — install dependencies and build (e.g. npm install && npm run build)
  • Restart Command — restart the application process (e.g. pm2 restart app)

Groups execute top-to-bottom. For a typical dual-stack setup:

1. Before Deploy     →  php artisan down
2. Git pull          →  git fetch && checkout && pull
3. Backend group     →  cd /var/www/api && pip install && systemctl restart gunicorn
4. Frontend group    →  cd /var/www/frontend && npm install && npm run build && pm2 restart frontend
5. After Deploy      →  php artisan up

Running frontend and backend on the same server is less secure than separating them. A compromise in one layer could affect the other. For production workloads, deploy frontend and backend on separate servers.

  1. Toggle Enable Deployment to ON
  2. Click Save Configuration

Step 2: Generate an API Key

  1. On the Cloud Servers page, click Actions on your server card
  2. Select Manage API Keys
  3. Click Generate New API Key
  4. Set the permission scope to Deployment
  5. Copy the key immediately — it is only shown once

The key format is cpk_.... You can also manage keys from API Keys in the dashboard sidebar.

Step 3: Add the Key to GitHub Secrets

  1. Go to your GitHub repository
  2. Click Settings > Secrets and variables > Actions
  3. Click New repository secret
  4. Name: CARPATHIAN_API_KEY
  5. Value: your cpk_... key
  6. Click Add secret

Step 4: Create the Workflow

Create .github/workflows/deploy.yml in your repository:

name: Deploy to Carpathian

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout code
        uses: actions/checkout@v3

      - name: Trigger Carpathian Deployment
        env:
          CARPATHIAN_API_KEY: ${{ secrets.CARPATHIAN_API_KEY }}
          # Pass commit data through the environment so the shell — not the
          # YAML templater — handles it. This keeps special characters safe.
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
          AUTHOR_NAME: ${{ github.event.head_commit.author.name }}
          AUTHOR_EMAIL: ${{ github.event.head_commit.author.email }}
        run: |
          # Build the JSON body with jq so the payload is always valid, even
          # when the commit message contains newlines, quotes, or other
          # special characters (e.g. merge commits or trailers like
          # "Co-Authored-By:"). jq is pre-installed on GitHub-hosted runners.
          payload=$(jq -n \
            --arg repository    "${{ github.repository }}" \
            --arg commit        "${{ github.sha }}" \
            --arg branch        "${{ github.ref_name }}" \
            --arg commit_message "$COMMIT_MESSAGE" \
            --arg author_name   "$AUTHOR_NAME" \
            --arg author_email  "$AUTHOR_EMAIL" \
            --arg webhook_url   "${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}" \
            '{
              repository:     $repository,
              commit:         $commit,
              branch:         $branch,
              commit_message: $commit_message,
              author_name:    $author_name,
              author_email:   $author_email,
              webhook_url:    $webhook_url
            }')

          # -f makes curl fail (non-zero exit) on HTTP 4xx/5xx, so a rejected
          # request fails the workflow instead of reporting a false success.
          curl -fsS -X POST https://api.carpathian.ai/api/v1/deployment/trigger \
            -H "Authorization: Bearer $CARPATHIAN_API_KEY" \
            -H "Content-Type: application/json" \
            -d "$payload"

Why jq instead of inline JSON? Embedding ${{ github.event.head_commit.message }} directly inside a JSON string breaks whenever the commit message spans multiple lines or contains a " — the result is invalid JSON and the API rejects the request. Building the body with jq escapes everything correctly, so deployments work regardless of how your commit messages are formatted.

Step 5: Test

git add .github/workflows/deploy.yml
git commit -m "Add Carpathian deployment workflow"
git push origin main

Check the Deployments page in your dashboard to see the deployment status and logs.

Framework Examples

Backend

Django

Build:    pip install -r requirements.txt && python manage.py migrate && python manage.py collectstatic --noinput
Restart:  systemctl restart gunicorn

Flask

Build:    pip install -r requirements.txt
Restart:  systemctl restart gunicorn

Node.js / Express

Build:    npm install
Restart:  pm2 restart node-app || pm2 start server.js --name node-app

Laravel

Build:    composer install --no-dev && php artisan migrate --force && php artisan config:cache && php artisan route:cache && php artisan view:cache
Restart:  php artisan queue:restart && systemctl restart php-fpm

Frontend

Next.js

Build:    npm install && npm run build
Restart:  pm2 restart nextjs-app || pm2 start npm --name nextjs-app -- start

React (Vite/CRA)

Build:    npm install && npm run build
Restart:  pm2 restart react-app || pm2 start serve --name react-app -- -s build -l 3000

Vue.js

Build:    npm install && npm run build
Restart:  pm2 restart vue-app || pm2 start serve --name vue-app -- -s dist -l 3000

Workflow Customization

Deploy on multiple branches:

on:
  push:
    branches: [main, staging, production]

Deploy on pull request merge:

on:
  pull_request:
    types: [closed]
    branches: [main]

Manual trigger:

on:
  workflow_dispatch:

Multiple environments with separate API keys:

Each job builds its payload with jq exactly as in Step 4 — only the API key secret differs per environment.

jobs:
  deploy-staging:
    if: github.ref == 'refs/heads/staging'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Staging
        env:
          CARPATHIAN_API_KEY: ${{ secrets.CARPATHIAN_STAGING_KEY }}
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
        run: |
          payload=$(jq -n \
            --arg repository "${{ github.repository }}" \
            --arg commit "${{ github.sha }}" \
            --arg branch "${{ github.ref_name }}" \
            --arg commit_message "$COMMIT_MESSAGE" \
            '{repository:$repository, commit:$commit, branch:$branch, commit_message:$commit_message}')
          curl -fsS -X POST https://api.carpathian.ai/api/v1/deployment/trigger \
            -H "Authorization: Bearer $CARPATHIAN_API_KEY" \
            -H "Content-Type: application/json" \
            -d "$payload"

  deploy-production:
    if: github.ref == 'refs/heads/main'
    runs-on: ubuntu-latest
    steps:
      - name: Deploy to Production
        env:
          CARPATHIAN_API_KEY: ${{ secrets.CARPATHIAN_PRODUCTION_KEY }}
          COMMIT_MESSAGE: ${{ github.event.head_commit.message }}
        run: |
          payload=$(jq -n \
            --arg repository "${{ github.repository }}" \
            --arg commit "${{ github.sha }}" \
            --arg branch "${{ github.ref_name }}" \
            --arg commit_message "$COMMIT_MESSAGE" \
            '{repository:$repository, commit:$commit, branch:$branch, commit_message:$commit_message}')
          curl -fsS -X POST https://api.carpathian.ai/api/v1/deployment/trigger \
            -H "Authorization: Bearer $CARPATHIAN_API_KEY" \
            -H "Content-Type: application/json" \
            -d "$payload"

Checking Deployment Status

The easiest way to check deployment status is the Deployments page in your dashboard. The Deployment Logs tab shows all deployments with their status, commit info, and full output logs.

If you need to check status programmatically (e.g. in a CI pipeline), the trigger endpoint returns a deployment_id in its response. You can use that ID with the status and logs endpoints. These endpoints require the same API key used to trigger the deployment — you can only query deployments that belong to your server.

# Check status (deployment_id is returned by the trigger response)
curl -H "Authorization: Bearer $CARPATHIAN_API_KEY" \
  https://api.carpathian.ai/api/v1/deployment/status/DEPLOYMENT_ID

# Get full logs
curl -H "Authorization: Bearer $CARPATHIAN_API_KEY" \
  https://api.carpathian.ai/api/v1/deployment/logs/DEPLOYMENT_ID

Troubleshooting

"Deployment Not Configured"

You haven't set up deployment on this server yet. Go to Cloud Servers > Actions > Configure Deployment.

Deployment not triggering

  • Check that the API key status is Active (not Locked or Revoked)
  • Verify the key has Deployment scope
  • Confirm CARPATHIAN_API_KEY exists in GitHub Secrets with the full cpk_... value
  • Check the GitHub Actions tab for workflow errors
  • Confirm the branch you pushed matches the branch in your workflow's on.push.branches and the branch configured in Configure Deployment
  • Verify all API calls use the base URL https://api.carpathian.ai

Workflow shows "success" but no deployment appears / "internal error occurred"

This almost always means the request body was malformed JSON. The most common cause is a commit message containing newlines or quotes (merge commits, or trailers like Co-Authored-By:) being inlined directly into the JSON payload.

  • Use the jq-based workflow from Step 4 — it escapes the commit message so the payload is always valid JSON.
  • Include -f in your curl command (curl -fsS ...) so a rejected request actually fails the workflow instead of reporting a false "success."

"Invalid API Key"

The key may be revoked, deleted, or locked. Check status in API Keys. Generate a new key if needed.

API Key Locked

Keys auto-lock after 3 failed IP allowlist checks within 5 minutes. Go to the key details, review Security Events, then click Unlock.

Deployment triggered but nothing changed

  • Check the Deployments page for logs and error messages
  • Verify your deploy directory path is correct
  • Confirm build and restart commands work when run manually via SSH

Security

  • Use Deployment scope — avoid Full Access for CI/CD
  • Never commit API keys to git — always use GitHub Secrets
  • Use IP allowlists to restrict key usage to GitHub Actions IPs (185.199.108.0/22, 140.82.112.0/20, 143.55.64.0/20)
  • Revoke keys that haven't been used in 90+ days
  • Monitor key usage and security events in the dashboard

This documentation is open to the public to make the platform API available for customers. However, since Carpathian Cloud is still in beta and being actively developed, this document might be outdated or incorrect. If you find anything confusing or misleading, please send an email to info@carpathian.ai.