Skip to content

EBS Common - Staging/Prod Deploy #382

EBS Common - Staging/Prod Deploy

EBS Common - Staging/Prod Deploy #382

name: EBS Common - Staging/Prod Deploy
on:
workflow_dispatch:
inputs:
branch:
description: "Select a Branch to deploy"
required: true
default: "main"
environment:
description: "Select environment to deploy (staging or prod)"
required: true
default: "staging"
email:
description: "Admin email for deployment"
required: true
default: "saravana.k.r@gmail.com"
commit_id:
description: "Commit ID to deploy (leave blank for latest on branch)"
required: false
default: ""
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout selected branch
uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.branch }}
- name: Checkout specific commit if provided
if: ${{ github.event.inputs.commit_id != '' }}
run: |
git fetch --depth=1 origin ${{ github.event.inputs.commit_id }}
git checkout ${{ github.event.inputs.commit_id }}
echo "✅ Checked out commit ${{ github.event.inputs.commit_id }}"
- name: Determine Commit ID
id: commit
run: |
if [ -n "${{ github.event.inputs.commit_id }}" ]; then
echo "commit_id=${{ github.event.inputs.commit_id }}" >> $GITHUB_OUTPUT
else
if [ "${{ github.event.inputs.branch }}" = "main" ]; then
COMMIT_ID=$(git rev-parse HEAD)
else
COMMIT_ID=$(git log -1 --format="%H")
fi
echo "commit_id=$COMMIT_ID" >> $GITHUB_OUTPUT
fi
- name: Determine Branch Name
id: branch
run: |
BRANCH_NAME=$(echo "${{ github.event.inputs.branch }}" | tr '/' '-')
echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT
- name: Install dependencies (sshpass + jq)
run: sudo apt-get update && sudo apt-get install -y sshpass jq
- name: Prepare env file with website domain
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
run: |
set -euo pipefail
ENV_FILE=".env-file"
if [ ! -f "$ENV_FILE" ]; then
echo "❌ $ENV_FILE not found"
exit 1
fi
BASE_DOMAIN=$(grep '^LANGFLOW_WEBSITE_DOMAIN=' "$ENV_FILE" | tail -n1 | cut -d'=' -f2-)
if [ -z "${BASE_DOMAIN}" ]; then
echo "❌ LANGFLOW_WEBSITE_DOMAIN is not set in $ENV_FILE"
exit 1
fi
STAGING_HOST="staging.${BASE_DOMAIN}"
PROD_HOST="${BASE_DOMAIN}"
if [ "$ENVIRONMENT" = "staging" ]; then
WEBSITE_DOMAIN="$STAGING_HOST"
else
WEBSITE_DOMAIN="$PROD_HOST"
fi
echo "🔧 Injecting LANGFLOW_WEBSITE_DOMAIN=${WEBSITE_DOMAIN} into ${ENV_FILE}"
echo "" >> "$ENV_FILE"
if grep -q '^LANGFLOW_WEBSITE_DOMAIN=' "$ENV_FILE"; then
sed -i "s|^LANGFLOW_WEBSITE_DOMAIN=.*|LANGFLOW_WEBSITE_DOMAIN=${WEBSITE_DOMAIN}|" "$ENV_FILE"
else
echo "LANGFLOW_WEBSITE_DOMAIN=${WEBSITE_DOMAIN}" >> "$ENV_FILE"
fi
{
echo "LANGFLOW_WEBSITE_DOMAIN=${WEBSITE_DOMAIN}"
echo "STAGING_HOST=${STAGING_HOST}"
echo "PROD_HOST=${PROD_HOST}"
} >> "$GITHUB_ENV"
- name: Prepare Paddle secrets
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
STAGING_PADDLE_API_KEY: ${{ secrets.STAGING_PADDLE_API_KEY }}
STAGING_PADDLE_CLIENT_KEY: ${{ secrets.STAGING_PADDLE_CLIENT_KEY }}
PROD_PADDLE_API_KEY: ${{ secrets.PROD_PADDLE_API_KEY }}
PROD_PADDLE_CLIENT_KEY: ${{ secrets.PROD_PADDLE_CLIENT_KEY }}
run: |
set -euo pipefail
ENV_FILE=".env-file"
if [ "$ENVIRONMENT" = "staging" ]; then
PADDLE_API_KEY="$STAGING_PADDLE_API_KEY"
PADDLE_CLIENT_KEY="$STAGING_PADDLE_CLIENT_KEY"
PADDLE_ENVIRONMENT="staging"
else
PADDLE_API_KEY="$PROD_PADDLE_API_KEY"
PADDLE_CLIENT_KEY="$PROD_PADDLE_CLIENT_KEY"
PADDLE_ENVIRONMENT="prod"
fi
VITE_PADDLE_CLIENT_KEY="$PADDLE_CLIENT_KEY"
VITE_PADDLE_ENVIRONMENT="$PADDLE_ENVIRONMENT"
if [ -z "$PADDLE_API_KEY" ] || [ -z "$PADDLE_CLIENT_KEY" ] || [ -z "$PADDLE_ENVIRONMENT" ]; then
echo "❌ Missing Paddle secrets for ${ENVIRONMENT} environment"
exit 1
fi
echo "🔧 Injecting Paddle keys into ${ENV_FILE}"
echo "" >> "$ENV_FILE"
if grep -q '^PADDLE_API_KEY=' "$ENV_FILE"; then
sed -i "s|^PADDLE_API_KEY=.*|PADDLE_API_KEY=${PADDLE_API_KEY}|" "$ENV_FILE"
else
echo "PADDLE_API_KEY=${PADDLE_API_KEY}" >> "$ENV_FILE"
fi
if grep -q '^PADDLE_CLIENT_KEY=' "$ENV_FILE"; then
sed -i "s|^PADDLE_CLIENT_KEY=.*|PADDLE_CLIENT_KEY=${PADDLE_CLIENT_KEY}|" "$ENV_FILE"
else
echo "PADDLE_CLIENT_KEY=${PADDLE_CLIENT_KEY}" >> "$ENV_FILE"
fi
if grep -q '^PADDLE_ENVIRONMENT=' "$ENV_FILE"; then
sed -i "s|^PADDLE_ENVIRONMENT=.*|PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}|" "$ENV_FILE"
else
echo "PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}" >> "$ENV_FILE"
fi
if grep -q '^VITE_PADDLE_CLIENT_KEY=' "$ENV_FILE"; then
sed -i "s|^VITE_PADDLE_CLIENT_KEY=.*|VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY}|" "$ENV_FILE"
else
echo "VITE_PADDLE_CLIENT_KEY=${VITE_PADDLE_CLIENT_KEY}" >> "$ENV_FILE"
fi
if grep -q '^VITE_PADDLE_ENVIRONMENT=' "$ENV_FILE"; then
sed -i "s|^VITE_PADDLE_ENVIRONMENT=.*|VITE_PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}|" "$ENV_FILE"
else
echo "VITE_PADDLE_ENVIRONMENT=${PADDLE_ENVIRONMENT}" >> "$ENV_FILE"
fi
- name: Prepare Clerk API key
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
STAGING_CLERK_API_KEY: ${{ secrets.STAGING_CLERK_SECRET_KEY }}
PROD_CLERK_API_KEY: ${{ secrets.PROD_CLERK_SECRET_KEY }}
run: |
set -euo pipefail
ENV_FILE=".env-file"
if [ "$ENVIRONMENT" = "staging" ]; then
CLERK_API_KEY="$STAGING_CLERK_API_KEY"
else
CLERK_API_KEY="$PROD_CLERK_API_KEY"
fi
if [ -z "$CLERK_API_KEY" ]; then
echo "❌ Missing Clerk API key for ${ENVIRONMENT} environment"
exit 1
fi
echo "🔧 Injecting Clerk API key into ${ENV_FILE}"
echo "" >> "$ENV_FILE"
if grep -q '^CLERK_API_KEY=' "$ENV_FILE"; then
sed -i "s|^CLERK_API_KEY=.*|CLERK_API_KEY=${CLERK_API_KEY}|" "$ENV_FILE"
else
echo "CLERK_API_KEY=${CLERK_API_KEY}" >> "$ENV_FILE"
fi
- name: Ensure admin user exists in Clerk
env:
ENVIRONMENT: ${{ github.event.inputs.environment }}
ADMIN_PASSWORD: ${{ secrets.ADMIN_PASSWORD }}
STAGING_CLERK_SECRET_KEY: ${{ secrets.STAGING_CLERK_SECRET_KEY }}
PROD_CLERK_SECRET_KEY: ${{ secrets.PROD_CLERK_SECRET_KEY }}
STAGING_HOST: ${{ env.STAGING_HOST }}
PROD_HOST: ${{ env.PROD_HOST }}
run: |
set -euo pipefail
if [ "$ENVIRONMENT" = "staging" ]; then
CLERK_SECRET_KEY="$STAGING_CLERK_SECRET_KEY"
WEBSITE_DOMAIN="$STAGING_HOST"
else
CLERK_SECRET_KEY="$PROD_CLERK_SECRET_KEY"
WEBSITE_DOMAIN="$PROD_HOST"
fi
ADMIN_EMAIL="admin@${WEBSITE_DOMAIN}"
AUTH_HEADER="Authorization: Bearer ${CLERK_SECRET_KEY}"
echo "🔐 Ensuring Clerk admin user ${ADMIN_EMAIL} exists"
# === LOOKUP USER ===
LOOKUP=$(curl -s -H "$AUTH_HEADER" \
"https://api.clerk.com/v1/users?email_address=${ADMIN_EMAIL}")
USER_ID=$(echo "$LOOKUP" | jq -r '.[0]?.id // empty')
if [ -z "$USER_ID" ] || [ "$USER_ID" = "null" ]; then
echo "➕ Admin not found. Creating user..."
CREATE=$(curl -s -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-X POST "https://api.clerk.com/v1/users" \
-d "{
\"email_address\": [\"${ADMIN_EMAIL}\"],
\"username\": \"admin\",
\"password\": \"${ADMIN_PASSWORD}\"
}")
USER_ID=$(echo "$CREATE" | jq -r '.id // empty')
EMAIL_ID=$(echo "$CREATE" | jq -r '.email_addresses[0]?.id // empty')
if [ -z "$USER_ID" ] || [ "$USER_ID" = "null" ]; then
echo "❌ User creation failed"
echo "$CREATE"
exit 1
fi
echo "✅ Created admin user with id ${USER_ID}"
else
echo "✅ Admin exists with id ${USER_ID} — refreshing password"
UPDATE=$(curl -s -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-X PATCH "https://api.clerk.com/v1/users/${USER_ID}" \
-d "{\"password\": \"${ADMIN_PASSWORD}\"}")
UPDATED_ID=$(echo "$UPDATE" | jq -r '.id // empty')
if [ -z "$UPDATED_ID" ] || [ "$UPDATED_ID" = "null" ]; then
echo "❌ Failed to update password"
echo "$UPDATE"
exit 1
fi
echo "🔄 Password updated"
# Only fetch email ID once here
EMAIL_ID=$(echo "$UPDATE" | jq -r '.email_addresses[0]?.id // empty')
# If missing email array (rare), re-fetch
if [ -z "$EMAIL_ID" ] || [ "$EMAIL_ID" = "null" ]; then
USER=$(curl -s -H "$AUTH_HEADER" "https://api.clerk.com/v1/users/${USER_ID}")
EMAIL_ID=$(echo "$USER" | jq -r '.email_addresses[0]?.id // empty')
fi
fi
if [ -z "$EMAIL_ID" ] || [ "$EMAIL_ID" = "null" ]; then
echo "❌ Could not determine email ID for ${ADMIN_EMAIL}"
exit 1
fi
# === VERIFY EMAIL ===
echo "✉️ Marking email ${ADMIN_EMAIL} as verified"
VERIFY=$(curl -s -H "$AUTH_HEADER" -H "Content-Type: application/json" \
-X PATCH "https://api.clerk.com/v1/email_addresses/${EMAIL_ID}" \
-d '{"verified": true}')
# Updated jq (recommended)
STATUS=$(echo "$VERIFY" | jq -r '.verification?.status // empty')
if [ "$STATUS" = "verified" ]; then
echo "✅ Email marked as verified"
else
echo "❌ Email verification failed"
echo "$VERIFY"
exit 1
fi
- name: Create GitHub Deployment
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
COMMIT_ID=${{ steps.commit.outputs.commit_id }}
ENVIRONMENT=${{ github.event.inputs.environment }}
BRANCH=${{ github.event.inputs.branch }}
ACTOR=${{ github.actor }}
echo "📦 Creating deployment for $ENVIRONMENT commit=$COMMIT_ID branch=$BRANCH"
DEPLOYMENT_ID=$(gh api repos/${{ github.repository }}/deployments \
-F ref=${{ github.event.inputs.branch }} \
-F environment=$ENVIRONMENT \
-F description="Deployment by $ACTOR from branch $BRANCH with commit $COMMIT_ID" \
-F auto_merge=false \
-F required_contexts[]=null --jq '.id')
echo "DEPLOYMENT_ID=$DEPLOYMENT_ID" >> $GITHUB_ENV
- name: Mark Deployment In Progress
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh api repos/${{ github.repository }}/deployments/${{ env.DEPLOYMENT_ID }}/statuses \
-F state=in_progress \
-F description="Deployment started and is in progress"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: forwardemailforaifirstdesk
password: ${{ secrets.DOCKERHUB_TOKEN }}
- name: Free up disk space
run: |
echo "=== Cleaning up space for Docker build ==="
sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc
docker builder prune -f
df -h
- name: Build Docker Image (staging)
if: ${{ github.event.inputs.environment == 'staging' }}
run: |
COMMIT_ID=${{ steps.commit.outputs.commit_id }}
BRANCH_NAME=${{ steps.branch.outputs.branch_name }}
echo "Using branch=$BRANCH_NAME commit=$COMMIT_ID"
echo "=== Building Staging Image ==="
DOCKER_BUILDKIT=1 \
VITE_AUTO_LOGIN=false \
VITE_CLERK_AUTH_ENABLED=true \
VITE_CLERK_PUBLISHABLE_KEY=${{ secrets.CLERK_PUBLISHABLE_KEY }} \
VITE_CLERK_FRONTEND_API=${{ vars.STAGING_CLERK_FRONTEND_API }} \
VITE_PADDLE_CLIENT_KEY=${{ secrets.STAGING_PADDLE_CLIENT_KEY }} \
VITE_PADDLE_ENVIRONMENT=${{ github.event.inputs.environment }} \
make docker_build TAG=langflow:staging-$BRANCH_NAME-$COMMIT_ID DOCKER=docker
docker tag langflow:staging-$BRANCH_NAME-$COMMIT_ID forwardemailforaifirstdesk/langflow:staging-$BRANCH_NAME-$COMMIT_ID
docker push forwardemailforaifirstdesk/langflow:staging-$BRANCH_NAME-$COMMIT_ID
- name: Build Docker Image (production)
if: ${{ github.event.inputs.environment == 'prod' }}
run: |
COMMIT_ID=${{ steps.commit.outputs.commit_id }}
BRANCH_NAME=${{ steps.branch.outputs.branch_name }}
echo "Using branch=$BRANCH_NAME commit=$COMMIT_ID"
echo "=== Building Production Image ==="
DOCKER_BUILDKIT=1 \
VITE_AUTO_LOGIN=false \
VITE_CLERK_AUTH_ENABLED=true \
VITE_CLERK_PUBLISHABLE_KEY=${{ secrets.PROD_CLERK_PUBLISHABLE_KEY }} \
VITE_CLERK_FRONTEND_API=${{ vars.PROD_CLERK_FRONTEND_API }} \
VITE_PADDLE_CLIENT_KEY=${{ secrets.PROD_PADDLE_CLIENT_KEY }} \
VITE_PADDLE_ENVIRONMENT=${{ github.event.inputs.environment }} \
make docker_build TAG=langflow:prod-$BRANCH_NAME-$COMMIT_ID DOCKER=docker
docker tag langflow:prod-$BRANCH_NAME-$COMMIT_ID forwardemailforaifirstdesk/langflow:prod-$BRANCH_NAME-$COMMIT_ID
docker push forwardemailforaifirstdesk/langflow:prod-$BRANCH_NAME-$COMMIT_ID
- name: Cleanup old Docker Hub tags (skip for prod)
if: ${{ github.event.inputs.environment == 'staging' }}
run: |
#!/usr/bin/env bash
set -euo pipefail
DOCKERHUB_USERNAME="forwardemailforaifirstdesk"
DOCKERHUB_PASSWORD="${{ secrets.DOCKERHUB_TOKEN }}"
REPOSITORY="forwardemailforaifirstdesk/langflow"
echo "=== Getting Docker Hub token ==="
TOKEN=$(curl -s -H "Content-Type: application/json" -X POST \
-d '{"username": "'$DOCKERHUB_USERNAME'", "password": "'$DOCKERHUB_PASSWORD'"}' \
https://hub.docker.com/v2/users/login/ | jq -r .token)
if [ -z "$TOKEN" ] || [ "$TOKEN" == "null" ]; then
echo "❌ Failed to authenticate"
exit 1
fi
echo "✅ Authentication successful"
echo "=== Fetching tags ==="
RESPONSE=$(curl -s -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/$REPOSITORY/tags?page_size=100")
TAGS=$(echo "$RESPONSE" | jq -r '.results[] | .name')
if [ -z "$TAGS" ]; then
echo "ℹ️ No tags found in repository $REPOSITORY. Nothing to delete."
exit 0
fi
echo "Found tags: $TAGS"
for TAG in $TAGS; do
COMMIT_ID=${{ steps.commit.outputs.commit_id }}
BRANCH_NAME=${{ steps.branch.outputs.branch_name }}
if [[ "$TAG" == "staging-$BRANCH_NAME-$COMMIT_ID" || "$TAG" == "prod-$BRANCH_NAME-$COMMIT_ID" ]]; then
echo "⏭️ Skipping current branch+commit tag: $TAG"
continue
fi
echo "Deleting old tag: $TAG"
DELETE_RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" \
-X DELETE -H "Authorization: JWT $TOKEN" \
"https://hub.docker.com/v2/repositories/$REPOSITORY/tags/$TAG/")
HTTP_CODE=$(echo $DELETE_RESPONSE | tr -d '\n' | sed -e 's/.*HTTPSTATUS://')
if [ "$HTTP_CODE" = "204" ]; then
echo " ✅ Successfully deleted tag $TAG"
else
echo " ❌ Failed to delete tag $TAG (HTTP: $HTTP_CODE)"
fi
sleep 1
done
echo "Note: Docker Hub will automatically clean up unreferenced images within 24-48 hours"
- name: Copy deployment files to VM
uses: appleboy/scp-action@v0.1.7
with:
host: ${{ github.event.inputs.environment == 'staging' && env.STAGING_HOST || env.PROD_HOST }}
username: root
password: ${{ github.event.inputs.environment == 'staging' && secrets.VM_PASSWORD || secrets.PROD_VM_PASSWORD }}
source: "./deploy/ebs-staging-prod.sh,./.env-file"
target: /root/
- name: Run deployment script on VM
uses: appleboy/ssh-action@v1.1.0
with:
host: ${{ github.event.inputs.environment == 'staging' && env.STAGING_HOST || env.PROD_HOST }}
username: root
password: ${{ github.event.inputs.environment == 'staging' && secrets.VM_PASSWORD || secrets.PROD_VM_PASSWORD }}
script: |
chmod +x /root/deploy/ebs-staging-prod.sh
if [ "${{ github.event.inputs.environment }}" = "staging" ]; then
IMAGE="forwardemailforaifirstdesk/langflow:staging-${{ steps.branch.outputs.branch_name }}-${{ steps.commit.outputs.commit_id }}"
DOMAIN="${{env.STAGING_HOST}}"
EMAIL="${{ github.event.inputs.email }}"
ENV_FILE="/root/.env-file"
DB_USER="${{ secrets.POSTGRES_USER }}"
DB_PASSWORD="${{ secrets.POSTGRES_PASSWD }}"
DB_NAME="${{ secrets.POSTGRES_DB }}"
VOLUME_NAME="${{ vars.STAGING_VOLUME_NAME }}"
else
IMAGE="forwardemailforaifirstdesk/langflow:prod-${{ steps.branch.outputs.branch_name }}-${{ steps.commit.outputs.commit_id }}"
DOMAIN="${{env.PROD_HOST}}"
EMAIL="${{ github.event.inputs.email }}"
ENV_FILE="/root/.env-file"
DB_USER="${{ secrets.PROD_POSTGRES_USER }}"
DB_PASSWORD="${{ secrets.PROD_POSTGRES_PASSWD }}"
DB_NAME="${{ secrets.PROD_POSTGRES_DB }}"
VOLUME_NAME="${{ vars.PROD_VOLUME_NAME }}"
fi
/root/deploy/ebs-staging-prod.sh \
--image "$IMAGE" \
--domain "$DOMAIN" \
--email "$EMAIL" \
--env-file "$ENV_FILE" \
--db-user "$DB_USER" \
--db-password "$DB_PASSWORD" \
--db-name "$DB_NAME" \
--volume-name "$VOLUME_NAME"
command_timeout: 20m
- name: Copy deploy_status.env from VM
run: |
sshpass -p "${{ secrets.VM_PASSWORD }}" scp -o StrictHostKeyChecking=no \
root@staging.visualaiagentsbuilder.com:/root/deploy_status.env \
$GITHUB_WORKSPACE/deploy_status.env
- name: Update GitHub Deployment Status with Docker info
if: always()
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
source "${GITHUB_WORKSPACE}/deploy_status.env"
# Encode container info for safe URL usage
CONTAINER_ENCODED=$(echo "$FINAL_CONTAINER" | jq -s -R -r @uri)
gh api repos/${{ github.repository }}/deployments/${{ env.DEPLOYMENT_ID }}/statuses \
-F state=$FINAL_STATUS \
-F description="$FINAL_DESCRIPTION" \
-F log_url="https://logs.${{ github.repository_owner }}.com/deploy/${{ env.DEPLOYMENT_ID }}?container=${CONTAINER_ENCODED}"