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) <noreply@anthropic.com>
This commit is contained in:
+115
@@ -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
|
||||
@@ -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::"
|
||||
@@ -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}`);
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
+22
@@ -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
|
||||
@@ -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 |
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user