From 06de7c7ab055d617f9311c1fc68d73c2887e3884 Mon Sep 17 00:00:00 2001 From: roberto Date: Tue, 3 Mar 2026 22:10:03 +0100 Subject: [PATCH] feat: deploy via SSH with port 8080, idempotent migrations --- .gitea/workflows/deploy.yaml | 106 +++++++++++-------------- alembic/versions/001_initial_schema.py | 8 +- docker-compose.yml | 5 +- 3 files changed, 53 insertions(+), 66 deletions(-) diff --git a/.gitea/workflows/deploy.yaml b/.gitea/workflows/deploy.yaml index ac64f1c..373ccb6 100644 --- a/.gitea/workflows/deploy.yaml +++ b/.gitea/workflows/deploy.yaml @@ -33,75 +33,61 @@ jobs: - name: Run Tests run: pytest tests/ -v --tb=short - # ── 2. Deploy to Docker LXC (only main branch & tags) ───────────── + # ── 2. Deploy to Docker LXC via SSH ───────────────────────────────── deploy: needs: test runs-on: ubuntu-latest if: gitea.event_name == 'push' steps: - - name: Checkout Code - run: | - cd /tmp - rm -rf adiuva-api-deploy - git clone --depth 1 "http://10.0.0.119:3000/${GITHUB_REPOSITORY}.git" adiuva-api-deploy || \ - git clone --depth 1 "http://10.0.0.119:3000/${GITHUB_REPOSITORY}.git" adiuva-api-deploy - cd adiuva-api-deploy && git checkout "${GITHUB_SHA}" 2>/dev/null || true + - name: Deploy via SSH + uses: appleboy/ssh-action@v1.0.0 + with: + host: ${{ secrets.SSH_HOST }} + username: ${{ secrets.SSH_USER }} + key: ${{ secrets.SSH_KEY }} + script: | + set -e + DEPLOY_DIR="/opt/adiuva-api" + REPO_URL="http://10.0.0.119:3000/${{ gitea.repository }}.git" + TAG="${{ gitea.ref_name }}" - - name: Sync to deploy directory - run: | - DEPLOY_DIR="/opt/adiuva-api" - SRC="/tmp/adiuva-api-deploy" - mkdir -p "$DEPLOY_DIR" + # ── Pull latest code ── + cd /tmp && rm -rf adiuva-api-deploy + git clone --depth 1 --branch "${TAG}" "${REPO_URL}" adiuva-api-deploy - # Sync source, preserve .env and volumes - cp -rf "$SRC/app/" "$SRC/alembic/" "$SRC/alembic.ini" "$SRC/Dockerfile" "$SRC/docker-compose.yml" "$SRC/requirements.txt" "$DEPLOY_DIR/" + # ── Sync source (preserve .env) ── + cp -rf /tmp/adiuva-api-deploy/app/ \ + /tmp/adiuva-api-deploy/alembic/ \ + /tmp/adiuva-api-deploy/alembic.ini \ + /tmp/adiuva-api-deploy/Dockerfile \ + /tmp/adiuva-api-deploy/docker-compose.yml \ + /tmp/adiuva-api-deploy/requirements.txt \ + "$DEPLOY_DIR/" + rm -rf /tmp/adiuva-api-deploy - - name: Build & restart services - run: | - cd /opt/adiuva-api - docker compose up -d --build --remove-orphans + # ── Verify .env ── + if [ ! -f "$DEPLOY_DIR/.env" ]; then + echo "❌ $DEPLOY_DIR/.env not found. Create it before deploying." + exit 1 + fi - - name: Run database migrations - run: | - cd /opt/adiuva-api - docker compose exec -T app alembic upgrade head + # ── Build & restart ── + cd "$DEPLOY_DIR" + docker compose down --remove-orphans || true + docker compose up -d --build - - name: Verify deployment - run: | - echo "Waiting for app to be ready..." - sleep 5 + # ── Migrations ── + docker compose exec -T app alembic upgrade head - HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/api/v1/health) - if [ "$HTTP_CODE" -eq 200 ]; then - echo "✅ API is healthy (HTTP ${HTTP_CODE})" - else - echo "❌ Health check failed (HTTP ${HTTP_CODE})" - docker compose -f /opt/adiuva-api/docker-compose.yml logs app --tail=50 - exit 1 - fi - - - name: Create Gitea Release (tags only) - if: startsWith(gitea.ref, 'refs/tags/') - run: | - GITEA_URL="http://10.0.0.119:3000" - TAG="${GITHUB_REF_NAME}" - REPO="${GITHUB_REPOSITORY}" - TOKEN="${{ gitea.token }}" - - RELEASE_ID=$(curl -sf \ - -H "Authorization: token ${TOKEN}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases/tags/${TAG}" \ - | grep -o '"id":[0-9]*' | head -1 | cut -d: -f2) - - if [ -z "$RELEASE_ID" ]; then - curl -sf \ - -X POST \ - -H "Authorization: token ${TOKEN}" \ - -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"${TAG}\",\"name\":\"Adiuva API ${TAG}\",\"body\":\"Deployed to Docker LXC\"}" \ - "${GITEA_URL}/api/v1/repos/${REPO}/releases" - echo "✅ Release ${TAG} created" - else - echo "ℹ️ Release ${TAG} already exists (ID: ${RELEASE_ID})" - fi \ No newline at end of file + # ── Health check ── + echo "Waiting for app..." + sleep 5 + HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/api/v1/health) + if [ "$HTTP_CODE" -eq 200 ]; then + echo "✅ API is healthy (HTTP ${HTTP_CODE})" + else + echo "❌ Health check failed (HTTP ${HTTP_CODE})" + docker compose logs app --tail=50 + exit 1 + fi \ No newline at end of file diff --git a/alembic/versions/001_initial_schema.py b/alembic/versions/001_initial_schema.py index abe611a..db2021f 100644 --- a/alembic/versions/001_initial_schema.py +++ b/alembic/versions/001_initial_schema.py @@ -40,7 +40,7 @@ def upgrade() -> None: sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("email", sa.String(255), nullable=False), sa.Column("password_hash", sa.String(255), nullable=False), - sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier"), nullable=False, server_default="free"), + sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier", create_type=False), nullable=False, server_default="free"), sa.Column("stripe_customer_id", sa.String(255), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("updated_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), @@ -70,7 +70,7 @@ def upgrade() -> None: sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("user_id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("stripe_subscription_id", sa.String(255), nullable=True), - sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier"), nullable=False, server_default="free"), + sa.Column("tier", sa.Enum("free", "pro", "power", "team", name="billing_tier", create_type=False), nullable=False, server_default="free"), sa.Column("status", sa.String(50), nullable=False, server_default="free"), sa.Column("current_period_end", sa.DateTime(timezone=True), nullable=True), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), @@ -125,7 +125,7 @@ def upgrade() -> None: sa.Column("category", sa.String(100), nullable=False, server_default=""), sa.Column("price_cents", sa.Integer, nullable=False, server_default="0"), sa.Column("permissions", sa.Text, nullable=False, server_default="[]"), - sa.Column("status", sa.Enum("pending_review", "approved", "rejected", name="plugin_status"), nullable=False, server_default="pending_review"), + sa.Column("status", sa.Enum("pending_review", "approved", "rejected", name="plugin_status", create_type=False), nullable=False, server_default="pending_review"), sa.Column("s3_package_key", sa.String(500), nullable=True), sa.Column("install_count", sa.Integer, nullable=False, server_default="0"), sa.Column("avg_rating", sa.Float, nullable=False, server_default="0.0"), @@ -157,7 +157,7 @@ def upgrade() -> None: sa.Column("id", postgresql.UUID(as_uuid=False), nullable=False), sa.Column("plugin_id", sa.String(255), nullable=False), sa.Column("reviewer_id", postgresql.UUID(as_uuid=False), nullable=True), - sa.Column("decision", sa.Enum("approved", "rejected", name="review_decision"), nullable=False), + sa.Column("decision", sa.Enum("approved", "rejected", name="review_decision", create_type=False), nullable=False), sa.Column("notes", sa.Text, nullable=True), sa.Column("reviewed_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), sa.Column("created_at", sa.DateTime(timezone=True), nullable=False, server_default=sa.text("now()")), diff --git a/docker-compose.yml b/docker-compose.yml index 67bf99f..0d40152 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,9 +2,10 @@ services: app: build: . ports: - - "8000:8000" + - "8080:8000" env_file: - - .env + - path: .env + required: false environment: DATABASE_URL: postgresql+asyncpg://postgres:postgres@db:5432/adiuva depends_on: