From 5a364ce638e3a1fa18c56b3232ff4659de53d220 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Sun, 31 May 2026 10:49:26 +0100 Subject: [PATCH] ci: replace Forgejo deploy workflow with container build pipeline --- .forgejo/workflows/ci.yml | 161 ++++++++++++++++++++++++---------- .forgejo/workflows/deploy.yml | 52 ----------- .forgejo/workflows/docker.yml | 50 +++++++++++ Dockerfile | 3 +- 4 files changed, 169 insertions(+), 97 deletions(-) delete mode 100644 .forgejo/workflows/deploy.yml create mode 100644 .forgejo/workflows/docker.yml diff --git a/.forgejo/workflows/ci.yml b/.forgejo/workflows/ci.yml index 9c0d13b..21d230e 100644 --- a/.forgejo/workflows/ci.yml +++ b/.forgejo/workflows/ci.yml @@ -8,77 +8,150 @@ on: jobs: backend: + name: Backend tests runs-on: ubuntu-latest + defaults: + run: + working-directory: backend steps: - uses: actions/checkout@v4 - name: Install uv - run: | - curl -LsSf https://astral.sh/uv/install.sh | sh - echo "$HOME/.local/bin" >> $GITHUB_PATH + uses: astral-sh/setup-uv@v4 + with: + version: latest + + - name: Set up Python + run: uv python install 3.13 - name: Sync dependencies - working-directory: backend run: uv sync - - name: Run tests - working-directory: backend + - name: Run pytest run: uv run pytest -v - frontend: + frontend-lint: + name: Frontend lint runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: '22' - cache: 'npm' + cache: npm cache-dependency-path: frontend/package-lock.json - name: Install dependencies - working-directory: frontend run: npm ci - - name: Lint (if configured) - working-directory: frontend - run: | - if [ -f eslint.config.js ] || [ -f .eslintrc.json ] || [ -f .eslintrc.js ]; then - npm run lint - else - echo "No ESLint config found, skipping lint" - fi + - name: Run ESLint + run: npm run lint - - name: Build - working-directory: frontend - run: npm run build - - - name: Test - working-directory: frontend - run: npm test - - docker: + frontend-test: + name: Frontend unit tests runs-on: ubuntu-latest - needs: [backend, frontend] + defaults: + run: + working-directory: frontend steps: - uses: actions/checkout@v4 - - name: Build Docker image - run: docker build -t life-towers:${{ github.sha }} . + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: frontend/package-lock.json - - name: Push to registry - if: github.event_name == 'push' && github.ref == 'refs/heads/master' - env: - REGISTRY_URL: ${{ secrets.REGISTRY_URL }} - REGISTRY_USER: ${{ secrets.REGISTRY_USER }} - REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }} + - name: Install dependencies + run: npm ci + + - name: Run Vitest + run: npm test + + frontend-build: + name: Frontend build + runs-on: ubuntu-latest + defaults: + run: + working-directory: frontend + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: npm + cache-dependency-path: frontend/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Build production bundle + run: npm run build + + e2e: + name: Playwright e2e + runs-on: ubuntu-latest + needs: [backend, frontend-build] + steps: + - uses: actions/checkout@v4 + + - name: Start stack + run: docker compose -p life-towers -f docker-compose.dev.yml up --build -d + + - name: Wait for /api/v1/health run: | - if [ -z "$REGISTRY_URL" ] || [ -z "$REGISTRY_USER" ] || [ -z "$REGISTRY_PASSWORD" ]; then - echo "Registry secrets not configured, skipping push" - exit 0 - fi - echo "$REGISTRY_PASSWORD" | docker login "$REGISTRY_URL" -u "$REGISTRY_USER" --password-stdin - docker tag life-towers:${{ github.sha }} "$REGISTRY_URL/life-towers:${{ github.sha }}" - docker tag life-towers:${{ github.sha }} "$REGISTRY_URL/life-towers:latest" - docker push "$REGISTRY_URL/life-towers:${{ github.sha }}" - docker push "$REGISTRY_URL/life-towers:latest" + set -e + cid=$(docker compose -p life-towers -f docker-compose.dev.yml ps -q life-towers) + for i in $(seq 1 60); do + status=$(docker inspect -f '{{.State.Health.Status}}' "$cid" 2>/dev/null || echo starting) + if [ "$status" = healthy ]; then + echo "stack healthy after ${i} attempts" + exit 0 + fi + sleep 2 + done + echo "stack failed to become healthy" >&2 + docker compose -p life-towers -f docker-compose.dev.yml logs >&2 + exit 1 + + - name: Run Playwright + run: | + docker run --rm \ + --network life-towers_default \ + -v "$(pwd)/frontend:/work" \ + -w /work \ + -e PLAYWRIGHT_BASE_URL=http://life-towers:8000 \ + -e CI=true \ + mcr.microsoft.com/playwright:v1.60.0-noble \ + sh -c 'npm ci && npx playwright test' + + - name: Upload Playwright report + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-report + path: frontend/playwright-report + if-no-files-found: ignore + retention-days: 14 + + - name: Upload visual screenshots + if: always() + uses: actions/upload-artifact@v4 + with: + name: playwright-visuals + path: frontend/visuals + if-no-files-found: ignore + retention-days: 14 + + - name: Dump container logs on failure + if: failure() + run: docker compose -p life-towers -f docker-compose.dev.yml logs + + - name: Tear down stack + if: always() + run: docker compose -p life-towers -f docker-compose.dev.yml down -v diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml deleted file mode 100644 index 598350d..0000000 --- a/.forgejo/workflows/deploy.yml +++ /dev/null @@ -1,52 +0,0 @@ -# IMPORTANT: Before this workflow will function, configure the following -# repository secrets in Forgejo (Settings → Secrets): -# DEPLOY_HOST — hostname or IP of the target server -# DEPLOY_USER — SSH user on the target server -# DEPLOY_SSH_KEY — private SSH key (PEM or OpenSSH format) -# DEPLOY_PATH — absolute path to the project directory on the server -# (must contain a docker-compose.yml + a .env file -# that sets LIFE_TOWERS_IMAGE to the registry tag, -# e.g. LIFE_TOWERS_IMAGE=registry.example.com/life-towers:latest) - -name: Deploy - -on: - workflow_dispatch: - push: - tags: - - 'v*' - -jobs: - deploy: - runs-on: ubuntu-latest - steps: - - name: Install SSH key - run: | - mkdir -p ~/.ssh - chmod 700 ~/.ssh - printf '%s\n' "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - ssh-keyscan -H "${{ secrets.DEPLOY_HOST }}" >> ~/.ssh/known_hosts - chmod 644 ~/.ssh/known_hosts - - - name: Deploy via SSH - run: | - set -euo pipefail - # Pulls the new image referenced by $LIFE_TOWERS_IMAGE in the - # server's .env, restarts the service, then verifies health. - ssh -i ~/.ssh/deploy_key \ - -o StrictHostKeyChecking=yes \ - "${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" \ - "set -euo pipefail - cd '${{ secrets.DEPLOY_PATH }}' - docker compose pull - docker compose up -d --remove-orphans - # Wait for healthcheck (max ~60s) - for i in \$(seq 1 30); do - status=\$(docker compose ps --format json life-towers | python3 -c 'import sys,json;[print(json.loads(l).get(\"Health\",\"\")) for l in sys.stdin]' || true) - if [ \"\$status\" = healthy ]; then echo deploy_healthy; exit 0; fi - sleep 2 - done - echo deploy_unhealthy >&2 - docker compose logs --tail 50 life-towers >&2 - exit 1" diff --git a/.forgejo/workflows/docker.yml b/.forgejo/workflows/docker.yml new file mode 100644 index 0000000..8e4f6d7 --- /dev/null +++ b/.forgejo/workflows/docker.yml @@ -0,0 +1,50 @@ +name: Docker + +on: + push: + branches: [master] + workflow_dispatch: + +env: + REGISTRY: ${{ vars.REGISTRY || 'ghcr.io' }} + IMAGE_NAME: ${{ github.repository }} + +jobs: + build-and-push: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Log in to container registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=latest + type=sha,prefix=sha-,format=short + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile index aad08e4..82fba50 100644 --- a/Dockerfile +++ b/Dockerfile @@ -44,6 +44,7 @@ USER appuser ENV LIFE_TOWERS_DB_PATH=/data/life-towers.db \ LIFE_TOWERS_STATIC_DIR=/app/static \ + LIFE_TOWERS_FORWARDED_ALLOW_IPS=* \ PYTHONUNBUFFERED=1 \ PYTHONPATH=/app/src \ PATH=/app/.venv/bin:$PATH @@ -53,4 +54,4 @@ EXPOSE 8000 HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ CMD curl -fsS http://localhost:8000/api/v1/health || exit 1 -CMD ["uvicorn", "life_towers.main:app", "--host", "0.0.0.0", "--port", "8000", "--proxy-headers", "--forwarded-allow-ips=*"] +CMD ["sh", "-c", "uvicorn life_towers.main:app --host 0.0.0.0 --port 8000 --proxy-headers --forwarded-allow-ips=${LIFE_TOWERS_FORWARDED_ALLOW_IPS}"]