Deploying from the dashboard works for manual releases, but production workflows need automation: every merge to main should trigger a build, run tests, deploy to staging, run smoke tests, then deploy to production. CI pipelines handle this, but most platforms require a proprietary CLI or GitHub App integration. The PandaStack API is simpler — it is a REST API with bearer token authentication, so any CI system that can run curl can deploy.
GitHub Actions is the most popular CI platform for open-source projects. A workflow file in .github/workflows/ listens for pushes to main, calls the PandaStack API to create a deployment, waits for the build to complete, and optionally runs post-deploy tests. The entire process runs without human intervention, and the API token is stored as a GitHub secret so it never appears in logs.
Generate a PandaStack API token for CI
The API supports two credential types: session JWTs (short-lived, tied to a user) and psk_ tokens (long-lived, scoped to an organization). CI needs a psk_ token because it runs unattended and should not depend on a user's session.
Generate a token in the dashboard:
- 1Navigate to Settings → API Tokens
- 2Click Create Token
- 3Name it
github-actions-deploy - 4Copy the token (it starts with
psk_live_)
Store the token as a GitHub secret:
- 1Go to your repository → Settings → Secrets and variables → Actions
- 2Click New repository secret
- 3Name:
PANDASTACK_TOKEN - 4Value: the
psk_live_token
GitHub Actions workflows access the secret via ${{ secrets.PANDASTACK_TOKEN }}, and it never appears in logs or workflow output.
Create a GitHub Actions workflow to deploy on every push
Create .github/workflows/deploy.yml:
name: Deploy to PandaStack
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger deployment
run: |
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"slug": "static",
"name": "preact-app",
"repositoryName": "${{ github.repository }}",
"branch": "main",
"autoDeploy": false,
"buildCommand": "npm run build",
"outputDir": "dist"
}'This workflow triggers on every push to main. It calls the PandaStack API to create or update a project named preact-app, then starts a deployment.
The autoDeploy: false flag prevents the platform from deploying on every commit outside of CI. Instead, deployments only happen when the workflow runs.
Handle project creation vs. redeployment in the same workflow
The first time the workflow runs, the project does not exist, so POST /v1/projects creates it. Subsequent runs should redeploy the existing project, not create a duplicate.
The API returns an error if you try to create a project with a name that already exists. Handle this by checking if the project exists before deciding whether to create or deploy:
- name: Get project ID
id: get_project
run: |
PROJECT=$(curl -s https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
| jq -r '.data[] | select(.name == "preact-app") | .id')
echo "project_id=$PROJECT" >> $GITHUB_OUTPUT
- name: Create or deploy project
run: |
if [ -z "${{ steps.get_project.outputs.project_id }}" ]; then
# Create new project
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"slug": "static",
"name": "preact-app",
"repositoryName": "${{ github.repository }}",
"branch": "main",
"buildCommand": "npm run build",
"outputDir": "dist"
}'
else
# Trigger deployment on existing project
curl -X POST https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}"
fiThe workflow queries the API for a project named preact-app. If it exists, it calls the deploy endpoint. If not, it creates the project, which automatically triggers the first deployment.
Wait for the deployment to complete before running tests
The deploy endpoint returns immediately, but the build runs asynchronously. If your workflow runs smoke tests after deploying, you need to poll the deployment status until it reaches RUNNING or FAILED.
Retrieve the deployment ID from the create/deploy response, then poll the status:
- name: Trigger deployment and get deployment ID
id: deploy
run: |
RESPONSE=$(curl -s -X POST https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}")
DEPLOYMENT_ID=$(echo $RESPONSE | jq -r '.data.deploymentId')
echo "deployment_id=$DEPLOYMENT_ID" >> $GITHUB_OUTPUT
- name: Wait for deployment to complete
run: |
while true; do
STATUS=$(curl -s https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deployments/${{ steps.deploy.outputs.deployment_id }} \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
| jq -r '.data.status')
if [ "$STATUS" == "RUNNING" ]; then
echo "Deployment succeeded"
exit 0
elif [ "$STATUS" == "FAILED" ]; then
echo "Deployment failed"
exit 1
fi
echo "Status: $STATUS, waiting..."
sleep 10
doneThe workflow polls the deployment status every 10 seconds. When the status is RUNNING, the deployment succeeded and the workflow proceeds to the next step. If the status is FAILED, the workflow exits with an error and GitHub marks the run as failed.
Run smoke tests against the deployed URL
Once the deployment completes, the app is live at a subdomain like https://preact-app-abc123.pandastack.app. Retrieve the URL from the deployment metadata and test it:
- name: Run smoke tests
run: |
URL=$(curl -s https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deployments/${{ steps.deploy.outputs.deployment_id }} \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
| jq -r '.data.url')
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" $URL)
if [ "$HTTP_CODE" -ne 200 ]; then
echo "Smoke test failed: expected 200, got $HTTP_CODE"
exit 1
fi
echo "Smoke test passed"The test fetches the root URL and checks the HTTP status code. If it is not 200, the workflow fails. You can extend this with more sophisticated tests: checking for specific text in the response, hitting API endpoints, or running a Playwright script.
Roll back automatically if smoke tests fail
If the smoke test fails, the deployment is live but broken. Roll back to the previous version by calling the rollback endpoint:
- name: Rollback on failure
if: failure()
run: |
PREVIOUS_DEPLOYMENT=$(curl -s https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deployments \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
| jq -r '.data | sort_by(.createdAt) | reverse | .[1].id')
curl -X POST https://api.pandastack.io/v1/projects/${{ steps.get_project.outputs.project_id }}/deployments/$PREVIOUS_DEPLOYMENT/rollback \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}"The workflow retrieves the second-most-recent deployment (the one before the current deploy) and rolls back to it. The broken version is replaced, and the app returns to the last known-good state.
Set environment variables in the workflow
If your Preact app needs environment variables at build time (API endpoints, feature flags), pass them in the env array:
- name: Create project with environment variables
run: |
curl -X POST https://api.pandastack.io/v1/projects \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}" \
-H "Content-Type: application/json" \
-d '{
"slug": "static",
"name": "preact-app",
"repositoryName": "${{ github.repository }}",
"branch": "main",
"buildCommand": "npm run build",
"outputDir": "dist",
"env": [
{ "name": "VITE_API_URL", "value": "https://api.example.com" },
{ "name": "VITE_ANALYTICS_ID", "value": "${{ secrets.ANALYTICS_ID }}" }
]
}'GitHub secrets are injected into the request payload, so sensitive values never appear in logs. The build receives the variables, Vite injects them into the bundle, and the app reads them from import.meta.env.
Use different workflows for staging and production
Production deploys should be more conservative: run more tests, require manual approval, or deploy only on tagged releases. Staging deploys can run on every push.
Create two workflows:
.github/workflows/deploy-staging.yml:
name: Deploy to Staging
on:
push:
branches:
- main
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to staging
run: |
curl -X POST https://api.pandastack.io/v1/projects/staging-preact-app/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}".github/workflows/deploy-production.yml:
name: Deploy to Production
on:
release:
types: [published]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to production
run: |
curl -X POST https://api.pandastack.io/v1/projects/production-preact-app/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}"The staging workflow runs on every push to main. The production workflow runs only when a GitHub release is published, which requires a manual action and ensures only vetted code reaches production.
References
- [PandaStack API documentation](https://docs.pandastack.io/api/)
- [GitHub Actions documentation](https://docs.github.com/en/actions)
- [Preact deployment guide](https://preactjs.com/guide/v10/getting-started#best-practices)