diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5c61bb7..1fc54c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,9 +2,9 @@ name: CI on: push: - branches: [main] + branches: [main, dev] pull_request: - branches: [main] + branches: [main, dev] concurrency: group: ci-${{ github.ref }} @@ -44,8 +44,6 @@ jobs: cache: pip - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libpq-dev build-essential - - name: Install cartsnitch-common from GitHub - run: pip install "cartsnitch-common @ git+https://github.com/cartsnitch/common.git" - run: pip install -e ".[dev]" mypy - name: Type check run: mypy src/cartsnitch_api @@ -93,8 +91,6 @@ jobs: cache: pip - name: Install system dependencies run: sudo apt-get update && sudo apt-get install -y libpq-dev build-essential - - name: Install cartsnitch-common from GitHub - run: pip install "cartsnitch-common @ git+https://github.com/cartsnitch/common.git" - run: pip install -e ".[dev]" - name: Run tests run: pytest --tb=short -q @@ -102,6 +98,9 @@ jobs: build-and-push: runs-on: runners-cartsnitch needs: [lint, test] + outputs: + calver_tag: ${{ steps.calver.outputs.version }} + sha_tag: sha-${{ github.sha }} steps: - uses: actions/checkout@v4 with: @@ -144,21 +143,162 @@ jobs: with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} tags: | - type=sha,prefix=sha- + type=sha,prefix=sha-,format=long type=raw,value=${{ steps.calver.outputs.version }},enable=${{ github.ref == 'refs/heads/main' }} type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }} - - name: Build and push Docker image + - name: Build Docker image uses: docker/build-push-action@v6 with: context: . - push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + file: ./Dockerfile + load: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - target: prod + build-args: | + APT_CACHE_BUST=${{ github.run_id }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Scan api image for vulnerabilities + uses: anchore/scan-action@v5 + id: scan + env: + GRYPE_CONFIG: .grype.yaml + with: + image: "${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:sha-${{ github.sha }}" + fail-build: true + severity-cutoff: high + only-fixed: "true" + output-format: sarif + + - name: Upload api scan results to GitHub Security + uses: github/codeql-action/upload-sarif@v3 + if: always() + with: + sarif_file: ${{ steps.scan.outputs.sarif }} + + - name: Push Docker image + if: github.event_name == 'push' + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + APT_CACHE_BUST=${{ github.run_id }} + cache-from: type=gha - name: Create git tag if: github.event_name == 'push' && github.ref == 'refs/heads/main' run: | git tag "v${{ steps.calver.outputs.version }}" - git push origin "v${{ steps.calver.outputs.version }}" \ No newline at end of file + git push origin "v${{ steps.calver.outputs.version }}" + + deploy-dev: + runs-on: runners-cartsnitch + needs: [build-and-push] + if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main') + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CARTSNITCH_APP_ID }} + private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: infra + + - name: Checkout infra repo + uses: actions/checkout@v4 + with: + repository: cartsnitch/infra + token: ${{ steps.app-token.outputs.token }} + ref: main + path: infra + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + + - name: Install kustomize + uses: imranismail/setup-kustomize@v2 + + - name: Determine image tag + id: api_tag + run: | + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + echo "tag=${{ needs.build-and-push.outputs.calver_tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ needs.build-and-push.outputs.sha_tag }}" >> "$GITHUB_OUTPUT" + fi + + - name: Update api image tag + if: needs.build-and-push.result == 'success' + run: | + cd infra/apps/overlays/dev + kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }} + + - name: Commit and push to infra + run: | + cd infra + git config user.name "cartsnitch-ci[bot]" + git config user.email "cartsnitch-ci[bot]@users.noreply.github.com" + git add apps/overlays/dev/kustomization.yaml + git commit -m "ci(dev): update api image" + git pull --rebase origin main + git push origin main + + deploy-uat: + runs-on: runners-cartsnitch + needs: [build-and-push] + if: always() && !cancelled() && github.event_name == 'push' && (github.ref == 'refs/heads/uat' || github.ref == 'refs/heads/main') + steps: + - name: Generate GitHub App token + id: app-token + uses: actions/create-github-app-token@v1 + with: + app-id: ${{ secrets.CARTSNITCH_APP_ID }} + private-key: ${{ secrets.CARTSNITCH_APP_PRIVATE_KEY }} + owner: ${{ github.repository_owner }} + repositories: infra + + - name: Checkout infra repo + uses: actions/checkout@v4 + with: + repository: cartsnitch/infra + token: ${{ steps.app-token.outputs.token }} + ref: main + path: infra + + - name: Install kubectl + uses: azure/setup-kubectl@v4 + + - name: Install kustomize + uses: imranismail/setup-kustomize@v2 + + - name: Determine image tag + id: api_tag + run: | + if [ "${{ github.ref }}" == "refs/heads/main" ]; then + echo "tag=${{ needs.build-and-push.outputs.calver_tag }}" >> "$GITHUB_OUTPUT" + else + echo "tag=${{ needs.build-and-push.outputs.sha_tag }}" >> "$GITHUB_OUTPUT" + fi + + - name: Update api image tag + if: needs.build-and-push.result == 'success' + run: | + cd infra/apps/overlays/uat + kustomize edit set image ghcr.io/cartsnitch/api:${{ steps.api_tag.outputs.tag }} + + - name: Commit and push to infra + run: | + cd infra + git config user.name "cartsnitch-ci[bot]" + git config user.email "cartsnitch-ci[bot]@users.noreply.github.com" + git add apps/overlays/uat/kustomization.yaml + git commit -m "ci(uat): update api image" + git pull --rebase origin main + git push origin main \ No newline at end of file diff --git a/.grype.yaml b/.grype.yaml new file mode 100644 index 0000000..001d21a --- /dev/null +++ b/.grype.yaml @@ -0,0 +1,4 @@ +ignore: + # Python 3.12 CVEs — only fixed in 3.13+, cannot upgrade major version safely + - vulnerability: CVE-2025-13836 + - vulnerability: CVE-2026-4519 \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md index fcba89c..dbd3c02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -23,7 +23,7 @@ CartSnitch is a self-hosted grocery price intelligence platform built as a polyr ### Architecture Decisions - **Polyrepo:** Each service has its own repo, Dockerfile, CI/CD pipeline. -- **Shared DB:** One PostgreSQL cluster. This service reads from all tables for serving frontend queries. Models come from `cartsnitch-common`. +- **Shared DB:** One PostgreSQL cluster. This service reads from all tables for serving frontend queries. The API has its own local SQLAlchemy models — it does NOT import from `cartsnitch-common`. - **Inter-service comms:** REST to internal services, Redis pub/sub for event subscriptions. - **Target scale:** 500–1,000 users initially. @@ -42,7 +42,7 @@ The API Gateway is the single entry point for the frontend PWA and any external - Python 3.12+ - FastAPI (async) -- SQLAlchemy 2.0 (via `cartsnitch-common`, read-heavy) +- SQLAlchemy 2.0 (async, read-heavy) - Pydantic v2 (request/response validation) - python-jose or PyJWT (JWT auth) - passlib + bcrypt (password hashing)