Init
This commit is contained in:
40
cleanup/action.yml
Normal file
40
cleanup/action.yml
Normal 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
85
deploy/action.yml
Normal 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
41
scripts/deploy.sh
Normal 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}"
|
||||||
38
scripts/pangolin-delete.sh
Normal file
38
scripts/pangolin-delete.sh
Normal 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"
|
||||||
89
scripts/pangolin-upsert.sh
Normal file
89
scripts/pangolin-upsert.sh
Normal 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}"
|
||||||
Reference in New Issue
Block a user