Skip to content

fix: add alembic merge migration to resolve multiple heads (#370) #977

fix: add alembic merge migration to resolve multiple heads (#370)

fix: add alembic merge migration to resolve multiple heads (#370) #977

Workflow file for this run

name: CI
on:
push:
branches: [main, develop, dev]
pull_request:
branches: [main, develop, dev]
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
backend-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: components/backend
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "components/backend/uv.lock"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run Ruff linter
run: uv run ruff check src/ tests/
- name: Run Ruff formatter
run: uv run ruff format --check src/ tests/
- name: Run mypy
run: uv run mypy src/
backend-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: components/backend
strategy:
matrix:
python-version: ["3.12", "3.13"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "components/backend/uv.lock"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv python pin ${{ matrix.python-version }}
uv sync --all-extras --dev
- name: Run tests with coverage (parallel execution)
run: uv run pytest --cov-fail-under=0
# Tests run in parallel using pytest-xdist (-n auto configured in pyproject.toml)
- name: Upload coverage reports
if: matrix.python-version == '3.12'
uses: codecov/codecov-action@v5
with:
file: ./components/backend/coverage.xml
fail_ci_if_error: false
# ============================================================================
# Aggregator (syfthub-aggregator)
# ============================================================================
aggregator-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: components/aggregator
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "components/aggregator/uv.lock"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run Ruff linter
run: uv run ruff check src/ tests/
- name: Run Ruff formatter
run: uv run ruff format --check src/ tests/
- name: Run mypy
run: uv run mypy src/
aggregator-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: components/aggregator
strategy:
matrix:
python-version: ["3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "components/aggregator/uv.lock"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv python pin ${{ matrix.python-version }}
uv sync --all-extras --dev
- name: Run tests (parallel execution)
run: uv run pytest
# Tests run in parallel using pytest-xdist (-n auto configured in pyproject.toml)
# ============================================================================
# CLI (syfthub-cli) - Go
# ============================================================================
cli-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cli
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: cli/go.sum
- name: Run go vet
run: go vet ./...
- name: Run go fmt check
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "The following files are not formatted:"
gofmt -l .
exit 1
fi
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- name: Run staticcheck
run: staticcheck ./...
cli-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: cli
strategy:
matrix:
go-version: ['1.23', '1.24']
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache-dependency-path: cli/go.sum
- name: Download dependencies
run: go mod download
- name: Run tests with coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage reports
if: matrix.go-version == '1.24'
uses: codecov/codecov-action@v5
with:
files: ./cli/coverage.out
flags: cli
fail_ci_if_error: false
# ============================================================================
# Python SDK (syfthub-sdk)
# ============================================================================
python-sdk-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdk/python
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "sdk/python/uv.lock"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"
- name: Install dependencies
run: uv sync --all-extras --dev
- name: Run Ruff linter
run: uv run ruff check src/ tests/
- name: Run Ruff formatter
run: uv run ruff format --check src/ tests/
- name: Run mypy
run: uv run mypy src/
python-sdk-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdk/python
strategy:
matrix:
python-version: ["3.10", "3.11", "3.12"]
steps:
- uses: actions/checkout@v4
- name: Install uv
uses: astral-sh/setup-uv@v5
with:
enable-cache: true
cache-dependency-glob: "sdk/python/uv.lock"
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v5
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
uv python pin ${{ matrix.python-version }}
uv sync --all-extras --dev
- name: Run unit tests (parallel execution)
run: uv run pytest tests/unit
# Tests run in parallel using pytest-xdist (-n auto configured in pyproject.toml)
# ============================================================================
# Go SDK (sdk/golang)
# ============================================================================
go-sdk-lint:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdk/golang
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: sdk/golang/go.sum
- name: Run go vet
run: go vet ./...
- name: Run go fmt check
run: |
if [ -n "$(gofmt -l .)" ]; then
echo "The following files are not formatted:"
gofmt -l .
exit 1
fi
- name: Install staticcheck
run: go install honnef.co/go/tools/cmd/staticcheck@latest
- name: Run staticcheck
run: staticcheck ./...
go-sdk-test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: sdk/golang
strategy:
matrix:
go-version: ['1.23', '1.24']
steps:
- uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ matrix.go-version }}
cache-dependency-path: sdk/golang/go.sum
- name: Download dependencies
run: go mod download
- name: Run tests with coverage
run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./...
- name: Upload coverage reports
if: matrix.go-version == '1.24'
uses: codecov/codecov-action@v5
with:
files: ./sdk/golang/coverage.out
flags: go-sdk
fail_ci_if_error: false
# ============================================================================
# Frontend
# ============================================================================
frontend-lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
cache-dependency-path: components/frontend/package-lock.json
- name: Install SDK dependencies
working-directory: sdk/typescript
run: npm ci
- name: Build SDK
working-directory: sdk/typescript
run: npm run build
- name: Install frontend dependencies
working-directory: components/frontend
run: npm ci
- name: Run ESLint
working-directory: components/frontend
run: npm run lint
- name: Run Prettier check
working-directory: components/frontend
run: npx prettier --check src/
- name: Run TypeScript type check
working-directory: components/frontend
run: npm run typecheck
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
cache-dependency-path: components/frontend/package-lock.json
- name: Install SDK dependencies
working-directory: sdk/typescript
run: npm ci
- name: Build SDK
working-directory: sdk/typescript
run: npm run build
- name: Install frontend dependencies
working-directory: components/frontend
run: npm ci
- name: Install Playwright browsers
working-directory: components/frontend
run: npx playwright install --with-deps
- name: Run Playwright tests
working-directory: components/frontend
run: npm test
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: components/frontend/playwright-report/
retention-days: 30
frontend-build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: lts/*
cache: 'npm'
cache-dependency-path: components/frontend/package-lock.json
- name: Install SDK dependencies
working-directory: sdk/typescript
run: npm ci
- name: Build SDK
working-directory: sdk/typescript
run: npm run build
- name: Install frontend dependencies
working-directory: components/frontend
run: npm ci
- name: Build for production
working-directory: components/frontend
run: npm run build
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: frontend-build
path: components/frontend/dist/
retention-days: 7
# ============================================================================
# BUILD AND PUSH DOCKER IMAGES TO GHCR
# Only runs on main branch after all CI checks pass
# ============================================================================
build-images:
name: Build ${{ matrix.image }}
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [backend-lint, backend-test, aggregator-lint, aggregator-test, cli-lint, cli-test, python-sdk-lint, python-sdk-test, go-sdk-lint, go-sdk-test, frontend-lint, frontend-test, frontend-build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- image: backend
context: ./components/backend
dockerfile: components/backend/Dockerfile
target: production
- image: frontend
context: .
dockerfile: components/frontend/Dockerfile
target: production
- image: aggregator
context: ./components/aggregator
dockerfile: components/aggregator/Dockerfile
target: ""
- image: mcp
context: .
dockerfile: components/mcp/Dockerfile
target: production
outputs:
image_tag: ${{ steps.vars.outputs.sha_short }}
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set output variables
id: vars
run: echo "sha_short=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}-${{ matrix.image }}
tags: |
type=raw,value=${{ steps.vars.outputs.sha_short }}
type=raw,value=latest
type=ref,event=tag
- name: Build and push ${{ matrix.image }}
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}
cache-to: type=gha,mode=max,scope=${{ matrix.image }}
build-args: |
${{ matrix.image == 'frontend' && format('VITE_GOOGLE_CLIENT_ID={0}', secrets.VITE_GOOGLE_CLIENT_ID) || '' }}
${{ matrix.image == 'frontend' && format('VITE_DEFAULT_MODEL={0}', secrets.VITE_DEFAULT_MODEL) || '' }}
# ============================================================================
# DEPLOY TO PRODUCTION VM
# Deploys all services to production after successful image builds
# ============================================================================
deploy:
name: Deploy to Production
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
needs: [build-images]
runs-on: ubuntu-latest
environment: production
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH key
env:
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
SSH_HOST: ${{ secrets.SSH_HOST }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p ${SSH_PORT:-22} -H "$SSH_HOST" >> ~/.ssh/known_hosts
- name: Sync config files to production
env:
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
run: |
echo "Syncing configuration files to production server..."
# Ensure destination directories exist
ssh -i ~/.ssh/deploy_key -p ${SSH_PORT:-22} \
"${SSH_USER}@${SSH_HOST}" \
"mkdir -p /opt/syfthub/nginx /opt/syfthub/nats"
# Sync docker-compose.deploy.yml
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/docker-compose.deploy.yml \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/"
# Sync nginx config
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/nginx/nginx.prod.conf \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/nginx/"
# Sync NATS config
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/nats/nats.prod.conf \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/nats/"
# Sync deploy script
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/scripts/deploy.sh \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/"
echo "Config files synced successfully"
- name: Deploy via SSH
uses: appleboy/ssh-action@v1.0.3
env:
IMAGE_TAG: ${{ needs.build-images.outputs.image_tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEILI_MASTER_KEY: ${{ secrets.MEILI_MASTER_KEY }}
NGROK_API_KEY: ${{ secrets.NGROK_API_KEY }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
envs: IMAGE_TAG,GITHUB_REPOSITORY,GHCR_TOKEN,MEILI_MASTER_KEY,NGROK_API_KEY,LINEAR_API_KEY,LINEAR_TEAM_ID,RESEND_API_KEY
script: |
set -e
cd /opt/syfthub
echo "=========================================="
echo "Deploying SyftHub: $IMAGE_TAG"
echo "Repository: $GITHUB_REPOSITORY"
echo "=========================================="
# Login to GHCR
echo "$GHCR_TOKEN" | docker login ghcr.io -u github-actions --password-stdin
# Ensure MEILI_MASTER_KEY is persisted in .env for docker-compose
if ! grep -q "^MEILI_MASTER_KEY=" .env 2>/dev/null; then
echo "MEILI_MASTER_KEY=${MEILI_MASTER_KEY}" >> .env
echo "Added MEILI_MASTER_KEY to .env"
fi
# Ensure NGROK_API_KEY is persisted in .env for docker-compose
if [ -n "$NGROK_API_KEY" ]; then
if grep -q "^NGROK_API_KEY=" .env 2>/dev/null; then
sed -i "s|^NGROK_API_KEY=.*|NGROK_API_KEY=${NGROK_API_KEY}|" .env
else
echo "NGROK_API_KEY=${NGROK_API_KEY}" >> .env
echo "Added NGROK_API_KEY to .env"
fi
fi
# Ensure LINEAR_API_KEY is persisted in .env for docker-compose
if [ -n "$LINEAR_API_KEY" ]; then
if grep -q "^LINEAR_API_KEY=" .env 2>/dev/null; then
sed -i "s|^LINEAR_API_KEY=.*|LINEAR_API_KEY=${LINEAR_API_KEY}|" .env
else
echo "LINEAR_API_KEY=${LINEAR_API_KEY}" >> .env
echo "Added LINEAR_API_KEY to .env"
fi
fi
# Ensure LINEAR_TEAM_ID is persisted in .env for docker-compose
if [ -n "$LINEAR_TEAM_ID" ]; then
if grep -q "^LINEAR_TEAM_ID=" .env 2>/dev/null; then
sed -i "s|^LINEAR_TEAM_ID=.*|LINEAR_TEAM_ID=${LINEAR_TEAM_ID}|" .env
else
echo "LINEAR_TEAM_ID=${LINEAR_TEAM_ID}" >> .env
echo "Added LINEAR_TEAM_ID to .env"
fi
fi
# Ensure RESEND_API_KEY is persisted in .env for docker-compose
if [ -n "$RESEND_API_KEY" ]; then
if grep -q "^RESEND_API_KEY=" .env 2>/dev/null; then
sed -i "s|^RESEND_API_KEY=.*|RESEND_API_KEY=${RESEND_API_KEY}|" .env
else
echo "RESEND_API_KEY=${RESEND_API_KEY}" >> .env
echo "Added RESEND_API_KEY to .env"
fi
fi
# Export for docker-compose
export IMAGE_TAG
export GITHUB_REPOSITORY
export MEILI_MASTER_KEY
export RESEND_API_KEY
# Run deployment script
./deploy.sh
- name: Deployment notification
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ Deployment successful: ${{ needs.build-images.outputs.image_tag }}"
else
echo "❌ Deployment failed"
fi
# ============================================================================
# BUILD AND PUSH DOCKER IMAGES TO GHCR (STAGING)
# Only runs on dev branch after all CI checks pass
# ============================================================================
build-images-staging:
name: Build ${{ matrix.image }} (Staging)
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
needs: [backend-lint, backend-test, go-sdk-lint, go-sdk-test, frontend-lint, frontend-test, frontend-build]
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
strategy:
fail-fast: false
matrix:
include:
- image: backend
context: ./components/backend
dockerfile: components/backend/Dockerfile
target: production
- image: frontend
context: .
dockerfile: components/frontend/Dockerfile
target: production
- image: aggregator
context: ./components/aggregator
dockerfile: components/aggregator/Dockerfile
target: ""
- image: mcp
context: .
dockerfile: components/mcp/Dockerfile
target: production
outputs:
image_tag: ${{ steps.vars.outputs.sha_short }}-staging
steps:
- uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Set output variables
id: vars
run: echo "sha_short=${GITHUB_SHA::7}" >> $GITHUB_OUTPUT
- name: Extract metadata for Docker
id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}-${{ matrix.image }}
tags: |
type=raw,value=${{ steps.vars.outputs.sha_short }}-staging
type=raw,value=staging
- name: Build and push ${{ matrix.image }} (staging)
uses: docker/build-push-action@v5
with:
context: ${{ matrix.context }}
file: ${{ matrix.dockerfile }}
target: ${{ matrix.target || '' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha,scope=${{ matrix.image }}-staging
cache-to: type=gha,mode=max,scope=${{ matrix.image }}-staging
# ============================================================================
# DEPLOY TO STAGING VM
# Deploys all services to staging after successful image builds
# ============================================================================
deploy-staging:
name: Deploy to Staging
if: github.ref == 'refs/heads/dev' && github.event_name == 'push'
needs: [build-images-staging]
runs-on: ubuntu-latest
environment: staging
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup SSH key
env:
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
SSH_HOST: ${{ secrets.SSH_HOST }}
run: |
mkdir -p ~/.ssh
echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/deploy_key
chmod 600 ~/.ssh/deploy_key
ssh-keyscan -p ${SSH_PORT:-22} -H "$SSH_HOST" >> ~/.ssh/known_hosts
- name: Sync config files to staging
env:
SSH_PORT: ${{ secrets.SSH_PORT || '22' }}
SSH_HOST: ${{ secrets.SSH_HOST }}
SSH_USER: ${{ secrets.SSH_USER }}
run: |
echo "Syncing configuration files to staging server..."
# Ensure destination directories exist
ssh -i ~/.ssh/deploy_key -p ${SSH_PORT:-22} \
"${SSH_USER}@${SSH_HOST}" \
"mkdir -p /opt/syfthub/nginx /opt/syfthub/nats"
# Sync docker-compose.deploy.yml
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/docker-compose.deploy.yml \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/"
# Sync nginx config
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/nginx/nginx.prod.conf \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/nginx/"
# Sync NATS config
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/nats/nats.prod.conf \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/nats/"
# Sync deploy script
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/scripts/deploy.sh \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/"
# Sync staging-specific compose override (lower resource limits)
scp -i ~/.ssh/deploy_key -P ${SSH_PORT:-22} \
deploy/docker-compose.staging.yml \
"${SSH_USER}@${SSH_HOST}:/opt/syfthub/"
echo "Config files synced successfully"
- name: Deploy to staging via SSH
uses: appleboy/ssh-action@v1.0.3
env:
IMAGE_TAG: ${{ needs.build-images-staging.outputs.image_tag }}
GITHUB_REPOSITORY: ${{ github.repository }}
GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }}
MEILI_MASTER_KEY: ${{ secrets.MEILI_MASTER_KEY }}
NGROK_API_KEY: ${{ secrets.NGROK_API_KEY }}
LINEAR_API_KEY: ${{ secrets.LINEAR_API_KEY }}
LINEAR_TEAM_ID: ${{ secrets.LINEAR_TEAM_ID }}
RESEND_API_KEY: ${{ secrets.RESEND_API_KEY }}
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
port: ${{ secrets.SSH_PORT }}
envs: IMAGE_TAG,GITHUB_REPOSITORY,GHCR_TOKEN,MEILI_MASTER_KEY,NGROK_API_KEY,LINEAR_API_KEY,LINEAR_TEAM_ID,RESEND_API_KEY
script: |
set -e
cd /opt/syfthub
echo "=========================================="
echo "Deploying SyftHub to STAGING: $IMAGE_TAG"
echo "Repository: $GITHUB_REPOSITORY"
echo "=========================================="
# Login to GHCR
echo "$GHCR_TOKEN" | docker login ghcr.io -u github-actions --password-stdin
# Ensure MEILI_MASTER_KEY is persisted in .env for docker-compose
if ! grep -q "^MEILI_MASTER_KEY=" .env 2>/dev/null; then
echo "MEILI_MASTER_KEY=${MEILI_MASTER_KEY}" >> .env
echo "Added MEILI_MASTER_KEY to .env"
fi
# Ensure NGROK_API_KEY is persisted in .env for docker-compose
if [ -n "$NGROK_API_KEY" ]; then
if grep -q "^NGROK_API_KEY=" .env 2>/dev/null; then
sed -i "s|^NGROK_API_KEY=.*|NGROK_API_KEY=${NGROK_API_KEY}|" .env
else
echo "NGROK_API_KEY=${NGROK_API_KEY}" >> .env
echo "Added NGROK_API_KEY to .env"
fi
fi
# Ensure LINEAR_API_KEY is persisted in .env for docker-compose
if [ -n "$LINEAR_API_KEY" ]; then
if grep -q "^LINEAR_API_KEY=" .env 2>/dev/null; then
sed -i "s|^LINEAR_API_KEY=.*|LINEAR_API_KEY=${LINEAR_API_KEY}|" .env
else
echo "LINEAR_API_KEY=${LINEAR_API_KEY}" >> .env
echo "Added LINEAR_API_KEY to .env"
fi
fi
# Ensure LINEAR_TEAM_ID is persisted in .env for docker-compose
if [ -n "$LINEAR_TEAM_ID" ]; then
if grep -q "^LINEAR_TEAM_ID=" .env 2>/dev/null; then
sed -i "s|^LINEAR_TEAM_ID=.*|LINEAR_TEAM_ID=${LINEAR_TEAM_ID}|" .env
else
echo "LINEAR_TEAM_ID=${LINEAR_TEAM_ID}" >> .env
echo "Added LINEAR_TEAM_ID to .env"
fi
fi
# Ensure RESEND_API_KEY is persisted in .env for docker-compose
if [ -n "$RESEND_API_KEY" ]; then
if grep -q "^RESEND_API_KEY=" .env 2>/dev/null; then
sed -i "s|^RESEND_API_KEY=.*|RESEND_API_KEY=${RESEND_API_KEY}|" .env
else
echo "RESEND_API_KEY=${RESEND_API_KEY}" >> .env
echo "Added RESEND_API_KEY to .env"
fi
fi
# Export for docker-compose
export IMAGE_TAG
export GITHUB_REPOSITORY
export MEILI_MASTER_KEY
export RESEND_API_KEY
export COMPOSE_OVERRIDE=docker-compose.staging.yml
# Run deployment script
./deploy.sh
- name: Staging deployment notification
if: always()
run: |
if [ "${{ job.status }}" == "success" ]; then
echo "✅ Staging deployment successful: ${{ needs.build-images-staging.outputs.image_tag }}"
else
echo "❌ Staging deployment failed"
fi