Skip to content

VNC Data Static Timeout #170

VNC Data Static Timeout

VNC Data Static Timeout #170

name: VNC Data Static Timeout
on:
workflow_dispatch:
inputs:
base64_config:
description: 'Base64 encoded JSON configuration'
required: true
type: string
env:
DISPLAY: :1
VNC_PORT: 5901
NOVNC_PORT: 6080
jobs:
process:
runs-on: ubuntu-latest
timeout-minutes: 380
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.12'
- name: Fetch and Parse Config
id: config
run: |
# Decode base64 config (without echoing it)
CONFIG=$(echo "${{ inputs.base64_config }}" | base64 -d)
# Parse VNC, Cloudflare, and Webshare values (lowercase keys)
VNC_ENABLED=$(echo "$CONFIG" | jq -r '.vnc_enabled // "false"')
CLOUDFLARE_ENABLED=$(echo "$CONFIG" | jq -r '.cloudflare_enabled // "false"')
WEBSHARE_ENABLED=$(echo "$CONFIG" | jq -r '.webshare_enabled // "false"')
echo "vnc_enabled=$VNC_ENABLED" >> $GITHUB_OUTPUT
echo "cloudflare_enabled=$CLOUDFLARE_ENABLED" >> $GITHUB_OUTPUT
echo "webshare_enabled=$WEBSHARE_ENABLED" >> $GITHUB_OUTPUT
# Parse user_id and workspace_id with defaults
USER_ID=$(echo "$CONFIG" | jq -r '.user_id // "69c3090b-464d-4de3-9358-ceca1c9e2aa8"')
WORKSPACE_ID=$(echo "$CONFIG" | jq -r '.workspace_id // "ceae1caa-1538-4859-8e1a-24a8b4904d5f"')
echo "user_id=$USER_ID" >> $GITHUB_OUTPUT
echo "workspace_id=$WORKSPACE_ID" >> $GITHUB_OUTPUT
# Parse VNC password if VNC is enabled
if [ "$VNC_ENABLED" = "true" ]; then
VNC_PASSWORD=$(echo "$CONFIG" | jq -r '.vnc_password // ""')
echo "vnc_password=$VNC_PASSWORD" >> $GITHUB_OUTPUT
fi
# Parse Cloudflare-related values if Cloudflare is enabled
if [ "$CLOUDFLARE_ENABLED" = "true" ]; then
CLOUDFLARE_API_TOKEN=$(echo "$CONFIG" | jq -r '.cloudflare_api_token // ""')
CLOUDFLARE_ACCOUNT_ID=$(echo "$CONFIG" | jq -r '.cloudflare_account_id // ""')
CLOUDFLARE_ZONE_ID=$(echo "$CONFIG" | jq -r '.cloudflare_zone_id // ""')
HOSTNAME=$(echo "$CONFIG" | jq -r '.hostname // ""')
# Extract browser_profile_id from hostname (UUID prefix before first dot)
# e.g. "a6b83dc3-1881-4285-957f-3c49c7b162ad.articleinnovator.com" -> "a6b83dc3-1881-4285-957f-3c49c7b162ad"
BROWSER_PROFILE_ID=$(echo "$HOSTNAME" | cut -d'.' -f1)
CLOUDFLARE_TUNNEL_CREDENTIAL_ID=$(echo "$CONFIG" | jq -r '.cloudflare_tunnel_credential_id // ""')
echo "cloudflare_api_token=$CLOUDFLARE_API_TOKEN" >> $GITHUB_OUTPUT
echo "cloudflare_account_id=$CLOUDFLARE_ACCOUNT_ID" >> $GITHUB_OUTPUT
echo "cloudflare_zone_id=$CLOUDFLARE_ZONE_ID" >> $GITHUB_OUTPUT
echo "hostname=$HOSTNAME" >> $GITHUB_OUTPUT
echo "browser_profile_id=$BROWSER_PROFILE_ID" >> $GITHUB_OUTPUT
echo "cloudflare_tunnel_credential_id=$CLOUDFLARE_TUNNEL_CREDENTIAL_ID" >> $GITHUB_OUTPUT
echo "✅ Cloudflare Config Loaded"
echo " Hostname: $HOSTNAME"
echo " Browser Profile ID: $BROWSER_PROFILE_ID"
fi
# Parse GitHub repo info for profile storage (GitHub Releases)
GITHUB_REPO_NAME=$(echo "$CONFIG" | jq -r '.github_repo_name // ""')
GITHUB_OWNER=$(echo "$CONFIG" | jq -r '.github_owner // ""')
PROFILE_GITHUB_TOKEN=$(echo "$CONFIG" | jq -r '.github_token // ""')
echo "github_repo_name=$GITHUB_REPO_NAME" >> $GITHUB_OUTPUT
echo "github_owner=$GITHUB_OWNER" >> $GITHUB_OUTPUT
echo "profile_github_token=$PROFILE_GITHUB_TOKEN" >> $GITHUB_OUTPUT
if [ -n "$GITHUB_REPO_NAME" ] && [ "$GITHUB_REPO_NAME" != "" ]; then
echo "✅ GitHub Repo Config Loaded (${GITHUB_OWNER}/${GITHUB_REPO_NAME})"
fi
# Parse proxy configuration from browser profile
PROXY_HOST=$(echo "$CONFIG" | jq -r '.proxy.host // ""')
PROXY_PORT=$(echo "$CONFIG" | jq -r '.proxy.port // ""')
PROXY_TYPE=$(echo "$CONFIG" | jq -r '.proxy.type // "http"')
echo "proxy_host=$PROXY_HOST" >> $GITHUB_OUTPUT
echo "proxy_port=$PROXY_PORT" >> $GITHUB_OUTPUT
echo "proxy_type=$PROXY_TYPE" >> $GITHUB_OUTPUT
if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "" ]; then
echo "✅ Proxy Config Loaded (${PROXY_TYPE}://${PROXY_HOST}:${PROXY_PORT})"
fi
# Parse Webshare-related values if Webshare is enabled
if [ "$WEBSHARE_ENABLED" = "true" ]; then
WEBSHARE_API_TOKEN=$(echo "$CONFIG" | jq -r '.webshare_api_token // ""')
echo "webshare_api_token=$WEBSHARE_API_TOKEN" >> $GITHUB_OUTPUT
echo "✅ Webshare Config Loaded"
fi
# Parse optional workflow_ids for auto-task dispatch (JSON array)
WORKFLOW_IDS=$(echo "$CONFIG" | jq -c '.workflow_ids // []')
echo "workflow_ids=$WORKFLOW_IDS" >> $GITHUB_OUTPUT
# Parse profile timeout (minutes), default 45 min, max 360 min (6 hours)
PROFILE_TIMEOUT_MINUTES=$(echo "$CONFIG" | jq -r '.profile_timeout_minutes // "45"')
# Clamp to valid range (5-360 minutes)
if [ "$PROFILE_TIMEOUT_MINUTES" -lt 5 ] 2>/dev/null; then PROFILE_TIMEOUT_MINUTES=5; fi
if [ "$PROFILE_TIMEOUT_MINUTES" -gt 360 ] 2>/dev/null; then PROFILE_TIMEOUT_MINUTES=360; fi
PROFILE_TIMEOUT_SECONDS=$((PROFILE_TIMEOUT_MINUTES * 60))
echo "profile_timeout_minutes=$PROFILE_TIMEOUT_MINUTES" >> $GITHUB_OUTPUT
echo "profile_timeout_seconds=$PROFILE_TIMEOUT_SECONDS" >> $GITHUB_OUTPUT
echo "✅ Config Loaded:"
echo " VNC: $VNC_ENABLED"
echo " Cloudflare: $CLOUDFLARE_ENABLED"
echo " Webshare: $WEBSHARE_ENABLED"
echo " User ID: $USER_ID"
echo " Workspace ID: $WORKSPACE_ID"
echo " Profile Timeout: ${PROFILE_TIMEOUT_MINUTES} min"
- name: Optimize APT Mirror and Network
if: steps.config.outputs.vnc_enabled == 'true'
timeout-minutes: 3
run: |
echo "Optimizing APT mirror and network settings..."
# Release possible dpkg lock holders on fresh runners
sudo systemctl stop unattended-upgrades 2>/dev/null || true
sudo killall -9 unattended-upgrades 2>/dev/null || true
# Add resilient APT networking defaults for CI
cat << 'EOF' | sudo tee /etc/apt/apt.conf.d/99ci-network >/dev/null
Acquire::Retries "6";
APT::Acquire::Retries "6";
Acquire::http::Timeout "15";
Acquire::https::Timeout "15";
Acquire::http::Pipeline-Depth "0";
Acquire::ForceIPv4 "true";
Acquire::PDiffs "false";
EOF
# Ubuntu 24.04 runners usually use ubuntu.sources; switch away from Azure mirror.
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list.d/ubuntu.sources
sudo sed -i 's|https://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list.d/ubuntu.sources
fi
if [ -f /etc/apt/sources.list ]; then
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list
sudo sed -i 's|https://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list
fi
sudo apt-get -o Acquire::Retries=6 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update -q
echo "✅ APT optimization complete"
- name: Restore Cached Dependencies
if: steps.config.outputs.vnc_enabled == 'true'
uses: actions/cache@v4
id: deps-cache
with:
path: |
~/cached-debs
~/noVNC
~/.cache/pip
key: vnc-deps-v5-${{ runner.os }}
restore-keys: |
vnc-deps-v5-
- name: Register IP with Webshare
if: steps.config.outputs.webshare_enabled == 'true'
id: webshare
timeout-minutes: 2
run: |
echo "Registering IP with Webshare..."
# Get current public IP (with timeout and retry)
PUBLIC_IP=""
for attempt in 1 2 3; do
PUBLIC_IP=$(curl -s --connect-timeout 10 --max-time 15 'https://api.ipify.org?format=json' | jq -r '.ip // empty')
if [ -n "$PUBLIC_IP" ]; then
break
fi
echo "Retrying IP fetch... ($attempt/3)"
sleep 2
done
if [ -z "$PUBLIC_IP" ]; then
echo "⚠️ Failed to get public IP, skipping Webshare registration"
exit 0
fi
echo "Public IP: $PUBLIC_IP"
# Register IP with Webshare
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
"https://proxy.webshare.io/api/v2/proxy/ipauthorization/?plan_id=12984667" \
-X POST \
-d "{\"ip_address\": \"$PUBLIC_IP\"}" \
-H "Content-Type: application/json" \
-H "Authorization: Token ${{ steps.config.outputs.webshare_api_token }}")
# Extract authorization ID
AUTH_ID=$(echo "$RESPONSE" | jq -r '.id // empty')
if [ -n "$AUTH_ID" ]; then
echo "webshare_auth_id=$AUTH_ID" >> $GITHUB_OUTPUT
echo "✅ IP registered with Webshare (Auth ID: $AUTH_ID)"
else
echo "⚠️ Failed to register IP with Webshare"
echo "$RESPONSE" | jq '.'
fi
- name: Install Desktop Environment
if: steps.config.outputs.vnc_enabled == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
timeout-minutes: 18
run: |
echo "Installing XFCE desktop environment..."
install_desktop() {
# Kill unattended-upgrades to prevent dpkg lock contention
sudo systemctl stop unattended-upgrades 2>/dev/null || true
sudo killall -9 unattended-upgrades 2>/dev/null || true
sudo killall -9 dpkg 2>/dev/null || true
# Clean up any broken dpkg state
sudo rm -f /var/lib/dpkg/lock-frontend /var/lib/dpkg/lock /var/cache/apt/archives/lock 2>/dev/null || true
sudo dpkg --configure -a 2>/dev/null || true
# Wait for any existing dpkg lock to release (max 60s)
for i in $(seq 1 12); do
if ! sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; then
break
fi
echo "Waiting for dpkg lock to release... ($i/12)"
sleep 5
done
# Enable parallel downloads
echo 'Acquire::Queue-Mode "access";' | sudo tee /etc/apt/apt.conf.d/99parallel
echo 'APT::Acquire::Retries "3";' | sudo tee -a /etc/apt/apt.conf.d/99parallel
# Ubuntu 24.04 runners may use ubuntu.sources instead of sources.list.
if [ -f /etc/apt/sources.list.d/ubuntu.sources ]; then
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list.d/ubuntu.sources
sudo sed -i 's|https://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list.d/ubuntu.sources
fi
# Use standard Ubuntu archive instead of Azure mirror
if [ -f /etc/apt/sources.list ]; then
sudo sed -i 's|http://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list
sudo sed -i 's|https://azure.archive.ubuntu.com/ubuntu|mirror://mirrors.ubuntu.com/mirrors.txt|g' /etc/apt/sources.list
fi
sudo apt-get -o Acquire::Retries=6 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update -q
# Create cached-debs directory for parallel downloads
mkdir -p ~/cached-debs
# Start parallel downloads in background
echo "📥 Starting parallel downloads..."
(wget -q -O ~/cached-debs/google-chrome-stable_current_amd64.deb \
https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb && echo "✅ Chrome downloaded") &
PID_CHROME=$!
(wget -q -O ~/cached-debs/turbovnc_2.2.5_amd64.deb \
https://phoenixnap.dl.sourceforge.net/project/turbovnc/2.2.5/turbovnc_2.2.5_amd64.deb && echo "✅ TurboVNC downloaded") &
PID_VNC=$!
(cd ~ && git clone --depth 1 https://github.com/novnc/noVNC.git && \
cd noVNC && git clone --depth 1 https://github.com/novnc/websockify.git && echo "✅ noVNC cloned") &
PID_NOVNC=$!
# Install XFCE while downloads happen in parallel
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
-o Dpkg::Options::="--force-confdef" \
-o Dpkg::Options::="--force-confold" \
xfce4 \
xfce4-terminal \
dbus-x11 \
x11-utils \
x11-xserver-utils \
xfonts-base \
ffmpeg \
wmctrl \
python3 \
python3-venv
echo "✅ XFCE installed"
# Wait for downloads and install
wait $PID_CHROME && sudo dpkg -i ~/cached-debs/google-chrome-stable_current_amd64.deb || sudo apt-get install -f -y
echo "✅ Chrome installed"
wait $PID_VNC && sudo dpkg -i ~/cached-debs/turbovnc_2.2.5_amd64.deb
echo "✅ TurboVNC installed"
wait $PID_NOVNC
echo "✅ noVNC ready"
}
MAX_RETRIES=3
RETRY_COUNT=0
INSTALL_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "📦 Installation attempt $RETRY_COUNT/$MAX_RETRIES..."
# Run installation with 5 minute timeout per attempt
if timeout 300 bash -c "$(declare -f install_desktop); install_desktop"; then
INSTALL_SUCCESS=true
break
else
echo "⚠️ Installation attempt $RETRY_COUNT failed"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
echo "🔄 Retrying in 30 seconds..."
sleep 30
fi
fi
done
if [ "$INSTALL_SUCCESS" = "true" ]; then
echo "✅ All dependencies installed"
else
echo "❌ Failed to install dependencies after $MAX_RETRIES attempts"
exit 1
fi
- name: Install from Cache (Fast Path)
if: steps.config.outputs.vnc_enabled == 'true' && steps.deps-cache.outputs.cache-hit == 'true'
run: |
echo "📦 Installing from cache (fast path)..."
# Kill unattended-upgrades
sudo systemctl stop unattended-upgrades 2>/dev/null || true
sudo killall -9 unattended-upgrades 2>/dev/null || true
# Wait for dpkg lock
for i in $(seq 1 6); do
if ! sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; then
break
fi
echo "Waiting for dpkg lock... ($i/6)"
sleep 5
done
# Install XFCE if not already installed
if ! command -v startxfce4 &> /dev/null; then
sudo apt-get -o Acquire::Retries=6 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update -q
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
xfce4 xfce4-terminal dbus-x11 x11-utils x11-xserver-utils xfonts-base ffmpeg wmctrl python3 python3-venv
fi
# Install cached debs
if [ -f ~/cached-debs/google-chrome-stable_current_amd64.deb ] && ! command -v google-chrome &> /dev/null; then
sudo dpkg -i ~/cached-debs/google-chrome-stable_current_amd64.deb || sudo apt-get install -f -y
fi
if [ -f ~/cached-debs/turbovnc_2.2.5_amd64.deb ] && [ ! -x /opt/TurboVNC/bin/vncserver ]; then
sudo dpkg -i ~/cached-debs/turbovnc_2.2.5_amd64.deb
fi
echo "✅ Cache-based installation complete"
- name: Install and Start Docker
if: steps.config.outputs.vnc_enabled == 'true'
run: |
echo "Setting up Docker..."
# Docker is pre-installed on ubuntu-latest, just ensure the service is running
if command -v docker &> /dev/null; then
echo "Docker already installed: $(docker --version)"
else
echo "Installing Docker..."
curl -fsSL https://get.docker.com | sudo sh
fi
# Ensure Docker service is running
sudo systemctl start docker 2>/dev/null || sudo dockerd &
sleep 3
# Add current user to docker group so commands work without sudo
sudo usermod -aG docker $USER
# Install docker compose plugin if not present
if ! docker compose version &> /dev/null; then
echo "Installing Docker Compose plugin..."
sudo apt-get -o Acquire::Retries=6 -o Acquire::http::Timeout=15 -o Acquire::https::Timeout=15 update -q
sudo apt-get install -y docker-compose-plugin
fi
# Verify
docker --version
docker compose version
echo "✅ Docker is ready"
- name: Configure VNC Server
if: steps.config.outputs.vnc_enabled == 'true'
run: |
export PATH=$PATH:/opt/TurboVNC/bin
mkdir -p ~/.vnc
# Create password file
echo "${{ steps.config.outputs.vnc_password }}" | vncpasswd -f > ~/.vnc/passwd
chmod 600 ~/.vnc/passwd
# Create xstartup script
cat > ~/.vnc/xstartup.turbovnc << 'EOF'
#!/bin/bash
# Suppress accessibility warnings
export NO_AT_BRIDGE=1
export SESSION_MANAGER=""
# Start XFCE desktop
dbus-launch /usr/bin/startxfce4 &
EOF
chmod +x ~/.vnc/xstartup.turbovnc
- name: Start VNC Server
if: steps.config.outputs.vnc_enabled == 'true'
run: |
export PATH=$PATH:/opt/TurboVNC/bin
echo "Starting TurboVNC server on display :1..."
vncserver :1 -geometry 1920x1080 -depth 24 -rfbport $VNC_PORT 2>&1 | tee /tmp/vnc-start.log
# Health check - wait up to 15s, exit early when ready
for i in $(seq 1 15); do
if pgrep -f "Xvnc.*:1" > /dev/null; then
echo "✅ TurboVNC Server started in ${i}s on port $VNC_PORT"
ps aux | grep Xvnc | grep -v grep
exit 0
fi
sleep 1
done
echo "❌ Failed to start VNC server"
cat /tmp/vnc-start.log
exit 1
- name: Start noVNC (Web VNC)
if: steps.config.outputs.vnc_enabled == 'true'
run: |
echo "Starting noVNC web server..."
cd ~/noVNC
./utils/novnc_proxy --vnc localhost:$VNC_PORT --listen $NOVNC_PORT &
# Health check - wait up to 10s
for i in $(seq 1 10); do
if netstat -tuln | grep :$NOVNC_PORT > /dev/null; then
echo "✅ noVNC started in ${i}s on port $NOVNC_PORT"
exit 0
fi
sleep 1
done
echo "❌ noVNC failed to start"
exit 1
- name: Download and Setup BotXByte Extension
if: steps.config.outputs.vnc_enabled == 'true'
run: |
echo "Setting up BotXByte extension..."
DOWNLOADS_DIR="$HOME/Downloads"
EXTENSION_DIR="$DOWNLOADS_DIR/real-botxbyte-extension"
mkdir -p "$DOWNLOADS_DIR"
# Always download fresh extension (remove any cached version)
rm -rf "$EXTENSION_DIR" 2>/dev/null || true
echo "Downloading BotXByte extension..."
# Download the extension zip
wget -O "$DOWNLOADS_DIR/real-botxbyte-extension.zip" \
"https://raw.githubusercontent.com/sanket-sakariya/test-abc/main/real-botxbyte-extension.zip"
# Extract to a temp location to handle nested directories
TEMP_EXTRACT="/tmp/extension-extract"
rm -rf "$TEMP_EXTRACT"
mkdir -p "$TEMP_EXTRACT"
unzip -o "$DOWNLOADS_DIR/real-botxbyte-extension.zip" -d "$TEMP_EXTRACT"
rm "$DOWNLOADS_DIR/real-botxbyte-extension.zip"
# Find where manifest.json actually is
MANIFEST_PATH=$(find "$TEMP_EXTRACT" -name "manifest.json" -type f | head -1)
if [ -z "$MANIFEST_PATH" ]; then
echo "❌ manifest.json not found in extracted zip!"
find "$TEMP_EXTRACT" -type f
exit 1
fi
ACTUAL_EXT_DIR=$(dirname "$MANIFEST_PATH")
echo "Found manifest.json at: $MANIFEST_PATH"
# Move to the expected location
rm -rf "$EXTENSION_DIR"
mv "$ACTUAL_EXT_DIR" "$EXTENSION_DIR"
rm -rf "$TEMP_EXTRACT"
# Clean up stale/non-extension files
rm -rf "$EXTENSION_DIR/_metadata" 2>/dev/null || true
rm -rf "$EXTENSION_DIR/.git" 2>/dev/null || true
rm -f "$EXTENSION_DIR/.gitignore" "$EXTENSION_DIR/README.md" "$EXTENSION_DIR/llm.txt"
echo "Extension contents:"
ls -la "$EXTENSION_DIR/"
echo "manifest.json:"
cat "$EXTENSION_DIR/manifest.json"
# Set up Python virtual environment and install requirements
cd "$EXTENSION_DIR"
python3 -m venv venv
source venv/bin/activate
if [ -f requirements.txt ]; then
pip install -r requirements.txt
echo "✅ Requirements installed in venv"
else
echo "⚠️ No requirements.txt found"
fi
echo "✅ BotXByte Extension setup complete"
echo " Extension path: $EXTENSION_DIR"
- name: Start BotXByte Server
if: steps.config.outputs.vnc_enabled == 'true'
id: botxbyte_server
run: |
echo "Starting BotXByte server (server.py)..."
EXTENSION_DIR="$HOME/Downloads/real-botxbyte-extension"
cd "$EXTENSION_DIR"
source venv/bin/activate
export WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
python3 server.py &
SERVER_PID=$!
echo "server_pid=$SERVER_PID" >> $GITHUB_OUTPUT
sleep 3
if kill -0 $SERVER_PID 2>/dev/null; then
echo "✅ BotXByte server started (PID: $SERVER_PID)"
echo " WebSocket: ws://localhost:8765"
echo " HTTP API: http://localhost:8766"
else
echo "❌ BotXByte server failed to start"
exit 1
fi
- name: Setup Browser Profile
if: steps.config.outputs.vnc_enabled == 'true'
id: browser_profile
env:
GH_TOKEN: ${{ steps.config.outputs.profile_github_token }}
run: |
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
GITHUB_REPO="${{ steps.config.outputs.github_owner }}/${{ steps.config.outputs.github_repo_name }}"
PROFILE_DIR="$HOME/browser-profiles/${BROWSER_PROFILE_ID}"
echo "browser_profile_dir=$PROFILE_DIR" >> $GITHUB_OUTPUT
mkdir -p "$HOME/browser-profiles"
set +e
PROFILE_EXISTS="false"
MAX_PROFILE_BYTES=524288000 # 500 MB
# === Download from GitHub Release ===
if [ -n "$GITHUB_REPO" ] && [ "$GITHUB_REPO" != "/" ] && [ -n "$GH_TOKEN" ]; then
echo "Checking GitHub Release for existing browser profile..."
if gh release download browser-profile -p "profile.zip" -D /tmp/ -R "$GITHUB_REPO" 2>/dev/null; then
ZIP_SIZE=$(stat -c%s /tmp/profile.zip 2>/dev/null || echo 0)
ZIP_MB=$((ZIP_SIZE / 1024 / 1024))
echo " Downloaded profile.zip from GitHub Release: ${ZIP_MB} MB"
if [ "$ZIP_SIZE" -gt "$MAX_PROFILE_BYTES" ]; then
echo "⚠️ Profile > 500 MB — deleting release asset and starting fresh"
gh release delete-asset browser-profile profile.zip -R "$GITHUB_REPO" -y 2>/dev/null || true
rm -f /tmp/profile.zip
else
if unzip -o /tmp/profile.zip -d "$HOME/browser-profiles/" >/dev/null 2>&1; then
rm -f /tmp/profile.zip
if [ -d "$PROFILE_DIR" ]; then
echo "✅ Browser profile extracted from GitHub Release"
echo " Profile size: $(du -sh "$PROFILE_DIR" 2>/dev/null | cut -f1)"
PROFILE_EXISTS="true"
fi
else
echo "⚠️ Unzip failed"
rm -f /tmp/profile.zip
fi
fi
else
echo "No profile found in GitHub Release"
fi
fi
# === FRESH PROFILE if nothing downloaded ===
if [ "$PROFILE_EXISTS" = "false" ]; then
echo "Creating fresh browser profile: $PROFILE_DIR"
rm -rf "$PROFILE_DIR"
mkdir -p "$PROFILE_DIR"
echo "✅ Fresh browser profile directory created"
fi
echo "profile_exists=$PROFILE_EXISTS" >> $GITHUB_OUTPUT
echo "Browser Profile ID: $BROWSER_PROFILE_ID"
echo "Profile Directory: $PROFILE_DIR"
exit 0
- name: Open Chrome on Desktop
if: steps.config.outputs.vnc_enabled == 'true'
run: |
echo "Opening Chrome on VNC desktop with browser profile..."
PROFILE_DIR="${{ steps.browser_profile.outputs.browser_profile_dir }}"
echo "Using profile directory: $PROFILE_DIR"
export DISPLAY=:1
export NO_AT_BRIDGE=1
export DBUS_SESSION_BUS_ADDRESS=/dev/null
# Build proxy flag if proxy is configured
PROXY_HOST="${{ steps.config.outputs.proxy_host }}"
PROXY_PORT="${{ steps.config.outputs.proxy_port }}"
PROXY_TYPE="${{ steps.config.outputs.proxy_type }}"
CHROME_PROXY_FLAG=""
if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "" ] && [ -n "$PROXY_PORT" ] && [ "$PROXY_PORT" != "" ]; then
CHROME_PROXY_FLAG="--proxy-server=${PROXY_TYPE}://${PROXY_HOST}:${PROXY_PORT}"
echo "Using proxy: $CHROME_PROXY_FLAG"
fi
google-chrome \
--no-first-run \
--user-data-dir="$PROFILE_DIR" \
$CHROME_PROXY_FLAG \
2>/tmp/chrome-stderr.log &
CHROME_PID=$!
echo " Chrome PID: $CHROME_PID"
sleep 5
if pgrep -f "chrome" > /dev/null; then
echo "✅ Chrome is running with persistent profile"
else
echo "❌ Chrome failed to launch"
tail -20 /tmp/chrome-stderr.log 2>/dev/null || true
exit 1
fi
- name: Load or Reload Extension via PyAutoGUI
if: steps.config.outputs.vnc_enabled == 'true'
run: |
export DISPLAY=:1
EXTENSION_DIR="$HOME/Downloads/real-botxbyte-extension"
PROFILE_EXISTS="${{ steps.browser_profile.outputs.profile_exists }}"
echo "profile_exists=$PROFILE_EXISTS"
cd "$EXTENSION_DIR"
source venv/bin/activate
if [ "$PROFILE_EXISTS" = "true" ]; then
echo "🔄 Profile existed → running refresh.py (reload extension)"
python3 refresh.py
else
echo "📦 Fresh profile → running opener.py (load extension first time)"
python3 opener.py
fi
echo "✅ Extension loader script finished"
- name: Check Developer Mode and Update DB
if: steps.config.outputs.vnc_enabled == 'true'
run: |
echo "Checking Chrome developer_mode status..."
PROFILE_DIR="${{ steps.browser_profile.outputs.browser_profile_dir }}"
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
USER_ID="${{ steps.config.outputs.user_id }}"
WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
# Search for "developer_mode" in the browser profile preferences
# Chrome stores developer_mode as an encrypted/hashed string, not a boolean
# The presence of the key means developer mode was enabled
IS_CHROME_EXTENSION="false"
if grep -rq '"developer_mode"' "$PROFILE_DIR/" 2>/dev/null; then
IS_CHROME_EXTENSION="true"
echo "✅ developer_mode key found → setting is_chrome_extension=true"
else
echo "ℹ️ developer_mode key not found → is_chrome_extension=false"
fi
echo "is_chrome_extension: $IS_CHROME_EXTENSION"
# Log the grep results for debugging
echo "--- Developer mode grep results ---"
grep -ro '"developer_mode":[^,}]*' "$PROFILE_DIR/" 2>/dev/null || echo " No developer_mode entries found"
echo "---"
# Update is_chrome_extension in the database via PATCH API
if [ -n "$BROWSER_PROFILE_ID" ] && [ "$BROWSER_PROFILE_ID" != "null" ]; then
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
"http://159.203.158.168/ai-management-service/api/v1/browser-profile/${BROWSER_PROFILE_ID}" \
-X PATCH \
-H "Content-Type: application/json" \
-H "user-id: ${USER_ID}" \
-H "workspace-id: ${WORKSPACE_ID}" \
-d "{\"is_chrome_extension\": ${IS_CHROME_EXTENSION}}")
SUCCESS=$(echo "$RESPONSE" | jq -r '.success // false')
if [ "$SUCCESS" = "true" ]; then
echo "✅ Browser profile updated: is_chrome_extension=$IS_CHROME_EXTENSION"
else
echo "⚠️ Failed to update is_chrome_extension (non-fatal)"
echo "$RESPONSE" | jq '.' 2>/dev/null || echo "$RESPONSE"
fi
else
echo "⚠️ No browser_profile_id, skipping is_chrome_extension DB update"
fi
- name: Install cloudflared
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Installing cloudflared..."
# Check if already installed
if command -v cloudflared &> /dev/null; then
echo "✅ cloudflared already installed (cached)"
cloudflared --version
exit 0
fi
# Release dpkg lock held by unattended-upgrades (may still be running
# on a fresh Ubuntu runner if the VNC Desktop Environment step was skipped)
sudo systemctl stop unattended-upgrades 2>/dev/null || true
sudo killall -9 unattended-upgrades 2>/dev/null || true
for i in $(seq 1 12); do
if ! sudo fuser /var/lib/dpkg/lock-frontend >/dev/null 2>&1; then
break
fi
echo "Waiting for dpkg lock to release... ($i/12)"
sleep 5
done
mkdir -p ~/cached-debs
# Use cached deb if available, otherwise download
if [ -f ~/cached-debs/cloudflared-linux-amd64.deb ]; then
echo "Using cached cloudflared deb..."
else
echo "Downloading cloudflared..."
wget -q -O ~/cached-debs/cloudflared-linux-amd64.deb \
https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb
fi
sudo dpkg -i ~/cached-debs/cloudflared-linux-amd64.deb
cloudflared --version
echo "✅ cloudflared installed"
- name: Cleanup Existing Cloudflare Tunnel
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Checking for existing tunnel..."
TUNNEL_NAME="vnc-${{ steps.config.outputs.browser_profile_id }}"
echo "Tunnel name: $TUNNEL_NAME"
# List tunnels and find matching one
EXISTING_TUNNEL=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel?name=${TUNNEL_NAME}" \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" | jq -r '.result[0].id // empty')
if [ -n "$EXISTING_TUNNEL" ]; then
echo "Found existing tunnel: $EXISTING_TUNNEL"
# First, try to clean up tunnel connections
echo "Cleaning up tunnel connections..."
CLEANUP_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${EXISTING_TUNNEL}/connections" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
echo "Cleanup connections response: $CLEANUP_RESPONSE"
# Wait for connections to close
sleep 5
# Retry deletion with backoff
MAX_RETRIES=3
RETRY_COUNT=0
DELETE_SUCCESS=false
while [ $RETRY_COUNT -lt $MAX_RETRIES ] && [ "$DELETE_SUCCESS" != "true" ]; do
RETRY_COUNT=$((RETRY_COUNT + 1))
echo "Attempting to delete tunnel (attempt $RETRY_COUNT/$MAX_RETRIES)..."
DELETE_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${EXISTING_TUNNEL}" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
DELETE_SUCCESS=$(echo "$DELETE_RESPONSE" | jq -r '.success // false')
if [ "$DELETE_SUCCESS" = "true" ]; then
echo "✅ Old tunnel deleted successfully"
else
ERROR_MSG=$(echo "$DELETE_RESPONSE" | jq -r '.errors[0].message // "Unknown error"')
echo "⚠️ Delete attempt $RETRY_COUNT failed: $ERROR_MSG"
if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then
WAIT_TIME=$((RETRY_COUNT * 2))
echo "Waiting ${WAIT_TIME} seconds before retry..."
sleep $WAIT_TIME
fi
fi
done
if [ "$DELETE_SUCCESS" != "true" ]; then
echo "⚠️ Could not delete existing tunnel after $MAX_RETRIES attempts. Will try to create a new tunnel anyway."
fi
else
echo "No existing tunnel found"
fi
- name: Create Cloudflare Tunnel
if: steps.config.outputs.cloudflare_enabled == 'true'
id: tunnel
run: |
echo "Creating Cloudflare Tunnel..."
TUNNEL_NAME="vnc-${{ steps.config.outputs.browser_profile_id }}"
echo "Tunnel name: $TUNNEL_NAME"
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel" \
--request POST \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" \
--header "Content-Type: application/json" \
--data "{
\"name\": \"${TUNNEL_NAME}\",
\"config_src\": \"cloudflare\"
}")
# Check if tunnel creation was successful
SUCCESS=$(echo "$RESPONSE" | jq -r '.success // false')
if [ "$SUCCESS" != "true" ]; then
echo "⚠️ Tunnel creation failed, checking if tunnel already exists..."
echo "$RESPONSE" | jq '.'
# If create failed (usually duplicate name), try to reuse the existing tunnel
EXISTING=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel?name=${TUNNEL_NAME}&is_deleted=false" \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
EXISTING_ID=$(echo "$EXISTING" | jq -r '.result[0].id // empty')
if [ -n "$EXISTING_ID" ]; then
echo "✅ Reusing existing tunnel: $EXISTING_ID"
# Clean up old connections so cloudflared can reconnect
curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${EXISTING_ID}/connections" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" || true
sleep 2
# Get a fresh token for the existing tunnel
TOKEN_RESP=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${EXISTING_ID}/token" \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
EXISTING_TOKEN=$(echo "$TOKEN_RESP" | jq -r '.result // empty')
if [ -n "$EXISTING_TOKEN" ] && [ "$EXISTING_TOKEN" != "null" ]; then
RESPONSE="$EXISTING"
SUCCESS="true"
else
echo "❌ Failed to get token for existing tunnel"
echo "$TOKEN_RESP" | jq '.'
exit 1
fi
else
echo "❌ Failed to create tunnel and no existing tunnel found"
exit 1
fi
fi
if [ "$SUCCESS" = "true" ]; then
# Try from create response first, fall back to existing tunnel lookup
TUNNEL_ID=$(echo "$RESPONSE" | jq -r '.result.id // .result[0].id // empty')
TUNNEL_TOKEN=$(echo "$RESPONSE" | jq -r '.result.token // empty')
# If token came from the /token endpoint (existing tunnel reuse)
if [ -z "$TUNNEL_TOKEN" ] || [ "$TUNNEL_TOKEN" = "null" ]; then
TUNNEL_TOKEN="${EXISTING_TOKEN:-}"
fi
fi
if [ -z "$TUNNEL_ID" ] || [ "$TUNNEL_ID" = "null" ]; then
echo "❌ Failed to extract tunnel ID"
exit 1
fi
echo "tunnel_id=$TUNNEL_ID" >> $GITHUB_OUTPUT
echo "tunnel_token=$TUNNEL_TOKEN" >> $GITHUB_OUTPUT
echo "✅ Tunnel created: $TUNNEL_ID"
- name: Configure Tunnel
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Configuring tunnel for noVNC..."
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${{ steps.tunnel.outputs.tunnel_id }}/configurations" \
--request PUT \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" \
--header "Content-Type: application/json" \
--data "{
\"config\": {
\"ingress\": [
{
\"hostname\": \"${{ steps.config.outputs.hostname }}\",
\"path\": \"/(workflow|terminal|status|transcribe|profile|chrome|server-logs|\\\\d+).*\",
\"service\": \"http://localhost:8766\",
\"originRequest\": {}
},
{
\"hostname\": \"${{ steps.config.outputs.hostname }}\",
\"service\": \"http://localhost:${NOVNC_PORT}\",
\"originRequest\": {}
},
{
\"service\": \"http_status:404\"
}
]
}
}")
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "❌ Failed to configure tunnel"
echo "$RESPONSE" | jq '.'
exit 1
fi
echo "✅ Tunnel configured"
- name: Create DNS Record
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Creating DNS record..."
FULL_HOSTNAME="${{ steps.config.outputs.hostname }}"
echo "Hostname: $FULL_HOSTNAME"
echo "Tunnel ID: ${{ steps.tunnel.outputs.tunnel_id }}"
echo "Target: ${{ steps.tunnel.outputs.tunnel_id }}.cfargotunnel.com"
# Check for existing DNS record
EXISTING_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/zones/${{ steps.config.outputs.cloudflare_zone_id }}/dns_records?name=${FULL_HOSTNAME}" \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
echo "Existing DNS check response:"
echo "$EXISTING_RESPONSE" | jq '.'
EXISTING=$(echo "$EXISTING_RESPONSE" | jq -r '.result[0].id // empty')
if [ -n "$EXISTING" ]; then
echo "⚠️ DNS record already exists (ID: $EXISTING), deleting..."
DELETE_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/zones/${{ steps.config.outputs.cloudflare_zone_id }}/dns_records/${EXISTING}" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
echo "Delete response:"
echo "$DELETE_RESPONSE" | jq '.'
sleep 2
fi
# Create new DNS record
echo "Creating new DNS record..."
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/zones/${{ steps.config.outputs.cloudflare_zone_id }}/dns_records" \
--request POST \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" \
--header "Content-Type: application/json" \
--data "{
\"type\": \"CNAME\",
\"proxied\": true,
\"name\": \"${FULL_HOSTNAME}\",
\"content\": \"${{ steps.tunnel.outputs.tunnel_id }}.cfargotunnel.com\"
}")
echo "Create DNS response:"
echo "$RESPONSE" | jq '.'
SUCCESS=$(echo "$RESPONSE" | jq -r '.success')
if [ "$SUCCESS" != "true" ]; then
echo "❌ Failed to create DNS record"
echo "Error details:"
echo "$RESPONSE" | jq '.errors'
exit 1
fi
DNS_RECORD_ID=$(echo "$RESPONSE" | jq -r '.result.id')
echo "✅ DNS record created successfully (ID: $DNS_RECORD_ID)"
- name: Start Cloudflare Tunnel
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Starting Cloudflare Tunnel..."
cloudflared tunnel run --token ${{ steps.tunnel.outputs.tunnel_token }} > /tmp/tunnel.log 2>&1 &
# Health check - wait up to 15s
for i in $(seq 1 15); do
if pgrep -x "cloudflared" > /dev/null; then
echo "✅ Cloudflare Tunnel started in ${i}s"
exit 0
fi
sleep 1
done
echo "❌ Cloudflare tunnel failed to start"
cat /tmp/tunnel.log
exit 1
- name: Register Tunnel URL with Backend
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo "Registering tunnel URL with backend..."
FULL_HOSTNAME="${{ steps.config.outputs.hostname }}"
TUNNEL_URL="https://${FULL_HOSTNAME}"
USER_ID="${{ steps.config.outputs.user_id }}"
WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
CLOUDFLARE_TUNNEL_CREDENTIAL_ID="${{ steps.config.outputs.cloudflare_tunnel_credential_id }}"
WORKFLOW_IDS='${{ steps.config.outputs.workflow_ids }}'
# Build base JSON body
BODY="{
\"tunnel_url\": \"${TUNNEL_URL}\",
\"browser_profile_id\": \"${BROWSER_PROFILE_ID}\",
\"status\": \"created\",
\"is_active\": true"
# Append optional cloudflare_tunnel_credential_id
if [ -n "$CLOUDFLARE_TUNNEL_CREDENTIAL_ID" ] && [ "$CLOUDFLARE_TUNNEL_CREDENTIAL_ID" != "null" ]; then
BODY="${BODY}, \"cloudflare_tunnel_credential_id\": \"${CLOUDFLARE_TUNNEL_CREDENTIAL_ID}\""
fi
# Append optional workflow_ids for auto-task dispatch (JSON array)
if [ -n "$WORKFLOW_IDS" ] && [ "$WORKFLOW_IDS" != "null" ] && [ "$WORKFLOW_IDS" != "[]" ]; then
BODY="${BODY}, \"workflow_ids\": ${WORKFLOW_IDS}"
fi
BODY="${BODY} }"
echo "Request body: $BODY"
RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
"http://159.203.158.168/ai-management-service/api/v1/cloudflare-tunnel-info/create/" \
-X POST \
-H "Content-Type: application/json" \
-H "user-id: ${USER_ID}" \
-H "workspace-id: ${WORKSPACE_ID}" \
-d "$BODY")
SUCCESS=$(echo "$RESPONSE" | jq -r '.success // false')
if [ "$SUCCESS" = "true" ]; then
echo "✅ Tunnel URL registered: ${TUNNEL_URL}"
else
echo "⚠️ Failed to register tunnel URL (non-fatal)"
echo "$RESPONSE" | jq '.' 2>/dev/null || echo "$RESPONSE"
fi
- name: Display VNC Connection Info
if: steps.config.outputs.cloudflare_enabled == 'true'
run: |
echo ""
echo "╔══════════════════════════════════════════════════════════════╗"
echo "║ 🎉 UBUNTU DESKTOP VNC IS NOW RUNNING! 🎉 ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "🌐 Access URL: https://${{ steps.config.outputs.hostname }}/vnc.html"
echo "🔑 Password: (configured)"
echo ""
echo "🔌 BotXByte server.py running"
echo " WebSocket: ws://localhost:8765"
echo " HTTP API: http://localhost:8766"
echo ""
echo "📦 Extension ready at: ~/Downloads/real-botxbyte-extension"
echo " Load it manually: Chrome > Extensions > Developer Mode > Load Unpacked"
echo ""
- name: Save Dependencies Cache
if: steps.config.outputs.vnc_enabled == 'true' && steps.deps-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v4
with:
path: |
~/cached-debs
~/noVNC
~/.cache/pip
key: vnc-deps-v5-${{ runner.os }}
- name: Keep VNC Session Alive
if: steps.config.outputs.vnc_enabled == 'true'
run: |
export PATH=$PATH:/opt/TurboVNC/bin
export DBUS_SESSION_BUS_ADDRESS=/dev/null
echo "Keeping VNC session alive..."
# Monitor for configured timeout (default 45 min)
TIMEOUT_SECONDS=${{ steps.config.outputs.profile_timeout_seconds }}
TIMEOUT_MINUTES=${{ steps.config.outputs.profile_timeout_minutes }}
echo "⏱️ Profile timeout: ${TIMEOUT_MINUTES} minutes (${TIMEOUT_SECONDS} seconds)"
END_TIME=$((SECONDS + TIMEOUT_SECONDS))
while [ $SECONDS -lt $END_TIME ]; do
REMAINING=$((END_TIME - SECONDS))
HOURS=$((REMAINING / 3600))
MINUTES=$(((REMAINING % 3600) / 60))
if [ "${{ steps.config.outputs.cloudflare_enabled }}" = "true" ]; then
echo "⏱️ Remaining: ${HOURS}h ${MINUTES}m | Access: https://${{ steps.config.outputs.hostname }}/vnc.html"
else
echo "⏱️ Remaining: ${HOURS}h ${MINUTES}m | VNC running locally"
fi
# Check and restart VNC if needed
if ! pgrep -f "Xvnc.*:1" > /dev/null; then
echo "❌ VNC stopped! Restarting..."
vncserver :1 -geometry 1920x1080 -depth 24 -rfbport $VNC_PORT
sleep 3
if ! pgrep -f "Xvnc.*:1" > /dev/null; then
echo "❌ Failed to restart VNC! Exiting..."
exit 1
fi
echo "✅ VNC restarted successfully"
fi
# Check and restart tunnel if Cloudflare is enabled
if [ "${{ steps.config.outputs.cloudflare_enabled }}" = "true" ]; then
if ! pgrep -x "cloudflared" > /dev/null; then
echo "❌ Tunnel stopped! Restarting..."
cloudflared tunnel run --token ${{ steps.tunnel.outputs.tunnel_token }} > /tmp/tunnel.log 2>&1 &
sleep 5
if ! pgrep -x "cloudflared" > /dev/null; then
echo "❌ Failed to restart tunnel! Exiting..."
cat /tmp/tunnel.log
exit 1
fi
echo "✅ Tunnel restarted successfully"
fi
fi
# Check and restart BotXByte server if needed
if ! pgrep -f "server.py" > /dev/null; then
echo "❌ BotXByte server stopped! Restarting..."
EXTENSION_DIR="$HOME/Downloads/real-botxbyte-extension"
cd "$EXTENSION_DIR"
source venv/bin/activate
export WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
python3 server.py &
sleep 3
if pgrep -f "server.py" > /dev/null; then
echo "✅ BotXByte server restarted"
else
echo "⚠️ BotXByte server failed to restart"
fi
fi
# Check and restart Chrome if needed
if ! pgrep -f "google-chrome" > /dev/null; then
echo "❌ Chrome stopped! Restarting..."
export DISPLAY=:1
export NO_AT_BRIDGE=1
PROFILE_DIR="${{ steps.browser_profile.outputs.browser_profile_dir }}"
PROXY_HOST="${{ steps.config.outputs.proxy_host }}"
PROXY_PORT="${{ steps.config.outputs.proxy_port }}"
PROXY_TYPE="${{ steps.config.outputs.proxy_type }}"
CHROME_PROXY_FLAG=""
if [ -n "$PROXY_HOST" ] && [ "$PROXY_HOST" != "" ] && [ -n "$PROXY_PORT" ] && [ "$PROXY_PORT" != "" ]; then
CHROME_PROXY_FLAG="--proxy-server=${PROXY_TYPE}://${PROXY_HOST}:${PROXY_PORT}"
fi
google-chrome \
--no-first-run \
--no-default-browser-check \
--user-data-dir="$PROFILE_DIR" \
$CHROME_PROXY_FLAG \
2>/tmp/chrome-stderr.log &
sleep 5
if pgrep -f "google-chrome" > /dev/null; then
echo "✅ Chrome restarted"
else
echo "⚠️ Chrome failed to restart"
fi
fi
sleep 30
done
echo "✅ Session time expired"
- name: Cleanup BotXByte Server and Chrome
if: always() && steps.config.outputs.vnc_enabled == 'true'
timeout-minutes: 1
run: |
echo "Cleaning up BotXByte server and Chrome..."
# Kill Chrome
pkill -f "google-chrome" || true
# Kill server.py
pkill -f "server.py" || true
sleep 2
echo "✅ BotXByte cleanup complete"
- name: Cleanup Cloudflare Tunnel and DNS
if: always() && steps.config.outputs.cloudflare_enabled == 'true'
timeout-minutes: 3
run: |
# Cleanup is best-effort: never let a single curl timeout (exit 28)
# or other transient failure abort the step. Each command logs its
# own warning; the step always exits 0.
set +e
echo "Cleaning up Cloudflare tunnel and DNS record..."
# Stop cloudflared process
pkill -x cloudflared || true
sleep 2
# Delete tunnel info from AI Management Service
FULL_HOSTNAME="${{ steps.config.outputs.hostname }}"
if [ -n "$FULL_HOSTNAME" ] && [ "$FULL_HOSTNAME" != "null" ]; then
echo "Deleting tunnel info from AI Management Service..."
USER_ID="${{ steps.config.outputs.user_id }}"
WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
# Build URL with parameters
DELETE_URL="http://159.203.158.168/ai-management-service/api/v1/cloudflare-tunnel-info/delete/?tunnel_url=https://${FULL_HOSTNAME}"
# Add browser_profile_id if it exists
if [ -n "$BROWSER_PROFILE_ID" ] && [ "$BROWSER_PROFILE_ID" != "null" ]; then
DELETE_URL="${DELETE_URL}&browser_profile_id=${BROWSER_PROFILE_ID}"
fi
# Generous timeouts + automatic retries so a slow AI-management
# response never aborts cleanup with curl exit code 28.
DELETE_TUNNEL_INFO=$(curl -sS --connect-timeout 10 --max-time 60 \
--retry 3 --retry-all-errors --retry-delay 5 --retry-max-time 120 \
"$DELETE_URL" \
-X DELETE \
-H "user-id: ${USER_ID}" \
-H "workspace-id: ${WORKSPACE_ID}" 2>&1)
CURL_RC=$?
if [ $CURL_RC -ne 0 ]; then
echo "⚠️ Tunnel info DELETE failed (curl exit=${CURL_RC}); continuing cleanup"
else
echo "Tunnel info deletion response:"
echo "$DELETE_TUNNEL_INFO"
fi
fi
# Delete DNS record
if [ -n "$FULL_HOSTNAME" ] && [ "$FULL_HOSTNAME" != "null" ]; then
echo "Deleting DNS record for ${FULL_HOSTNAME}..."
EXISTING_DNS=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/zones/${{ steps.config.outputs.cloudflare_zone_id }}/dns_records?name=${FULL_HOSTNAME}" \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}" | jq -r '.result[0].id // empty')
if [ -n "$EXISTING_DNS" ]; then
DELETE_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/zones/${{ steps.config.outputs.cloudflare_zone_id }}/dns_records/${EXISTING_DNS}" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
DELETE_SUCCESS=$(echo "$DELETE_RESPONSE" | jq -r '.success // false')
if [ "$DELETE_SUCCESS" = "true" ]; then
echo "✅ DNS record deleted successfully"
else
echo "⚠️ Failed to delete DNS record"
echo "$DELETE_RESPONSE" | jq '.'
fi
else
echo "⚠️ DNS record not found"
fi
fi
# Delete Cloudflare tunnel
if [ -n "${{ steps.tunnel.outputs.tunnel_id }}" ] && [ "${{ steps.tunnel.outputs.tunnel_id }}" != "null" ]; then
echo "Deleting Cloudflare tunnel ${{ steps.tunnel.outputs.tunnel_id }}..."
TUNNEL_DELETE_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
-A "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36" \
"https://api.cloudflare.com/client/v4/accounts/${{ steps.config.outputs.cloudflare_account_id }}/cfd_tunnel/${{ steps.tunnel.outputs.tunnel_id }}" \
--request DELETE \
--header "Authorization: Bearer ${{ steps.config.outputs.cloudflare_api_token }}")
TUNNEL_DELETE_SUCCESS=$(echo "$TUNNEL_DELETE_RESPONSE" | jq -r '.success // false')
if [ "$TUNNEL_DELETE_SUCCESS" = "true" ]; then
echo "✅ Cloudflare tunnel deleted successfully"
else
echo "⚠️ Failed to delete Cloudflare tunnel"
echo "$TUNNEL_DELETE_RESPONSE" | jq '.'
fi
else
echo "⚠️ Tunnel ID not found"
fi
echo "✅ Cloudflare cleanup complete"
exit 0
- name: Cleanup Webshare IP Authorization
if: always() && steps.config.outputs.webshare_enabled == 'true'
timeout-minutes: 1
run: |
echo "Deregistering IP from Webshare..."
if [ -n "${{ steps.webshare.outputs.webshare_auth_id }}" ] && [ "${{ steps.webshare.outputs.webshare_auth_id }}" != "null" ]; then
DELETE_RESPONSE=$(curl -s --connect-timeout 10 --max-time 30 \
"https://proxy.webshare.io/api/v2/proxy/ipauthorization/${{ steps.webshare.outputs.webshare_auth_id }}/" \
-X DELETE \
-H "Authorization: Token ${{ steps.config.outputs.webshare_api_token }}")
# Check if deletion was successful (empty response or 204 status)
if [ -z "$DELETE_RESPONSE" ] || [ "$DELETE_RESPONSE" = "{}" ]; then
echo "✅ IP authorization removed from Webshare"
else
echo "⚠️ Failed to remove IP authorization"
echo "$DELETE_RESPONSE"
fi
else
echo "⚠️ No Webshare authorization ID found to delete"
fi
echo "✅ Webshare cleanup complete"
- name: Cleanup VNC
if: always() && steps.config.outputs.vnc_enabled == 'true'
timeout-minutes: 1
run: |
export PATH=$PATH:/opt/TurboVNC/bin
echo "Cleaning up VNC server..."
vncserver -kill :1 || true
echo "✅ VNC cleanup complete"
- name: Upload Browser Profile
if: always() && steps.config.outputs.vnc_enabled == 'true'
timeout-minutes: 5
env:
GH_TOKEN: ${{ steps.config.outputs.profile_github_token }}
run: |
echo "Uploading browser profile..."
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
GITHUB_REPO="${{ steps.config.outputs.github_owner }}/${{ steps.config.outputs.github_repo_name }}"
PROFILE_DIR="$HOME/browser-profiles/${BROWSER_PROFILE_ID}"
if [ -z "$BROWSER_PROFILE_ID" ] || [ "$BROWSER_PROFILE_ID" = "" ]; then
echo "⚠️ No browser_profile_id, skipping upload"
exit 0
fi
if [ ! -d "$PROFILE_DIR" ]; then
echo "⚠️ Browser profile directory not found: $PROFILE_DIR, skipping upload"
exit 0
fi
echo "Cleaning up browser profile cache files..."
echo " Profile size before cleanup: $(du -sh "$PROFILE_DIR" | cut -f1)"
# Remove Chrome cache directories to reduce upload size
find "$PROFILE_DIR" -type d \( \
-name "Cache" \
-o -name "Code Cache" \
-o -name "GPUCache" \
-o -name "ShaderCache" \
-o -name "GrShaderCache" \
-o -name "CacheStorage" \
-o -name "ScriptCache" \
-o -name "Service Worker" \
-o -name "blob_storage" \
-o -name "IndexedDB" \
-o -name "Session Storage" \
-o -name "Crashpad" \
-o -name "BrowserMetrics" \
\) -exec rm -rf {} + 2>/dev/null || true
# Remove large temporary/log files
find "$PROFILE_DIR" -type f \( \
-name "*.log" \
-o -name "*.tmp" \
-o -name "LOG" \
-o -name "LOG.old" \
-o -name "LOCK" \
-o -name "TransportSecurity" \
\) -delete 2>/dev/null || true
echo " Profile size after cleanup: $(du -sh "$PROFILE_DIR" | cut -f1)"
# Hard size cap
MAX_PROFILE_BYTES=524288000 # 500 MB
PROFILE_BYTES=$(du -sb "$PROFILE_DIR" | cut -f1)
PROFILE_MB=$((PROFILE_BYTES / 1024 / 1024))
if [ "$PROFILE_BYTES" -gt "$MAX_PROFILE_BYTES" ] 2>/dev/null; then
echo "❌ Profile is still > 500 MB after cleanup — skipping upload"
exit 0
fi
echo "Zipping browser profile..."
cd "$HOME/browser-profiles"
zip -r "/tmp/profile.zip" "${BROWSER_PROFILE_ID}/" -x "*/SingletonLock" "*/SingletonCookie" "*/SingletonSocket"
ZIP_SIZE=$(du -sh "/tmp/profile.zip" | cut -f1)
echo " Zip size: $ZIP_SIZE"
# Upload to GitHub Release (only destination)
if [ -n "$GITHUB_REPO" ] && [ "$GITHUB_REPO" != "/" ] && [ -n "$GH_TOKEN" ]; then
echo "Uploading to GitHub Release: $GITHUB_REPO"
# Create release if not exists
gh release create browser-profile --title "Browser Profile" --notes "Auto-managed browser profile storage" -R "$GITHUB_REPO" 2>/dev/null || true
# Upload (overwrite existing asset)
if gh release upload browser-profile /tmp/profile.zip --clobber -R "$GITHUB_REPO" 2>/dev/null; then
echo "✅ Browser profile uploaded to GitHub Release"
else
echo "❌ GitHub Release upload failed"
fi
else
echo "❌ No GitHub repo configured — cannot upload profile"
fi
rm -f "/tmp/profile.zip"
- name: Trigger Auto-Restart
if: always() && steps.config.outputs.vnc_enabled == 'true'
timeout-minutes: 5
run: |
echo "Triggering auto-restart check..."
BROWSER_PROFILE_ID="${{ steps.config.outputs.browser_profile_id }}"
USER_ID="${{ steps.config.outputs.user_id }}"
WORKSPACE_ID="${{ steps.config.outputs.workspace_id }}"
if [ -z "$BROWSER_PROFILE_ID" ] || [ "$BROWSER_PROFILE_ID" = "null" ]; then
echo "⚠️ No browser_profile_id, skipping auto-restart trigger"
exit 0
fi
# Call the auto-restart trigger endpoint
# Timeout is set to 180s to accommodate delay_seconds (default 30s) + github-management-service call time
echo "Calling auto-restart trigger for profile: $BROWSER_PROFILE_ID"
RESPONSE=$(curl -s --connect-timeout 30 --max-time 180 \
-X POST \
"http://159.203.158.168/ai-management-service/api/v1/browser-profile/auto-restart/trigger" \
-H "Content-Type: application/json" \
-H "user-id: ${USER_ID}" \
-H "workspace-id: ${WORKSPACE_ID}" \
-d "{\"profile_id\": \"${BROWSER_PROFILE_ID}\", \"reason\": \"timeout\"}")
echo "Auto-restart trigger response:"
echo "$RESPONSE" | jq '.' 2>/dev/null || echo "$RESPONSE"
# Check if restart was triggered
RESTARTED=$(echo "$RESPONSE" | jq -r '.data.restarted // false')
if [ "$RESTARTED" = "true" ]; then
echo "✅ Auto-restart triggered successfully"
else
REASON=$(echo "$RESPONSE" | jq -r '.data.reason // "unknown"')
echo "ℹ️ Auto-restart not triggered: $REASON"
fi
echo "✅ Auto-restart check complete"