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
.envfile 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 1What this does:
- 1Triggers a deploy by POSTing to
/v1/projects/:id/deploy. - 2Injects
PUBLIC_API_URLfrom GitHub Secrets (not hard-coded in the repo). - 3Extracts the
deploymentIdfrom the response. - 4Polls
/v1/projects/:id/deployments/:deploymentIdevery 10 seconds. - 5Exits 0 if the status is
COMPLETED, exits 1 ifFAILEDor 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— yourpsk_live_...tokenPANDASTACK_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/)