Most developers expect CI to handle deployment, not a manual button in a dashboard. When you want git push to trigger a production deploy without leaving your existing GitHub Actions workflow, the PandaStack REST API is the correct tool. It accepts a bearer token, returns structured responses, and integrates cleanly into any CI pipeline.
This post walks through building a GitHub Actions workflow that deploys a Node.js app on every push to main, checking build status and failing the workflow if the deploy breaks.
Why the API instead of the CLI
The panda CLI is designed for interactive use and handles authentication through a login flow. GitHub Actions is headless, so a bearer token (psk_live_...) is the right choice. The API takes a psk_ token via the Authorization header, and the organization is baked into the token—no additional headers needed.
A psk_ token is scoped to your organization and never expires unless you revoke it. Generate one from Settings → API Tokens in the dashboard, then store it as a GitHub secret.
The API endpoint for deploys
The core endpoint is POST /v1/projects/:id/deploy. You need the numeric project ID, which you get when you first create the project or from GET /v1/projects. The request looks like this:
curl -X POST https://api.pandastack.io/v1/projects/42/deploy \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json"No body is required—it triggers a redeploy of the current branch. The response envelope is:
{
"success": true,
"data": {
"deploymentId": 123,
"deploymentUuid": "abc-def-ghi",
"status": "PENDING"
}
}The deploymentUuid is what you use to poll build logs or check final status.
GitHub Actions workflow
Start with a minimal workflow file at .github/workflows/deploy.yml:
name: Deploy to PandaStack
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger deployment
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")
echo "$response"
success=$(echo "$response" | jq -r '.success')
if [ "$success" != "true" ]; then
echo "Deploy trigger failed"
exit 1
fi
deployment_id=$(echo "$response" | jq -r '.data.deploymentId')
echo "DEPLOYMENT_ID=$deployment_id" >> $GITHUB_ENV
- name: Wait for deployment
run: |
echo "Deployment ${{ env.DEPLOYMENT_ID }} started. Check logs at https://dashboard.pandastack.io"You need two GitHub secrets:
PANDASTACK_TOKEN: yourpsk_live_...API keyPANDASTACK_PROJECT_ID: the numeric ID of the project (e.g.,42)
This workflow triggers a deploy and prints the deployment ID. The actual build happens asynchronously on PandaStack's infrastructure, so the workflow doesn't block on build completion. If you want to fail the CI job when the build breaks, add a polling step.
Polling for build status
Add a step that queries the deployment status every 10 seconds:
- name: Wait for deployment to finish
run: |
deployment_id=${{ env.DEPLOYMENT_ID }}
max_wait=300
elapsed=0
while [ $elapsed -lt $max_wait ]; do
status_response=$(curl -s \
https://api.pandastack.io/v1/projects/${{ secrets.PANDASTACK_PROJECT_ID }}/deployments/$deployment_id \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}")
status=$(echo "$status_response" | jq -r '.data.status')
echo "Status: $status"
if [ "$status" = "RUNNING" ]; then
echo "Deployment succeeded"
exit 0
elif [ "$status" = "FAILED" ]; then
echo "Deployment failed"
exit 1
fi
sleep 10
elapsed=$((elapsed + 10))
done
echo "Deployment timed out after ${max_wait}s"
exit 1This polls GET /v1/projects/:id/deployments/:deploymentId until the status is RUNNING or FAILED. If the build breaks, the workflow fails. If the deploy succeeds, the workflow passes. The 5-minute timeout prevents the job from hanging indefinitely.
Creating a project from scratch via API
If you don't have a project yet, create one from the workflow:
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"slug": "auto",
"name": "my-api",
"repositoryName": "acme/my-api",
"branch": "main",
"autoDeploy": true,
"env": [
{ "name": "NODE_ENV", "value": "production" },
{ "name": "PORT", "value": "3000" }
]
}'The response includes projectId, which you store as the GitHub secret. "slug": "auto" auto-detects static vs container; use "static" or "container" to force a type. The env array injects environment variables at runtime.
Streaming build logs (optional)
The API exposes build logs at GET /v1/projects/:id/logs?deploymentId=:deploymentId. The response is line-delimited log output. You can fetch it in the workflow and print it inline:
- name: Fetch build logs
run: |
curl -s "https://api.pandastack.io/v1/projects/${{ secrets.PANDASTACK_PROJECT_ID }}/logs?deploymentId=${{ env.DEPLOYMENT_ID }}" \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" | head -n 50This prints the first 50 lines. The full log is available in the dashboard Logs tab, which is more reliable for real-time tailing than the API.
Handling environment variable updates
If your deploy needs new env vars, set them before triggering:
curl -X POST https://api.pandastack.io/v1/projects/42/env \
-H "Authorization: Bearer $PANDASTACK_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"env": [
{ "name": "API_KEY", "value": "secret-value" }
]
}'Then trigger the deploy. The new variables are injected into the next build.
Why this matters
Heroku used to offer seamless GitHub integration with automatic deploys on push. When they removed the free tier in 2022, many teams migrated to platforms that required manual dashboard clicks or complex webhook setups. The PandaStack API brings back the push-to-deploy workflow without platform lock-in—your CI owns the deploy logic, and you can switch to a different platform by swapping the API endpoint.
The API is stateless, returns structured JSON, and works with any CI system (GitLab CI, CircleCI, Jenkins). GitHub Actions is just the most common case.
Full workflow example
Here's the complete .github/workflows/deploy.yml:
name: Deploy to PandaStack
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger deployment
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")
echo "$response"
success=$(echo "$response" | jq -r '.success')
[ "$success" != "true" ] && exit 1
deployment_id=$(echo "$response" | jq -r '.data.deploymentId')
echo "DEPLOYMENT_ID=$deployment_id" >> $GITHUB_ENV
- name: Wait for deployment
run: |
deployment_id=${{ env.DEPLOYMENT_ID }}
max_wait=300
elapsed=0
while [ $elapsed -lt $max_wait ]; do
status_response=$(curl -s \
https://api.pandastack.io/v1/projects/${{ secrets.PANDASTACK_PROJECT_ID }}/deployments/$deployment_id \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}")
status=$(echo "$status_response" | jq -r '.data.status')
echo "Status: $status"
[ "$status" = "RUNNING" ] && exit 0
[ "$status" = "FAILED" ] && exit 1
sleep 10
elapsed=$((elapsed + 10))
done
echo "Timeout"
exit 1Push this to your repo, configure the secrets, and the next push to main deploys automatically. The workflow fails if the build breaks, so you get immediate feedback in your pull request checks.
PandaStack's free tier includes 300 build pipeline minutes per month, which covers most side projects. Paid plans start at $15/mo for 1000 minutes. If your CI already runs tests and linting, the deploy step adds minimal overhead—typically under 2 minutes for a Node.js app with cached dependencies.
References
- [PandaStack API Documentation](https://docs.pandastack.io/api)
- [GitHub Actions Workflow Syntax](https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions)
- [Managing GitHub Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets)