fix: add alembic merge migration to resolve multiple heads (#370) #977
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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 |