Files
shared-actions/celld-deploy/deploy.sh
T

234 lines
9.1 KiB
Bash
Executable File

#!/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 <<EOF > "${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}"