This commit is contained in:
2026-04-11 20:21:55 -05:00
commit fa31f3756a
5 changed files with 293 additions and 0 deletions

40
cleanup/action.yml Normal file
View File

@@ -0,0 +1,40 @@
name: Cleanup Preview
description: Stop container and remove Pangolin resource
inputs:
app-name:
required: true
description: Application name (used for image/container naming)
slug:
required: true
description: Branch slug to clean up
pangolin-api-url:
required: true
description: Base URL for the Pangolin API (https://api.pangolin.local)
pangolin-api-key:
required: true
description: Pangolin API key
pangolin-org-id:
required: true
description: Pangolin organization ID
runs:
using: composite
steps:
- name: Remove Pangolin resource
shell: bash
env:
PANGOLIN_API_URL: ${{ inputs.pangolin-api-url }}
PANGOLIN_API_KEY: ${{ inputs.pangolin-api-key }}
PANGOLIN_ORG_ID: ${{ inputs.pangolin-org-id }}
run: |
bash ${{ github.action_path }}/../scripts/pangolin-delete.sh \
--resource-name "${{ inputs.app-name }}-${{ inputs.slug }}"
- name: Stop preview container
shell: bash
run: |
CONTAINER="${{ inputs.app-name }}-${{ inputs.slug }}"
docker stop "$CONTAINER" 2>/dev/null || true
docker rm "$CONTAINER" 2>/dev/null || true
docker rmi "${{ inputs.app-name }}:${{ inputs.slug }}" 2>/dev/null || true

85
deploy/action.yml Normal file
View File

@@ -0,0 +1,85 @@
name: Deploy Container
description: Build Docker image, deploy container, and register with Pangolin
inputs:
app-name:
required: true
description: Application name (used for image/container naming)
tag:
required: true
description: Image/container tag (e.g., "production" or a branch slug)
port:
required: true
description: Host port to expose
internal-port:
required: false
default: "3000"
description: Port the app listens on inside the container
environment:
required: true
description: Node environment name ("production" or "preview")
subdomain:
required: true
description: Subdomain for Pangolin resource
target-ip:
required: true
description: Target IP for Pangolin
build-args:
required: false
default: ""
description: Extra docker build args (space-separated KEY=VALUE pairs)
pangolin-api-url:
required: true
description: Base URL for the Pangolin API (https://api.pangolin.local)
pangolin-api-key:
required: true
description: Pangolin API key
pangolin-org-id:
required: true
description: Pangolin organization ID
pangolin-domain-id:
required: true
description: Pangolin domain ID
pangolin-site-id:
required: true
description: Pangolin site ID
runs:
using: composite
steps:
- name: Build Docker image
shell: bash
run: |
BUILD_ARGS="--build-arg APP_ENV=${{ inputs.environment }}"
for arg in ${{ inputs.build-args }}; do
BUILD_ARGS="$BUILD_ARGS --build-arg $arg"
done
docker build \
$BUILD_ARGS \
-t ${{ inputs.app-name }}:${{ inputs.tag }} \
-f dockerfile .
- name: Deploy container
shell: bash
run: |
bash ${{ github.action_path }}/../scripts/deploy.sh \
--name "${{ inputs.app-name }}" \
--tag "${{ inputs.tag }}" \
--port "${{ inputs.port }}" \
--internal-port "${{ inputs.internal-port }}" \
--env "${{ inputs.environment }}"
- name: Register Pangolin resource
shell: bash
env:
PANGOLIN_API_URL: ${{ inputs.pangolin-api-url }}
PANGOLIN_API_KEY: ${{ inputs.pangolin-api-key }}
PANGOLIN_ORG_ID: ${{ inputs.pangolin-org-id }}
PANGOLIN_DOMAIN_ID: ${{ inputs.pangolin-domain-id }}
PANGOLIN_SITE_ID: ${{ inputs.pangolin-site-id }}
run: |
bash ${{ github.action_path }}/../scripts/pangolin-upsert.sh \
--subdomain "${{ inputs.subdomain }}" \
--port "${{ inputs.port }}" \
--resource-name "${{ inputs.app-name }}-${{ inputs.tag }}" \
--target-ip "${{ inputs.target-ip }}"

41
scripts/deploy.sh Normal file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
set -euo pipefail
APP_NAME=""
TAG=""
PORT=""
INTERNAL_PORT="3000"
ENV=""
while [[ $# -gt 0 ]]; do
case $1 in
--name) APP_NAME="$2"; shift 2 ;;
--tag) TAG="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--internal-port) INTERNAL_PORT="$2"; shift 2 ;;
--env) ENV="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
CONTAINER="${APP_NAME}-${TAG}"
echo "→ Deploying container: ${CONTAINER} on port ${PORT}"
docker stop "${CONTAINER}" 2>/dev/null && docker rm "${CONTAINER}" 2>/dev/null || true
ENV_FILE_ARG=""
if [[ -f "/opt/apps/${APP_NAME}/.env.${ENV}" ]]; then
ENV_FILE_ARG="--env-file /opt/apps/${APP_NAME}/.env.${ENV}"
fi
docker run -d \
--name "${CONTAINER}" \
--restart unless-stopped \
-p "0.0.0.0:${PORT}:${INTERNAL_PORT}" \
-e NODE_ENV=production \
-e PORT="${INTERNAL_PORT}" \
${ENV_FILE_ARG} \
"${APP_NAME}:${TAG}"
echo "✓ Container ${CONTAINER} running on 0.0.0.0:${PORT}"

View File

@@ -0,0 +1,38 @@
#!/usr/bin/env bash
# Remove a Pangolin resource by name
set -euo pipefail
RESOURCE_NAME=""
while [[ $# -gt 0 ]]; do
case $1 in
--resource-name) RESOURCE_NAME="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
API="${PANGOLIN_API_URL}/v1"
AUTH="Authorization: Bearer ${PANGOLIN_API_KEY}"
echo "→ Pangolin delete: ${RESOURCE_NAME}"
RESOURCE_ID=$(curl -sf \
-H "${AUTH}" \
"${API}/org/${PANGOLIN_ORG_ID}/resources?limit=1000" \
| jq -r --arg name "${RESOURCE_NAME}" \
'.data.resources[] | select(.name == $name) | .resourceId' \
|| echo "")
if [[ -z "${RESOURCE_ID}" ]]; then
echo " No resource found with name '${RESOURCE_NAME}', skipping."
exit 0
fi
echo " Found resource ${RESOURCE_ID}, deleting…"
curl -sf -X DELETE \
-H "${AUTH}" \
"${API}/resource/${RESOURCE_ID}" \
> /dev/null
echo "✓ Resource ${RESOURCE_ID} (${RESOURCE_NAME}) removed from Pangolin"

View File

@@ -0,0 +1,89 @@
#!/usr/bin/env bash
# Idempotently register an HTTP resource + target in Pangolin
set -euo pipefail
SUBDOMAIN=""
PORT=""
RESOURCE_NAME=""
TARGET_IP=""
while [[ $# -gt 0 ]]; do
case $1 in
--subdomain) SUBDOMAIN="$2"; shift 2 ;;
--port) PORT="$2"; shift 2 ;;
--resource-name) RESOURCE_NAME="$2"; shift 2 ;;
--target-ip) TARGET_IP="$2"; shift 2 ;;
*) echo "Unknown arg: $1"; exit 1 ;;
esac
done
API="${PANGOLIN_API_URL}/v1"
AUTH="Authorization: Bearer ${PANGOLIN_API_KEY}"
echo "→ Pangolin upsert: ${RESOURCE_NAME}${SUBDOMAIN} on port ${PORT}"
# Check if resource already exists
EXISTING=$(curl -sf \
-H "${AUTH}" \
"${API}/org/${PANGOLIN_ORG_ID}/resources?limit=1000" \
| jq -r --arg name "${RESOURCE_NAME}" \
'.data.resources[] | select(.name == $name) | .resourceId' \
|| echo "")
if [[ -n "${EXISTING}" ]]; then
echo " Resource already exists (id=${EXISTING}), updating…"
RESOURCE_ID="${EXISTING}"
curl -sf -X POST \
-H "${AUTH}" \
-H "Content-Type: application/json" \
-d "{\"subdomain\": \"${SUBDOMAIN}\"}" \
"${API}/resource/${RESOURCE_ID}" \
> /dev/null
# Remove existing targets so we can re-register with potentially new port
TARGETS=$(curl -sf \
-H "${AUTH}" \
"${API}/resource/${RESOURCE_ID}/targets" \
| jq -r '.data.targets[].targetId' || echo "")
for TID in ${TARGETS}; do
curl -sf -X DELETE \
-H "${AUTH}" \
"${API}/target/${TID}" > /dev/null
echo " Removed old target ${TID}"
done
else
echo " Creating new resource…"
CREATE_RESP=$(curl -sf -X PUT \
-H "${AUTH}" \
-H "Content-Type: application/json" \
-d "{
\"name\": \"${RESOURCE_NAME}\",
\"http\": true,
\"subdomain\": \"${SUBDOMAIN}\",
\"domainId\": \"${PANGOLIN_DOMAIN_ID}\",
\"protocol\": \"tcp\"
}" \
"${API}/org/${PANGOLIN_ORG_ID}/resource")
RESOURCE_ID=$(echo "${CREATE_RESP}" | jq -r '.data.resourceId')
FULL_DOMAIN=$(echo "${CREATE_RESP}" | jq -r '.data.fullDomain')
echo " Created resource ${RESOURCE_ID}${FULL_DOMAIN}"
fi
# Add target
echo " Adding target localhost:${PORT} on site ${PANGOLIN_SITE_ID}"
TARGET_RESP=$(curl -sf -X PUT \
-H "${AUTH}" \
-H "Content-Type: application/json" \
-d "{
\"ip\": \"${TARGET_IP}\",
\"port\": ${PORT},
\"method\": \"http\",
\"siteId\": ${PANGOLIN_SITE_ID}
}" \
"${API}/resource/${RESOURCE_ID}/target")
TARGET_ID=$(echo "${TARGET_RESP}" | jq -r '.data.targetId')
echo "✓ Target ${TARGET_ID} registered for resource ${RESOURCE_ID}"