Back to Blog
Tutorial12 min read2026-08-13

Scripting Deploys in CI — Preact and the PandaStack API

Automate Preact deployments from GitHub Actions using the PandaStack REST API — psk_ tokens, project creation, build triggers, and deployment status checks in CI.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

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:

  1. 1Navigate to Settings → API Tokens
  2. 2Click Create Token
  3. 3Name it github-actions-deploy
  4. 4Copy the token (it starts with psk_live_)

Store the token as a GitHub secret:

  1. 1Go to your repository → Settings → Secrets and variables → Actions
  2. 2Click New repository secret
  3. 3Name: PANDASTACK_TOKEN
  4. 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 }}"
    fi

The 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
    done

The 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)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also