Add --self-hosted-endpoint with your management API hostname to each zuplo list and zuplo delete command so it targets your cluster. See zuplo deploy.
Delete Zuplo environments automatically when branches are deleted. This keeps
your environment list clean and avoids accumulating unused preview environments.
.github/workflows/cleanup-on-branch-delete.yaml
name: Cleanup on Branch Deleteon: delete:jobs: cleanup: # Only run for branch deletions, not tag deletions if: github.event.ref_type == 'branch' runs-on: ubuntu-latest env: ZUPLO_API_KEY: ${{ secrets.ZUPLO_API_KEY }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm install - name: Delete environment run: | # The deleted branch name BRANCH_NAME="${{ github.event.ref }}" # Deployment names use the format {project}-{branch}-{hash}, where # the branch segment is normalized (lowercase, special characters # replaced by hyphens) and truncated to 10 characters ENV_NAME=$(echo "$BRANCH_NAME" | tr '/_' '--' | tr '[:upper:]' '[:lower:]' | cut -c1-10 | sed 's/-*$//') # Find the deployment for the deleted branch and delete it by URL npx zuplo list --api-key "$ZUPLO_API_KEY" \ --output json --show-details | jq -r --arg env "$ENV_NAME" \ '.[] | select(.name | test("-" + $env + "-[a-z0-9]+$")) | .url' | while read -r URL; do echo "Deleting environment: $URL" npx zuplo delete --url "$URL" --api-key "$ZUPLO_API_KEY" --wait || true done
This workflow:
Triggers when any branch is deleted
Converts the branch name to the deployment-name branch segment format
Looks up the matching deployment and deletes it by URL
Continues without error if no matching environment exists
Combining with PR Cleanup
Use this as a backup for
PR preview environments. The PR workflow
handles cleanup when PRs close, but this catches cases where:
Someone deletes a branch without closing the PR first
A branch was pushed but never had a PR opened
The PR cleanup job failed
Scheduled Cleanup
For additional safety, run periodic cleanup to catch any orphaned environments:
.github/workflows/scheduled-cleanup.yaml
name: Scheduled Cleanupon: schedule: # Run daily at midnight UTC - cron: "0 0 * * *"jobs: cleanup: runs-on: ubuntu-latest env: ZUPLO_API_KEY: ${{ secrets.ZUPLO_API_KEY }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # Fetch all branches - uses: actions/setup-node@v4 with: node-version: 24 - name: Install dependencies run: npm install - name: Cleanup stale environments run: | # Get all remote branches, converted to the deployment-name branch # segment format: lowercase, special characters replaced by hyphens, # truncated to 10 characters BRANCHES=$(git branch -r | sed 's|origin/||' | tr -d ' ' | tr '/_' '--' | tr '[:upper:]' '[:lower:]' | cut -c1-10 | sed 's/-*$//') # List deployed environments as JSON. Each entry has the shape # {"projectName": "...", "name": "...", "url": "..."} npx zuplo list --api-key "$ZUPLO_API_KEY" \ --output json --show-details > environments.json # Deployment names follow the format {project}-{branch}-{hash} jq -r '.[] | "\(.name) \(.url)"' environments.json | while read -r NAME URL; do # Skip protected environments case "$NAME" in *-main-* | *-production-* | *-staging-*) continue ;; esac # Delete only if no branch matches the deployment name STALE=true for BRANCH in $BRANCHES; do if [[ "$NAME" == *"-$BRANCH-"* ]]; then STALE=false break fi done if [[ "$STALE" == "true" ]]; then echo "Deleting stale environment: $NAME" npx zuplo delete --url "$URL" --api-key "$ZUPLO_API_KEY" --wait || true fi done