Back to Blog
Tutorial11 min read2026-08-08

Automate SvelteKit Deployments with GitHub Actions and the API

Deploy SvelteKit on every push to main using GitHub Actions — script builds with the REST API, inject secrets from GitHub, and handle deployment failures in CI.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

You've deployed a SvelteKit app manually via the dashboard. It works. Now you want every push to main to deploy automatically, without clicking buttons or running commands locally. GitHub Actions can do this, but instead of installing a third-party action from the marketplace, you call the platform's REST API directly. Full control, no vendor lock-in, and you can debug the deployment in CI logs.

Why script deployments in CI

Manual deploys are fine for side projects. For production apps:

  • You want every merged PR to go live automatically.
  • You need to inject secrets (API keys, database URLs) from GitHub Secrets, not from a .env file in the repo.
  • You want deployment failures to fail the CI build, so you catch broken deploys before users see them.

The platform's API supports all of this. You POST to /v1/projects/:id/deploy, pass environment variables as JSON, and poll the deployment status until it's COMPLETED or FAILED.

Setting up a project-scoped token

GitHub Actions needs a credential to call the API. A psk_ token (project-scoped key) is the right type — it bakes in the organization, so you don't need to pass x-organization-id headers.

Generate one via the dashboard (Settings → API Keys → Create Key) or via the API:

curl -X POST https://api.pandastack.io/v1/tokens \
  -H "Authorization: Bearer $SESSION_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "github-actions",
    "type": "project"
  }'

Response: { "success": true, "data": { "token": "psk_live_abc123..." } }.

Add it to GitHub Secrets (repo Settings → Secrets → New repository secret):

  • Name: PANDASTACK_TOKEN
  • Value: psk_live_abc123...

Creating the SvelteKit project

If you haven't deployed the app yet, create it via the API:

curl -X POST https://api.pandastack.io/v1/projects \
  -H "Authorization: Bearer $PANDASTACK_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "slug": "static",
    "name": "sveltekit-app",
    "repositoryName": "yourorg/sveltekit-app",
    "branch": "main",
    "autoDeploy": false,
    "buildCommand": "npm run build",
    "outputDir": "build",
    "env": [
      { "name": "PUBLIC_API_URL", "value": "https://api.example.com" }
    ]
  }'

Set autoDeploy: false — you want GitHub Actions to trigger deploys, not the platform's built-in webhook.

Response: { "success": true, "data": { "projectId": "proj_xyz123", ... } }.

Save the projectId. You'll use it in the GitHub Actions workflow.

The GitHub Actions workflow

Create .github/workflows/deploy.yml:

name: Deploy to PandaStack

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Trigger deploy
        run: |
          RESPONSE=$(curl -s -X POST https://api.pandastack.io/v1/projects/${{ secrets.PANDASTACK_PROJECT_ID }}/deploy \
            -H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d '{
              "env": [
                { "name": "PUBLIC_API_URL", "value": "${{ secrets.PUBLIC_API_URL }}" }
              ]
            }')

          echo "Deploy response: $RESPONSE"

          DEPLOYMENT_ID=$(echo "$RESPONSE" | jq -r '.data.deploymentId')
          if [ "$DEPLOYMENT_ID" = "null" ]; then
            echo "Failed to trigger deploy"
            exit 1
          fi

          echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV

      - name: Wait for deploy to complete
        run: |
          for i in {1..60}; do
            STATUS=$(curl -s https://api.pandastack.io/v1/projects/${{ secrets.PANDASTACK_PROJECT_ID }}/deployments/$DEPLOYMENT_ID \
              -H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
              | jq -r '.data.status')

            echo "Deployment status: $STATUS"

            if [ "$STATUS" = "COMPLETED" ]; then
              echo "Deploy succeeded!"
              exit 0
            elif [ "$STATUS" = "FAILED" ]; then
              echo "Deploy failed!"
              exit 1
            fi

            sleep 10
          done

          echo "Deploy timed out after 10 minutes"
          exit 1

What this does:

  1. 1Triggers a deploy by POSTing to /v1/projects/:id/deploy.
  2. 2Injects PUBLIC_API_URL from GitHub Secrets (not hard-coded in the repo).
  3. 3Extracts the deploymentId from the response.
  4. 4Polls /v1/projects/:id/deployments/:deploymentId every 10 seconds.
  5. 5Exits 0 if the status is COMPLETED, exits 1 if FAILED or timed out.

If the deploy fails, the CI build fails, and you get a GitHub notification.

Adding GitHub Secrets

Go to repo Settings → Secrets → Actions, and add:

  • PANDASTACK_TOKEN — your psk_live_... token
  • PANDASTACK_PROJECT_ID — the project ID from the create response (e.g. proj_xyz123)
  • PUBLIC_API_URL — the backend API endpoint (e.g. https://api.example.com)

Now every push to main triggers a deploy with these secrets injected.

Handling multiple environments (staging and production)

For staging deploys from a staging branch:

on:
  push:
    branches:
      - main
      - staging

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Set environment
        run: |
          if [ "${{ github.ref }}" = "refs/heads/main" ]; then
            echo "PROJECT_ID=${{ secrets.PROD_PROJECT_ID }}" >> $GITHUB_ENV
            echo "API_URL=${{ secrets.PROD_API_URL }}" >> $GITHUB_ENV
          else
            echo "PROJECT_ID=${{ secrets.STAGING_PROJECT_ID }}" >> $GITHUB_ENV
            echo "API_URL=${{ secrets.STAGING_API_URL }}" >> $GITHUB_ENV
          fi

      - name: Trigger deploy
        run: |
          curl -s -X POST https://api.pandastack.io/v1/projects/$PROJECT_ID/deploy \
            -H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
            -H "Content-Type: application/json" \
            -d "{\"env\": [{\"name\": \"PUBLIC_API_URL\", \"value\": \"$API_URL\"}]}"

Create two projects (one for main, one for staging) and store their IDs in GitHub Secrets (PROD_PROJECT_ID and STAGING_PROJECT_ID).

Debugging deploy failures in CI

If the workflow exits with "Deploy failed!", check the API response:

- name: Trigger deploy
  run: |
    RESPONSE=$(curl -s -X POST ...)
    echo "Deploy response: $RESPONSE"

Look for "success": false and read the message field. Common failures:

  • Invalid projectId → "Project not found"
  • Missing environment variable → build fails during npm run build
  • Syntax error in the deploy payload → 400 Bad Request

The platform's build logs are accessible via the dashboard (Projects → select project → Deployments → select deployment → Logs). For CI, you'd poll /v1/projects/:id/deployments/:deploymentId/logs to fetch them programmatically.

Comparing this to auto-deploy webhooks

The platform's built-in autoDeploy feature triggers a deploy on every push to the configured branch. It works, but:

  • You can't inject secrets from GitHub Secrets (environment variables are set in the dashboard, not per-deploy).
  • You can't run pre-deploy or post-deploy scripts (e.g. running tests before deploying).
  • You can't deploy conditionally (e.g. skip deploy if the commit message includes [skip ci]).

GitHub Actions gives you all of this. The trade-off: you write the YAML yourself.

Using the CLI instead of the API

For one-off deploys or local testing, the panda CLI is simpler:

panda login
panda projects deploy <project-id>

The CLI reads environment variables from your shell or from a .env file. For CI, the API is more scriptable.

What you've deployed

A SvelteKit static site that auto-deploys on every push to main, with environment variables injected from GitHub Secrets. The workflow polls the deployment status and fails the CI build if the deploy fails. You get GitHub notifications on success or failure, and you can see build logs in the dashboard.

SvelteKit's adapter-static outputs pre-rendered HTML to build/. The platform uploads this to a CDN and serves it with aggressive caching (hashed assets cached for a year, index.html revalidated). Redeployments purge the cache automatically.

Free-tier apps get 300 build pipeline minutes per month (enough for ~100 deploys if each build takes 3 minutes). Pro ($15/mo) increases this to 1000 minutes. If you hit the limit, builds queue until the next billing cycle or you upgrade.

The API is rate-limited (1000 requests per hour per organization). For typical CI workflows (1 deploy per push, maybe 50 pushes per day), you won't hit this. For mass automation (e.g. deploying 100 projects simultaneously), contact support for higher limits.

References

  • [SvelteKit adapter-static](https://kit.svelte.dev/docs/adapter-static)
  • [GitHub Actions syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
  • [PandaStack API projects endpoints](https://docs.pandastack.io/api/projects/)
  • [PandaStack static sites](https://docs.pandastack.io/projects/static/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also