From 718cb1c4e1f8607244eb6e54e35d45e4518a6f09 Mon Sep 17 00:00:00 2001 From: azlam-salam Date: Tue, 14 Apr 2026 11:08:06 +1000 Subject: [PATCH] Initial commit: self-hosted SFP server deployment repository Provides GitHub Actions workflows for customers to initialize, update, and monitor their self-hosted SFP Pro server instances. Includes a composite action for CLI installation from Gitea and SSH setup. Co-Authored-By: Claude Opus 4.6 (1M context) --- .env.template | 115 ++++++++++++++++ .github/actions/setup-sfp/action.yml | 105 +++++++++++++++ .github/workflows/check-update.yml | 165 +++++++++++++++++++++++ .github/workflows/init.yml | 131 ++++++++++++++++++ .github/workflows/update.yml | 114 ++++++++++++++++ .gitignore | 22 +++ README.md | 194 +++++++++++++++++++++++++++ config/server-config.example.json | 15 +++ 8 files changed, 861 insertions(+) create mode 100644 .env.template create mode 100644 .github/actions/setup-sfp/action.yml create mode 100644 .github/workflows/check-update.yml create mode 100644 .github/workflows/init.yml create mode 100644 .github/workflows/update.yml create mode 100644 .gitignore create mode 100644 README.md create mode 100644 config/server-config.example.json diff --git a/.env.template b/.env.template new file mode 100644 index 0000000..4ac78b8 --- /dev/null +++ b/.env.template @@ -0,0 +1,115 @@ +# SFP Server Self-Hosted Configuration Reference +# ================================================ +# +# This file documents all configuration variables and where they come from. +# Do NOT edit this file directly -- configure values via GitHub Actions +# secrets/variables, or on the server's .env file post-init. +# +# The sfp CLI manages the actual .env file on the remote server. + +# ============================================================ +# Set via GitHub Actions Secrets (required for init & update) +# ============================================================ + +# Token to authenticate with Docker registry for pulling images +# GitHub Secret: DOCKER_REGISTRY_TOKEN +# DOCKER_REGISTRY_TOKEN= + +# Token for source.flxbl.io Gitea API (CLI download) +# GitHub Secret: GITEA_TOKEN +# GITEA_TOKEN= + +# SSH private key to access the remote server +# GitHub Secret: SSH_PRIVATE_KEY +# SSH_PRIVATE_KEY= + +# ============================================================ +# Set via GitHub Actions Secrets (conditional) +# ============================================================ + +# TLS certificate and private key (base64-encoded PEM) +# Only needed when using tls-mode=custom +# GitHub Secret: ORIGIN_CERT +# ORIGIN_CERT= + +# GitHub Secret: ORIGIN_KEY +# ORIGIN_KEY= + +# ============================================================ +# Set via GitHub Actions Variables +# ============================================================ + +# Target server hostname or IP (required) +# GitHub Variable: SSH_HOST +# SSH_HOST= + +# Tenant identifier (required) +# GitHub Variable: TENANT_NAME +# TENANT_NAME=my-company + +# FQDN for the server (required) +# GitHub Variable: DOMAIN +# DOMAIN=sfp.yourcompany.com + +# Docker registry hostname (required) +# Examples: source.flxbl.io, ghcr.io, your-registry.example.com +# GitHub Variable: DOCKER_REGISTRY +# DOCKER_REGISTRY=source.flxbl.io + +# SSH username (optional, default: root) +# GitHub Variable: SSH_USER +# SSH_USER=root + +# Full Docker image path (optional, overrides default) +# Example: source.flxbl.io/flxbl/sfp-server +# GitHub Variable: IMAGE_FQDN +# IMAGE_FQDN= + +# Docker image tag (optional, default: latest) +# Examples: latest, v3-latest, 3.28.0-12345 +# GitHub Variable: IMAGE_TAG +# IMAGE_TAG=latest + +# Number of background workers (optional, default: 1) +# GitHub Variable: WORKERS +# WORKERS=1 + +# Base directory on the remote server (optional, default: ./sfp-server) +# GitHub Variable: BASE_DIR +# BASE_DIR=./sfp-server + +# SFP CLI version to download (optional, default: latest) +# GitHub Variable: SFP_CLI_VERSION +# SFP_CLI_VERSION=latest + +# ============================================================ +# Auto-generated during init (persisted on server's .env) +# ============================================================ +# These values are automatically generated by `sfp server init` +# and stored on the remote server. They are read by `sfp server update` +# via SSH -- you do NOT need to store them in GitHub. +# +# SUPABASE_JWT_SECRET +# SUPABASE_ANON_KEY +# SUPABASE_SERVICE_KEY +# POSTGRES_PASSWORD +# HATCHET_APPLICATION_TOKEN +# HATCHET_DB_PASSWORD +# PG_META_CRYPTO_KEY +# SUPABASE_ENCRYPTION_KEY + +# ============================================================ +# Configured post-init (integration setup) +# ============================================================ +# These are NOT needed for server initialization. Configure them +# after the server is running via the integration API or by +# SSH-ing to the server and editing .env + restarting services. +# +# GITHUB_OAUTH_CLIENT_ID # GitHub OAuth App for user login +# GITHUB_OAUTH_CLIENT_SECRET +# GITHUB_APP_ID # GitHub App for repo operations +# GITHUB_APP_PRIVATE_KEY +# SLACK_APP_TOKEN # Slack integration +# SLACK_SIGNING_SECRET +# SLACK_BOT_TOKEN +# OPENAI_API_KEY # AI features diff --git a/.github/actions/setup-sfp/action.yml b/.github/actions/setup-sfp/action.yml new file mode 100644 index 0000000..b75594e --- /dev/null +++ b/.github/actions/setup-sfp/action.yml @@ -0,0 +1,105 @@ +name: 'Setup SFP CLI and SSH' +description: 'Downloads SFP CLI from Gitea releases and configures SSH access to the target server' + +inputs: + gitea-token: + description: 'Token for authenticating to source.flxbl.io Gitea API' + required: true + ssh-private-key: + description: 'SSH private key for connecting to the remote server' + required: true + ssh-host: + description: 'Hostname or IP of the target server' + required: true + cli-version: + description: 'SFP CLI version to install (default: latest non-draft release)' + required: false + default: 'latest' + +runs: + using: 'composite' + steps: + - name: Install SFP CLI from Gitea + shell: bash + env: + GITEA_TOKEN: ${{ inputs.gitea-token }} + CLI_VERSION: ${{ inputs.cli-version }} + run: | + echo "::group::Install SFP CLI" + + GITEA_API="https://source.flxbl.io/api/v1/repos/flxbl/sfp-pro/releases" + + if [ "$CLI_VERSION" = "latest" ]; then + echo "Fetching latest release from Gitea..." + RELEASE_INFO=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \ + "${GITEA_API}?limit=10") + + if [ $? -ne 0 ] || [ -z "$RELEASE_INFO" ]; then + echo "Failed to fetch releases from Gitea API" + exit 1 + fi + + # Find first non-draft release that has a .deb asset + DEB_URL=$(echo "$RELEASE_INFO" | jq -r ' + [.[] | select(.draft == false)] | first | + .assets[] | select(.name | test("_linux_amd64\\.deb$")) | + .browser_download_url') + else + echo "Fetching release matching version: $CLI_VERSION..." + RELEASE_INFO=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \ + "${GITEA_API}?limit=50") + + if [ $? -ne 0 ] || [ -z "$RELEASE_INFO" ]; then + echo "Failed to fetch releases from Gitea API" + exit 1 + fi + + # Find release with matching version in asset names + DEB_URL=$(echo "$RELEASE_INFO" | jq -r --arg ver "$CLI_VERSION" ' + [.[] | select(.draft == false) | + select(.assets[]? | .name | contains($ver))] | first | + .assets[] | select(.name | test("_linux_amd64\\.deb$")) | + .browser_download_url') + fi + + if [ -z "$DEB_URL" ] || [ "$DEB_URL" = "null" ]; then + echo "No .deb package found for version: $CLI_VERSION" + echo "Available releases:" + echo "$RELEASE_INFO" | jq -r '[.[] | select(.draft == false)] | .[0:5] | .[] | " - \(.tag_name): \([.assets[].name] | join(", "))"' + exit 1 + fi + + echo "Downloading SFP CLI from: $DEB_URL" + curl -L -f -H "Authorization: token $GITEA_TOKEN" -o /tmp/sfp-pro.deb "$DEB_URL" + + if [ $? -ne 0 ]; then + echo "Failed to download SFP CLI package" + exit 1 + fi + + echo "Installing SFP CLI..." + sudo dpkg -i /tmp/sfp-pro.deb || sudo apt-get install -f -y + rm -f /tmp/sfp-pro.deb + + echo "SFP CLI installed:" + sfp --version + + echo "::endgroup::" + + - name: Setup SSH + shell: bash + env: + SSH_PRIVATE_KEY: ${{ inputs.ssh-private-key }} + SSH_HOST: ${{ inputs.ssh-host }} + run: | + echo "::group::Setup SSH" + + mkdir -p ~/.ssh + echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + echo "Adding $SSH_HOST to known hosts..." + ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null + + echo "SSH configured for $SSH_HOST" + echo "::endgroup::" diff --git a/.github/workflows/check-update.yml b/.github/workflows/check-update.yml new file mode 100644 index 0000000..fc97f1a --- /dev/null +++ b/.github/workflows/check-update.yml @@ -0,0 +1,165 @@ +name: Check for Updates + +on: + schedule: + - cron: '0 8 * * 1' # Weekly on Monday at 8 AM UTC + workflow_dispatch: + +jobs: + check: + name: 'Check for new version' + runs-on: ubuntu-latest + + steps: + - name: Check latest version on Gitea + id: check + env: + GITEA_TOKEN: ${{ secrets.GITEA_TOKEN }} + run: | + echo "Checking for latest SFP Server release..." + + RELEASE_INFO=$(curl -sf -H "Authorization: token $GITEA_TOKEN" \ + "https://source.flxbl.io/api/v1/repos/flxbl/sfp-pro/releases?limit=10") + + if [ $? -ne 0 ] || [ -z "$RELEASE_INFO" ]; then + echo "Failed to fetch releases from Gitea API" + exit 1 + fi + + # Get latest non-draft release + LATEST_TAG=$(echo "$RELEASE_INFO" | jq -r '[.[] | select(.draft == false)] | first | .tag_name // "unknown"') + LATEST_DATE=$(echo "$RELEASE_INFO" | jq -r '[.[] | select(.draft == false)] | first | .published_at // "unknown"') + LATEST_BODY=$(echo "$RELEASE_INFO" | jq -r '[.[] | select(.draft == false)] | first | .body // ""') + + echo "latest_tag=$LATEST_TAG" >> $GITHUB_OUTPUT + echo "latest_date=$LATEST_DATE" >> $GITHUB_OUTPUT + + # Save release body for the issue + echo "$LATEST_BODY" > /tmp/release_notes.md + + echo "Latest release: $LATEST_TAG (published: $LATEST_DATE)" + + - name: Check current deployed version + id: current + env: + SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} + run: | + SSH_HOST="${{ vars.SSH_HOST }}" + SSH_USER="${{ vars.SSH_USER || 'root' }}" + TENANT="${{ vars.TENANT_NAME }}" + BASE_DIR="${{ vars.BASE_DIR || './sfp-server' }}" + + if [ -z "$SSH_HOST" ] || [ -z "$SSH_PRIVATE_KEY" ]; then + echo "SSH not configured, skipping deployed version check" + echo "current_tag=unknown" >> $GITHUB_OUTPUT + exit 0 + fi + + mkdir -p ~/.ssh + echo "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + ssh-keyscan -H "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null + + CURRENT_TAG=$(ssh -i ~/.ssh/deploy_key "$SSH_USER@$SSH_HOST" \ + "grep '^IMAGE_TAG=' ${BASE_DIR}/tenants/${TENANT}/.env 2>/dev/null | cut -d= -f2" \ + 2>/dev/null || echo "unknown") + + echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT + echo "Currently deployed: $CURRENT_TAG" + + - name: Generate summary + run: | + LATEST="${{ steps.check.outputs.latest_tag }}" + CURRENT="${{ steps.current.outputs.current_tag }}" + + echo "## SFP Server Version Check" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| | Version |" >> $GITHUB_STEP_SUMMARY + echo "|---|---------|" >> $GITHUB_STEP_SUMMARY + echo "| Latest Available | \`$LATEST\` |" >> $GITHUB_STEP_SUMMARY + echo "| Currently Deployed | \`$CURRENT\` |" >> $GITHUB_STEP_SUMMARY + + if [ "$LATEST" != "$CURRENT" ] && [ "$CURRENT" != "unknown" ] && [ "$LATEST" != "unknown" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "A newer version is available. Run the **Update SFP Server** workflow to update." >> $GITHUB_STEP_SUMMARY + elif [ "$CURRENT" = "unknown" ]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "Could not determine currently deployed version." >> $GITHUB_STEP_SUMMARY + else + echo "" >> $GITHUB_STEP_SUMMARY + echo "Server is up to date." >> $GITHUB_STEP_SUMMARY + fi + + - name: Create or update issue if update available + if: steps.check.outputs.latest_tag != steps.current.outputs.current_tag && steps.current.outputs.current_tag != 'unknown' && steps.check.outputs.latest_tag != 'unknown' + uses: actions/github-script@v7 + with: + script: | + const title = 'New SFP Server Version Available'; + const latest = '${{ steps.check.outputs.latest_tag }}'; + const current = '${{ steps.current.outputs.current_tag }}'; + const fs = require('fs'); + const releaseNotes = fs.existsSync('/tmp/release_notes.md') + ? fs.readFileSync('/tmp/release_notes.md', 'utf8') + : ''; + + const body = [ + `## New Version Available`, + ``, + `| | Version |`, + `|---|---------|`, + `| **Latest** | \`${latest}\` |`, + `| **Current** | \`${current}\` |`, + ``, + `### How to Update`, + `1. Go to **Actions** > **Update SFP Server**`, + `2. Click **Run workflow**`, + `3. Optionally specify the docker tag: \`${latest}\``, + ``, + releaseNotes ? `### Release Notes\n\n${releaseNotes}` : '', + ``, + `---`, + `*This issue was automatically created by the version check workflow.*` + ].join('\n'); + + // Check for existing open issue + const issues = await github.rest.issues.listForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + state: 'open', + labels: 'update-available', + }); + + const existing = issues.data.find(i => i.title === title); + + if (existing) { + await github.rest.issues.update({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: existing.number, + body: body, + }); + console.log(`Updated issue #${existing.number}`); + } else { + // Create label if it doesn't exist + try { + await github.rest.issues.createLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'update-available', + color: '0075ca', + description: 'A new SFP Server version is available', + }); + } catch (e) { + // Label may already exist + } + + const issue = await github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: title, + body: body, + labels: ['update-available'], + }); + console.log(`Created issue #${issue.data.number}`); + } diff --git a/.github/workflows/init.yml b/.github/workflows/init.yml new file mode 100644 index 0000000..575cde3 --- /dev/null +++ b/.github/workflows/init.yml @@ -0,0 +1,131 @@ +name: Initialize SFP Server + +on: + workflow_dispatch: + inputs: + force: + description: 'Force re-initialization (destroys existing data)' + type: boolean + default: false + tls_mode: + description: 'TLS certificate mode' + type: choice + options: + - 'custom' + - 'letsencrypt' + default: 'custom' + +jobs: + init: + name: 'Initialize server' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate required variables + run: | + MISSING="" + [ -z "${{ vars.SSH_HOST }}" ] && MISSING="$MISSING SSH_HOST" + [ -z "${{ vars.TENANT_NAME }}" ] && MISSING="$MISSING TENANT_NAME" + [ -z "${{ vars.DOMAIN }}" ] && MISSING="$MISSING DOMAIN" + [ -z "${{ vars.DOCKER_REGISTRY }}" ] && MISSING="$MISSING DOCKER_REGISTRY" + + if [ -n "$MISSING" ]; then + echo "Missing required GitHub Variables:$MISSING" + echo "" + echo "Configure these in: Settings > Secrets and variables > Actions > Variables" + exit 1 + fi + + - name: Setup SFP CLI and SSH + uses: ./.github/actions/setup-sfp + with: + gitea-token: ${{ secrets.GITEA_TOKEN }} + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + ssh-host: ${{ vars.SSH_HOST }} + cli-version: ${{ vars.SFP_CLI_VERSION || 'latest' }} + + - name: Initialize server + env: + DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY }} + DOCKER_REGISTRY_TOKEN: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + ORIGIN_CERT: ${{ secrets.ORIGIN_CERT }} + ORIGIN_KEY: ${{ secrets.ORIGIN_KEY }} + run: | + TENANT="${{ vars.TENANT_NAME }}" + DOMAIN="${{ vars.DOMAIN }}" + SSH_USER="${{ vars.SSH_USER || 'root' }}" + SSH_HOST="${{ vars.SSH_HOST }}" + TLS_MODE="${{ inputs.tls_mode }}" + WORKERS="${{ vars.WORKERS || '1' }}" + BASE_DIR="${{ vars.BASE_DIR || './sfp-server' }}" + + echo "Initializing SFP Server" + echo " Tenant: $TENANT" + echo " Domain: $DOMAIN" + echo " Host: $SSH_HOST" + echo " TLS Mode: $TLS_MODE" + echo " Workers: $WORKERS" + echo " Registry: $DOCKER_REGISTRY" + + # Build the init command + INIT_CMD="sfp server init" + INIT_CMD="$INIT_CMD --tenant \"$TENANT\"" + INIT_CMD="$INIT_CMD --mode prod" + INIT_CMD="$INIT_CMD --domain \"$DOMAIN\"" + INIT_CMD="$INIT_CMD --tls-mode \"$TLS_MODE\"" + INIT_CMD="$INIT_CMD --workers $WORKERS" + INIT_CMD="$INIT_CMD --base-dir \"$BASE_DIR\"" + INIT_CMD="$INIT_CMD --supabase-mode self-hosted" + INIT_CMD="$INIT_CMD --secrets-provider custom" + INIT_CMD="$INIT_CMD --no-interactive" + INIT_CMD="$INIT_CMD --ssh-connection \"$SSH_USER@$SSH_HOST\"" + INIT_CMD="$INIT_CMD --identity-file ~/.ssh/deploy_key" + + # Add image override if IMAGE_FQDN is configured + IMAGE_FQDN="${{ vars.IMAGE_FQDN }}" + IMAGE_TAG="${{ vars.IMAGE_TAG || 'latest' }}" + if [ -n "$IMAGE_FQDN" ]; then + INIT_CMD="$INIT_CMD --image \"${IMAGE_FQDN}:${IMAGE_TAG}\"" + fi + + # Add force flag if requested + if [ "${{ inputs.force }}" = "true" ]; then + INIT_CMD="$INIT_CMD --force" + fi + + eval "$INIT_CMD" + + - name: Output init results + if: always() + run: | + TENANT="${{ vars.TENANT_NAME }}" + RESULT_FILE="sfp-server-init-${TENANT}.json" + + echo "## SFP Server Initialization" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [ -f "$RESULT_FILE" ]; then + echo "### Results" >> $GITHUB_STEP_SUMMARY + echo '```json' >> $GITHUB_STEP_SUMMARY + cat "$RESULT_FILE" | jq '.' >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + fi + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Configuration" >> $GITHUB_STEP_SUMMARY + echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Tenant | $TENANT |" >> $GITHUB_STEP_SUMMARY + echo "| Domain | ${{ vars.DOMAIN }} |" >> $GITHUB_STEP_SUMMARY + echo "| Host | ${{ vars.SSH_HOST }} |" >> $GITHUB_STEP_SUMMARY + echo "| TLS Mode | ${{ inputs.tls_mode }} |" >> $GITHUB_STEP_SUMMARY + echo "| Registry | ${{ vars.DOCKER_REGISTRY }} |" >> $GITHUB_STEP_SUMMARY + + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. Verify the server is accessible at \`https://${{ vars.DOMAIN }}\`" >> $GITHUB_STEP_SUMMARY + echo "2. Configure integrations (GitHub OAuth, GitHub App, Slack) via the integration API or by editing \`.env\` on the server" >> $GITHUB_STEP_SUMMARY + echo "3. Use the **Update SFP Server** workflow for future updates" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/update.yml b/.github/workflows/update.yml new file mode 100644 index 0000000..a832609 --- /dev/null +++ b/.github/workflows/update.yml @@ -0,0 +1,114 @@ +name: Update SFP Server + +on: + workflow_dispatch: + inputs: + docker_tag: + description: 'Docker image tag to deploy (leave empty to use IMAGE_TAG variable or "latest")' + required: false + default: '' + skip_drain: + description: 'Skip waiting for active workflows to complete' + type: boolean + default: false + drain_timeout: + description: 'Max seconds to wait for active workflows to complete' + required: false + default: '3600' + skip_backup: + description: 'Skip backup of current configuration before update' + type: boolean + default: false + +jobs: + update: + name: 'Update server' + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Validate required variables + run: | + MISSING="" + [ -z "${{ vars.SSH_HOST }}" ] && MISSING="$MISSING SSH_HOST" + [ -z "${{ vars.TENANT_NAME }}" ] && MISSING="$MISSING TENANT_NAME" + [ -z "${{ vars.DOCKER_REGISTRY }}" ] && MISSING="$MISSING DOCKER_REGISTRY" + + if [ -n "$MISSING" ]; then + echo "Missing required GitHub Variables:$MISSING" + echo "" + echo "Configure these in: Settings > Secrets and variables > Actions > Variables" + exit 1 + fi + + - name: Setup SFP CLI and SSH + uses: ./.github/actions/setup-sfp + with: + gitea-token: ${{ secrets.GITEA_TOKEN }} + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + ssh-host: ${{ vars.SSH_HOST }} + cli-version: ${{ vars.SFP_CLI_VERSION || 'latest' }} + + - name: Update server + env: + DOCKER_REGISTRY: ${{ vars.DOCKER_REGISTRY }} + DOCKER_REGISTRY_TOKEN: ${{ secrets.DOCKER_REGISTRY_TOKEN }} + run: | + TENANT="${{ vars.TENANT_NAME }}" + SSH_USER="${{ vars.SSH_USER || 'root' }}" + SSH_HOST="${{ vars.SSH_HOST }}" + BASE_DIR="${{ vars.BASE_DIR || './sfp-server' }}" + + # Resolve image tag: workflow input > variable > "latest" + TAG="${{ inputs.docker_tag }}" + if [ -z "$TAG" ]; then + TAG="${{ vars.IMAGE_TAG || 'latest' }}" + fi + + echo "Updating SFP Server" + echo " Tenant: $TENANT" + echo " Host: $SSH_HOST" + echo " Tag: $TAG" + echo " Registry: $DOCKER_REGISTRY" + + # Build the update command + UPDATE_CMD="sfp server update" + UPDATE_CMD="$UPDATE_CMD --tenant \"$TENANT\"" + UPDATE_CMD="$UPDATE_CMD --base-dir \"$BASE_DIR\"" + UPDATE_CMD="$UPDATE_CMD --secrets-provider custom" + UPDATE_CMD="$UPDATE_CMD --ssh-connection \"$SSH_USER@$SSH_HOST\"" + UPDATE_CMD="$UPDATE_CMD --identity-file ~/.ssh/deploy_key" + UPDATE_CMD="$UPDATE_CMD --docker-tag \"$TAG\"" + + if [ "${{ inputs.skip_drain }}" = "true" ]; then + UPDATE_CMD="$UPDATE_CMD --skip-drain" + else + UPDATE_CMD="$UPDATE_CMD --drain-timeout ${{ inputs.drain_timeout }}" + fi + + if [ "${{ inputs.skip_backup }}" = "true" ]; then + UPDATE_CMD="$UPDATE_CMD --skip-backup" + fi + + eval "$UPDATE_CMD" + + - name: Output update results + if: always() + run: | + TAG="${{ inputs.docker_tag }}" + if [ -z "$TAG" ]; then + TAG="${{ vars.IMAGE_TAG || 'latest' }}" + fi + + echo "## SFP Server Update" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Setting | Value |" >> $GITHUB_STEP_SUMMARY + echo "|---------|-------|" >> $GITHUB_STEP_SUMMARY + echo "| Tenant | ${{ vars.TENANT_NAME }} |" >> $GITHUB_STEP_SUMMARY + echo "| Host | ${{ vars.SSH_HOST }} |" >> $GITHUB_STEP_SUMMARY + echo "| Image Tag | $TAG |" >> $GITHUB_STEP_SUMMARY + echo "| Registry | ${{ vars.DOCKER_REGISTRY }} |" >> $GITHUB_STEP_SUMMARY + echo "| Skip Drain | ${{ inputs.skip_drain }} |" >> $GITHUB_STEP_SUMMARY + echo "| Skip Backup | ${{ inputs.skip_backup }} |" >> $GITHUB_STEP_SUMMARY diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..61d1189 --- /dev/null +++ b/.gitignore @@ -0,0 +1,22 @@ +# OS +.DS_Store +Thumbs.db + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# Secrets (never commit these) +*.pem +*.key +*.env.local + +# Temporary files +*.tmp +*.log + + +#Ignore vscode AI rules +.github/instructions/codacy.instructions.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..e47cad1 --- /dev/null +++ b/README.md @@ -0,0 +1,194 @@ +# SFP Server Self-Hosted Deployment + +Automated deployment and management of self-hosted SFP Pro server instances using GitHub Actions. + +## Overview + +This repository provides GitHub Actions workflows to: + +- **Initialize** a new SFP server on your infrastructure +- **Update** an existing server to the latest version +- **Check** for new versions on a weekly schedule + +The workflows connect to your server via SSH, download the SFP CLI from Gitea, and run the appropriate server lifecycle commands. + +## Prerequisites + +Before using this repository, ensure you have: + +1. **Linux server** -- x86_64, 8+ vCPU, 32+ GB RAM, 250+ GB SSD +2. **Docker Engine 24+** and **Docker Compose v2** installed on the server +3. **Domain name** (FQDN) resolving to the server +4. **TLS certificate + private key** (PEM format) -- or use Let's Encrypt for automatic TLS +5. **SSH access** to the server from GitHub Actions runners +6. **Gitea token** for `source.flxbl.io` (provided by flxbl) +7. **Docker registry token** for pulling SFP server images +8. **Port 443** open on the server firewall + +For detailed requirements, see the [Self-Hosting Prerequisites](https://source.flxbl.io/flxbl/sfp-pro/src/branch/main/docs/self-hosting-prerequisites.md) guide. + +## Quick Setup + +### 1. Fork or Clone This Repository + +Fork this repository to your GitHub organization, or clone and push to a new private repository. + +### 2. Configure GitHub Secrets + +Go to **Settings** > **Secrets and variables** > **Actions** > **Secrets** and add: + +| Secret | Description | +|--------|-------------| +| `SSH_PRIVATE_KEY` | SSH private key for connecting to the server | +| `GITEA_TOKEN` | Token for `source.flxbl.io` (CLI downloads) | +| `DOCKER_REGISTRY_TOKEN` | Token for authenticating with your Docker registry | +| `ORIGIN_CERT` | TLS certificate in base64 PEM *(only if using custom TLS)* | +| `ORIGIN_KEY` | TLS private key in base64 PEM *(only if using custom TLS)* | + +### 3. Configure GitHub Variables + +Go to **Settings** > **Secrets and variables** > **Actions** > **Variables** and add: + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `SSH_HOST` | Yes | -- | Server hostname or IP address | +| `TENANT_NAME` | Yes | -- | Tenant identifier (lowercase, alphanumeric, hyphens) | +| `DOMAIN` | Yes | -- | FQDN for the server (e.g., `sfp.yourcompany.com`) | +| `DOCKER_REGISTRY` | Yes | -- | Docker registry hostname (e.g., `source.flxbl.io`) | +| `SSH_USER` | No | `root` | SSH username | +| `IMAGE_FQDN` | No | -- | Full Docker image path (e.g., `source.flxbl.io/flxbl/sfp-server`) | +| `IMAGE_TAG` | No | `latest` | Docker image tag (e.g., `latest`, `v3-latest`, `3.28.0`) | +| `WORKERS` | No | `1` | Number of background workers (1-10) | +| `BASE_DIR` | No | `./sfp-server` | Base directory on the server | +| `SFP_CLI_VERSION` | No | `latest` | Specific SFP CLI version to use | + +### 4. Run Initialization + +1. Go to **Actions** > **Initialize SFP Server** +2. Click **Run workflow** +3. Select the TLS mode (`custom` or `letsencrypt`) +4. Click **Run workflow** + +The init process will: +- Install SFP CLI on the GitHub Actions runner +- Connect to your server via SSH +- Create the directory structure and configuration +- Auto-generate database credentials +- Pull Docker images from your registry +- Start all services +- Create the default admin user + +The admin credentials will be displayed in the workflow output. + +### 5. Verify + +Open `https://your-domain` in a browser to verify the server is accessible. + +## Post-Init: Integration Setup + +After initialization, configure integrations by SSH-ing to the server and editing the `.env` file, or via the integration API: + +### GitHub OAuth (for user login) + +1. Create a GitHub OAuth App ([instructions](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/creating-an-oauth-app)) + - **Homepage URL**: `https://your-domain` + - **Callback URL**: `https://your-domain/auth/v1/callback` +2. SSH to the server and add to `.env`: + ```bash + GITHUB_OAUTH_ENABLED=true + GITHUB_OAUTH_CLIENT_ID=your-client-id + GITHUB_OAUTH_CLIENT_SECRET=your-client-secret + ``` +3. Restart services: `docker compose restart supabase-auth` + +### GitHub App (for repository operations) + +1. Create a GitHub App ([instructions](https://docs.github.com/en/apps/creating-github-apps/registering-a-github-app/registering-a-github-app)) + - **Webhook URL**: `https://your-domain/sfp/api/webhook/github` +2. Configure via the integration API (`POST /api/integrations`) or add to `.env`: + ```bash + GITHUB_APP_ID=your-app-id + GITHUB_APP_PRIVATE_KEY=your-private-key-pem + ``` + +### Slack, AI, and Other Integrations + +Add the relevant environment variables to `.env` on the server and restart: +- `SLACK_APP_TOKEN`, `SLACK_SIGNING_SECRET`, `SLACK_BOT_TOKEN` +- `OPENAI_API_KEY`, `AI_PROVIDER`, `AI_MODEL` + +## Updating + +To update the server to a new version: + +1. Go to **Actions** > **Update SFP Server** +2. Click **Run workflow** +3. Optionally specify a Docker tag (defaults to `IMAGE_TAG` variable or `latest`) +4. Configure drain and backup options as needed +5. Click **Run workflow** + +The update process: +1. Backs up current configuration (unless skipped) +2. Waits for active workflows to complete (unless skipped) +3. Updates configuration files +4. Caddy serves a maintenance page while app services restart +5. Pulls new Docker images +6. Starts services and runs database migrations + +## Version Checks + +The **Check for Updates** workflow runs weekly (Monday 8 AM UTC) and: +- Queries Gitea for the latest release +- Compares with the currently deployed version +- Creates a GitHub Issue if a newer version is available + +You can also trigger it manually from **Actions** > **Check for Updates**. + +## Backup and Recovery + +The server's `.env` file contains auto-generated credentials that are critical for operation: +- Supabase JWT secret and API keys +- Database passwords +- Hatchet workflow engine tokens + +**These are NOT stored in GitHub** -- they live on the server. + +We recommend: +- Regularly backing up the `.env` file from `{BASE_DIR}/tenants/{TENANT_NAME}/.env` +- The `sfp server update` command automatically backs up configuration before each update (stored in `backups/` on the server) +- Store a copy of `.env` in a secure location (e.g., password manager, secrets vault) + +If the server is destroyed, you will need the backed-up `.env` to restore without re-initializing. + +## Troubleshooting + +### Workflow fails at "Install SFP CLI" +- Verify `GITEA_TOKEN` is valid and has read access to `flxbl/sfp-pro` releases +- Check if a release exists with a `.deb` asset + +### Workflow fails at SSH connection +- Verify `SSH_PRIVATE_KEY` is the full private key (including headers) +- Verify `SSH_HOST` is reachable from GitHub Actions runners +- Verify the SSH user has permission to run `docker` commands + +### Server not accessible after init +- Check port 443 is open in the firewall +- Verify DNS resolves to the server IP +- Check TLS certificate is valid for the domain +- SSH to the server and check logs: `docker compose logs caddy` + +### Update fails during image pull +- Verify `DOCKER_REGISTRY_TOKEN` is valid +- Verify `DOCKER_REGISTRY` matches where your images are hosted +- Check if the specified `IMAGE_TAG` exists in the registry + +## File Reference + +| File | Purpose | +|------|---------| +| `.github/actions/setup-sfp/action.yml` | Composite action: install SFP CLI + configure SSH | +| `.github/workflows/init.yml` | One-time server initialization workflow | +| `.github/workflows/update.yml` | Server update workflow | +| `.github/workflows/check-update.yml` | Weekly version check workflow | +| `config/server-config.example.json` | Example JSON config for manual (non-workflow) init | +| `.env.template` | Reference for all configuration variables | diff --git a/config/server-config.example.json b/config/server-config.example.json new file mode 100644 index 0000000..febc027 --- /dev/null +++ b/config/server-config.example.json @@ -0,0 +1,15 @@ +{ + "domain": "sfp.yourcompany.com", + "release_cadence": "production", + "cicdProvider": "github", + "workers": 1, + "auth": { + "useGlobalAuth": false + }, + "supabase": { + "mode": "self-hosted" + }, + "tlsMode": "custom", + "image_fqdn": "source.flxbl.io/flxbl/sfp-server", + "image_tag": "latest" +}