#!/usr/bin/env bash set -euo pipefail echo "🚀 Starting celld Worker & Durable Objects Deployment..." # 1. Resolve repository, owner, and application directory FULL_REPO="${REPO_FULL_NAME:-$GITHUB_REPOSITORY}" OWNER="${REPO_OWNER:-$(dirname "$FULL_REPO")}" if [ -z "${REPO_NAME:-}" ]; then REPO_NAME=$(basename "$FULL_REPO") fi if [ -n "${INPUT_APP_NAME:-}" ]; then case "$INPUT_APP_NAME" in */*) APP_DIR_NAME="$INPUT_APP_NAME" ;; *) APP_DIR_NAME="${OWNER}/${INPUT_APP_NAME}" ;; esac else APP_DIR_NAME="$FULL_REPO" fi CLEAN_APP=$(basename "$APP_DIR_NAME" | tr '_' '-' | tr '[:upper:]' '[:lower:]') ROUTER_NAME=$(echo "$CLEAN_APP" | tr -cd '[:alnum:]') # 2. Resolve S3 bucket name RAW_BUCKET="${INPUT_BUCKET:-celld-${CLEAN_APP}}" BUCKET="${RAW_BUCKET#s3://}" BUCKET=$(echo "$BUCKET" | tr '_' '-' | tr '[:upper:]' '[:lower:]') S3_ENDPOINT="${INPUT_S3_ENDPOINT:-https://s3.rodydavis.dev}" S3_ENDPOINT="${S3_ENDPOINT%/}" INTERNAL_S3_ENDPOINT="${INPUT_INTERNAL_S3_ENDPOINT:-http://minio-s3:9000}" INTERNAL_S3_ENDPOINT="${INTERNAL_S3_ENDPOINT%/}" S3_REGION="${INPUT_S3_REGION:-us-east-1}" # 3. Resolve domain(s) and build Traefik Host() rule RAW_DOMAINS="${INPUT_DOMAIN:-${CLEAN_APP}.rodydavis.dev}" IFS=',' read -ra DOMAIN_ARR <<< "$RAW_DOMAINS" TRAEFIK_RULE="" PRIMARY_DOMAIN="" for d in "${DOMAIN_ARR[@]}"; do d_clean=$(echo "$d" | xargs | sed -E 's#^https?://##' | sed 's#/.*$##') if [ -z "$d_clean" ]; then continue; fi if [ -z "$PRIMARY_DOMAIN" ]; then PRIMARY_DOMAIN="$d_clean"; fi if [ -z "$TRAEFIK_RULE" ]; then TRAEFIK_RULE="Host(\`${d_clean}\`)" else TRAEFIK_RULE="${TRAEFIK_RULE} || Host(\`${d_clean}\`)" fi done echo "📦 Application: ${APP_DIR_NAME}" echo "🪣 Fleet S3 Bucket: s3://${BUCKET} (${S3_ENDPOINT})" echo "🌐 Routing Rule: ${TRAEFIK_RULE}" # 4. Ensure celld and esbuild binaries are on PATH if ! command -v celld >/dev/null 2>&1; then echo "⚙️ Installing celld CLI..." if command -v docker >/dev/null 2>&1 && docker image inspect ghcr.io/denoland/celld:latest >/dev/null 2>&1; then CID=$(docker create ghcr.io/denoland/celld:latest) docker cp "${CID}:/usr/local/bin/celld" /usr/local/bin/celld docker rm -f "$CID" >/dev/null chmod +x /usr/local/bin/celld else curl -fsSL https://celld.dev/install.sh | sh export PATH="$HOME/.local/bin:$PATH" fi fi echo "✅ Using $(celld --version)" WORKDIR="${INPUT_WORKING_DIRECTORY:-.}" cd "$WORKDIR" if [ -f "./node_modules/.bin/esbuild" ]; then export PATH="$(pwd)/node_modules/.bin:$PATH" fi if ! command -v esbuild >/dev/null 2>&1; then echo "⚙️ Installing esbuild..." npm install -g esbuild --no-audit --no-fund >/dev/null 2>&1 fi echo "✅ Using esbuild $(esbuild --version)" # 5. Export AWS credentials for S3 and ensure fleet bucket exists export AWS_ACCESS_KEY_ID="$INPUT_S3_ACCESS_KEY" export AWS_SECRET_ACCESS_KEY="$INPUT_S3_SECRET_KEY" export AWS_DEFAULT_REGION="$S3_REGION" EMPTY_SHA256="e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" HTTP_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ --aws-sigv4 "aws:amz:${S3_REGION}:s3" \ --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \ -H "x-amz-content-sha256: ${EMPTY_SHA256}" \ -I "${S3_ENDPOINT}/${BUCKET}" || echo "000") if [ "$HTTP_STATUS" = "404" ]; then echo "🪣 Creating S3 bucket '${BUCKET}' on ${S3_ENDPOINT}..." CREATE_STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ -X PUT \ --aws-sigv4 "aws:amz:${S3_REGION}:s3" \ --user "${AWS_ACCESS_KEY_ID}:${AWS_SECRET_ACCESS_KEY}" \ -H "x-amz-content-sha256: ${EMPTY_SHA256}" \ "${S3_ENDPOINT}/${BUCKET}") if [ "$CREATE_STATUS" != "200" ] && [ "$CREATE_STATUS" != "204" ] && [ "$CREATE_STATUS" != "409" ]; then echo "::error::Failed to create bucket '${BUCKET}' (HTTP ${CREATE_STATUS})" exit 1 fi echo "✅ Created bucket s3://${BUCKET}" elif [ "$HTTP_STATUS" = "200" ]; then echo "✅ Verified existing bucket s3://${BUCKET}" else echo "⚠️ Bucket probe returned HTTP ${HTTP_STATUS}; proceeding with celld deploy..." fi # 6. Deploy Worker bundle & assets directly to S3 bucket echo "📤 Deploying Worker bundle to s3://${BUCKET}..." celld deploy . \ --bucket "s3://${BUCKET}" \ --endpoint "${S3_ENDPOINT}" \ --region "${S3_REGION}" # 7. Ensure celld Fleet Daemon is provisioned and running on TrueNAS HOST="${INPUT_HOST:-10.0.0.218}" USER="${INPUT_USER:-root}" NETWORK="${INPUT_NETWORK:-apps-net}" ENTRYPOINTS="${INPUT_ENTRYPOINTS:-web}" IDLE_EVICT_S="${INPUT_IDLE_EVICT_S:-60}" LTX_RETENTION_SECS="${INPUT_LTX_RETENTION_SECS:-86400}" DEPLOY_POLL_S="${INPUT_DEPLOY_POLL_S:-5}" STAGING_DIR=$(mktemp -d) trap 'rm -rf "$STAGING_DIR" ~/.ssh/truenas_deploy_key' EXIT { echo "AWS_ACCESS_KEY_ID=${INPUT_S3_ACCESS_KEY}" echo "AWS_SECRET_ACCESS_KEY=${INPUT_S3_SECRET_KEY}" echo "AWS_REGION=${S3_REGION}" if [ -n "${INPUT_ENV_CONTENT:-}" ]; then printf "%s\n" "$INPUT_ENV_CONTENT" fi } > "${STAGING_DIR}/.env" if [ -f "docker-compose.yml" ]; then cp "docker-compose.yml" "${STAGING_DIR}/docker-compose.yml" elif [ -f "docker-compose.yaml" ]; then cp "docker-compose.yaml" "${STAGING_DIR}/docker-compose.yml" else cat < "${STAGING_DIR}/docker-compose.yml" services: celld: image: ghcr.io/denoland/celld:latest container_name: ${CLEAN_APP}-celld restart: unless-stopped env_file: - .env environment: - CELLD_BUCKET=s3://${BUCKET} - S3_ENDPOINT=${INTERNAL_S3_ENDPOINT} - CELLD_ADDR=0.0.0.0:8080 - CELLD_INTERNAL_ADDR=127.0.0.1:8081 - CELLD_ADVERTISE=127.0.0.1:8081 - CELLD_TRUST_FORWARDED_HEADERS=1 - CELLD_NODE=${CLEAN_APP}-truenas-1 - CELLD_WATCH=/var/lib/celld/state - CELLD_IDLE_EVICT_S=${IDLE_EVICT_S} - CELLD_LTX_RETENTION_SECS=${LTX_RETENTION_SECS} - CELLD_DEPLOY_POLL_S=${DEPLOY_POLL_S} volumes: - ./data:/var/lib/celld networks: - ${NETWORK} labels: - "traefik.enable=true" - "traefik.http.routers.${ROUTER_NAME}.rule=${TRAEFIK_RULE}" - "traefik.http.routers.${ROUTER_NAME}.entrypoints=${ENTRYPOINTS}" - "traefik.http.services.${ROUTER_NAME}.loadbalancer.server.port=8080" networks: ${NETWORK}: external: true EOF fi mkdir -p ~/.ssh echo "$INPUT_SSH_KEY" > ~/.ssh/truenas_deploy_key chmod 600 ~/.ssh/truenas_deploy_key ssh-keyscan -H "$HOST" >> ~/.ssh/known_hosts 2>/dev/null REMOTE_DIR="/mnt/jupiter/apps/${APP_DIR_NAME}" ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" "mkdir -p ${REMOTE_DIR}/data" LOCAL_ENV_HASH=$(sha256sum "${STAGING_DIR}/.env" | awk '{print $1}') LOCAL_COMPOSE_HASH=$(sha256sum "${STAGING_DIR}/docker-compose.yml" | awk '{print $1}') REMOTE_STATE=$(ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" " ENV_H=\$(sha256sum ${REMOTE_DIR}/.env 2>/dev/null | awk '{print \$1}' || echo 'none') COMP_H=\$(sha256sum ${REMOTE_DIR}/docker-compose.yml 2>/dev/null | awk '{print \$1}' || echo 'none') RUNNING=\$(docker inspect -f '{{.State.Running}}' ${CLEAN_APP}-celld 2>/dev/null || echo 'false') echo \"\${ENV_H}:\${COMP_H}:\${RUNNING}\" ") if [ "$REMOTE_STATE" != "${LOCAL_ENV_HASH}:${LOCAL_COMPOSE_HASH}:true" ]; then echo "🚢 Updating TrueNAS celld container stack at ${REMOTE_DIR}..." scp -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${STAGING_DIR}/.env" "${USER}@${HOST}:${REMOTE_DIR}/.env" scp -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${STAGING_DIR}/docker-compose.yml" "${USER}@${HOST}:${REMOTE_DIR}/docker-compose.yml" ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" "chmod 600 ${REMOTE_DIR}/.env && cd ${REMOTE_DIR} && docker compose up -d --remove-orphans" sleep 4 else echo "⚡ TrueNAS container ${CLEAN_APP}-celld is already running; triggering immediate in-place reload..." ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" " docker exec ${CLEAN_APP}-celld bash -c 'exec 3<>/dev/tcp/127.0.0.1/8081 && printf \"POST /reload HTTP/1.1\r\nHost: 127.0.0.1:8081\r\nContent-Length: 0\r\nConnection: close\r\n\r\n\" >&3 && head -n 1 <&3' 2>/dev/null || true " fi # 8. Optional D1 SQLite migrations if [ -n "${INPUT_D1_DATABASE:-}" ] && [ -d "migrations" ]; then echo "🗄️ Applying D1 migrations for database '${INPUT_D1_DATABASE}'..." ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" "rm -rf ${REMOTE_DIR}/data/project && mkdir -p ${REMOTE_DIR}/data/project/migrations" for f in wrangler.json wrangler.jsonc wrangler.toml; do if [ -f "$f" ]; then scp -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "$f" "${USER}@${HOST}:${REMOTE_DIR}/data/project/$f" fi done scp -r -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no migrations/. "${USER}@${HOST}:${REMOTE_DIR}/data/project/migrations/" ssh -i ~/.ssh/truenas_deploy_key -o StrictHostKeyChecking=no "${USER}@${HOST}" " docker exec -w /var/lib/celld/project ${CLEAN_APP}-celld celld d1 migrations apply '${INPUT_D1_DATABASE}' . \ --bucket 's3://${BUCKET}' \ --endpoint '${INTERNAL_S3_ENDPOINT}' \ --region '${S3_REGION}' " fi echo "✅ Successfully deployed ${APP_DIR_NAME} to https://${PRIMARY_DOMAIN}"