Some checks failed
Basil CI/CD Pipeline / Shared Package Tests (push) Successful in 3m18s
Basil CI/CD Pipeline / Code Linting (push) Successful in 3m39s
Basil CI/CD Pipeline / Web Tests (push) Successful in 4m1s
Basil CI/CD Pipeline / API Tests (push) Failing after 4m7s
Basil CI/CD Pipeline / Trigger Deployment (push) Has been skipped
Basil CI/CD Pipeline / Security Scanning (push) Successful in 3m42s
Basil CI/CD Pipeline / Build All Packages (push) Has been skipped
Basil CI/CD Pipeline / E2E Tests (push) Has been skipped
Basil CI/CD Pipeline / Build & Push Docker Images (push) Has been skipped
Moves migration handling into the pipeline and production deploy so schema changes ship atomically with the code that depends on them. Previously migrations were manual and the migrations/ directory was gitignored, which caused silent drift between environments. - Track packages/api/prisma/migrations/ in git (including the baseline 20260416000000_init and the family-tenant delta). - Add `prisma:deploy` script that runs `prisma migrate deploy` (the non-interactive, CI-safe command). `prisma:migrate` still maps to `migrate dev` for local authoring. - Pipeline test-api and e2e-tests jobs now use `prisma:deploy` and test-api adds a drift check (`prisma migrate diff --exit-code`) that fails the build if schema.prisma has changes without a corresponding migration. - deploy.sh runs migrations against prod using `docker run --rm` with the freshly pulled API image before restarting containers, so a failing migration aborts the deploy with the old containers still serving traffic. The [skip-deploy] tag avoids re-triggering deployment for this infrastructure commit; the updated deploy.sh must be pulled to the production host out-of-band before the next deployment benefits from the new migration step. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
453 lines
15 KiB
YAML
453 lines
15 KiB
YAML
name: Basil CI/CD Pipeline
|
|
|
|
on:
|
|
push:
|
|
branches:
|
|
- main
|
|
- develop
|
|
tags:
|
|
- 'v*'
|
|
pull_request:
|
|
branches:
|
|
- main
|
|
- develop
|
|
|
|
env:
|
|
NODE_VERSION: '20'
|
|
HARBOR_REGISTRY: harbor.pkartchner.com
|
|
HARBOR_PROJECT: basil
|
|
|
|
jobs:
|
|
# ============================================================================
|
|
# STAGE 1: PARALLEL QUALITY CHECKS
|
|
# ============================================================================
|
|
|
|
lint:
|
|
name: Code Linting
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run linter
|
|
run: npm run lint || true
|
|
continue-on-error: true
|
|
|
|
test-api:
|
|
name: API Tests
|
|
runs-on: ubuntu-latest
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: basil
|
|
POSTGRES_PASSWORD: basil
|
|
POSTGRES_DB: basil_test
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
# Python setup temporarily disabled - scraper tests are skipped
|
|
# TODO: Re-enable when Python dependency setup works in Gitea runners
|
|
# - name: Set up Python
|
|
# uses: actions/setup-python@v6
|
|
# with:
|
|
# python-version: '3.11'
|
|
# cache: 'pip'
|
|
# - name: Install Python dependencies
|
|
# run: |
|
|
# pip install -r packages/api/requirements.txt
|
|
# python -c "import recipe_scrapers; print('✓ recipe-scrapers installed successfully')"
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build shared package
|
|
run: cd packages/shared && npm run build
|
|
|
|
- name: Generate Prisma Client
|
|
run: cd packages/api && npm run prisma:generate
|
|
|
|
- name: Apply database migrations
|
|
run: cd packages/api && npm run prisma:deploy
|
|
env:
|
|
DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public
|
|
|
|
- name: Check for schema drift
|
|
run: |
|
|
cd packages/api && npx prisma migrate diff \
|
|
--from-url "$DATABASE_URL" \
|
|
--to-schema-datamodel ./prisma/schema.prisma \
|
|
--exit-code && echo "✓ schema.prisma matches applied migrations"
|
|
env:
|
|
DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public
|
|
|
|
- name: Run API tests
|
|
run: cd packages/api && npm run test
|
|
env:
|
|
DATABASE_URL: postgresql://basil:basil@postgres:5432/basil_test?schema=public
|
|
NODE_ENV: test
|
|
|
|
- name: Upload coverage
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: api-coverage
|
|
path: packages/api/coverage/
|
|
retention-days: 14
|
|
|
|
test-web:
|
|
name: Web Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build shared package
|
|
run: cd packages/shared && npm run build
|
|
|
|
- name: Run Web tests
|
|
run: cd packages/web && npm run test
|
|
|
|
- name: Upload coverage
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: web-coverage
|
|
path: packages/web/coverage/
|
|
retention-days: 14
|
|
|
|
test-shared:
|
|
name: Shared Package Tests
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Run Shared tests
|
|
run: cd packages/shared && npm run test
|
|
|
|
- name: Upload coverage
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: shared-coverage
|
|
path: packages/shared/coverage/
|
|
retention-days: 14
|
|
|
|
security-scan:
|
|
name: Security Scanning
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: NPM Audit
|
|
run: |
|
|
echo "Running npm audit..."
|
|
npm audit --audit-level=high || true
|
|
cd packages/api && npm audit --audit-level=high || true
|
|
cd ../web && npm audit --audit-level=high || true
|
|
continue-on-error: true
|
|
|
|
- name: Secret Scanning
|
|
run: |
|
|
echo "Scanning for hardcoded secrets..."
|
|
if grep -r -E "(password|secret|api[_-]?key|token)\s*=\s*['\"][^'\"]+['\"]" \
|
|
--include="*.ts" --include="*.js" \
|
|
--exclude-dir=node_modules --exclude-dir=dist --exclude-dir=e2e \
|
|
--exclude="*.test.ts" --exclude="*.spec.ts" .; then
|
|
echo "⚠️ Potential hardcoded secrets found in non-test files!"
|
|
exit 1
|
|
fi
|
|
echo "✓ No hardcoded secrets detected"
|
|
|
|
- name: Check outdated dependencies
|
|
run: |
|
|
echo "Checking for outdated dependencies..."
|
|
npm outdated || true
|
|
continue-on-error: true
|
|
|
|
# ============================================================================
|
|
# STAGE 2: BUILD VERIFICATION
|
|
# ============================================================================
|
|
|
|
build:
|
|
name: Build All Packages
|
|
runs-on: ubuntu-latest
|
|
needs: [lint, test-api, test-web, test-shared, security-scan]
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Build all packages
|
|
run: npm run build
|
|
|
|
- name: Upload build artifacts
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: build-artifacts
|
|
path: |
|
|
packages/api/dist/
|
|
packages/web/dist/
|
|
packages/shared/dist/
|
|
retention-days: 7
|
|
|
|
# ============================================================================
|
|
# STAGE 3: E2E TESTING
|
|
# ============================================================================
|
|
|
|
e2e-tests:
|
|
name: E2E Tests
|
|
runs-on: ubuntu-latest
|
|
needs: build
|
|
timeout-minutes: 30
|
|
if: false # Temporarily disabled - E2E tests need fixing
|
|
services:
|
|
postgres:
|
|
image: postgres:16-alpine
|
|
env:
|
|
POSTGRES_USER: basil
|
|
POSTGRES_PASSWORD: basil
|
|
POSTGRES_DB: basil
|
|
options: >-
|
|
--health-cmd pg_isready
|
|
--health-interval 10s
|
|
--health-timeout 5s
|
|
--health-retries 5
|
|
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: ${{ env.NODE_VERSION }}
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Install Playwright browsers
|
|
run: npx playwright install --with-deps
|
|
|
|
- name: Build application
|
|
run: npm run build
|
|
|
|
- name: Apply database migrations
|
|
run: cd packages/api && npm run prisma:deploy
|
|
env:
|
|
DATABASE_URL: postgresql://basil:basil@postgres:5432/basil?schema=public
|
|
|
|
- name: Run E2E tests
|
|
run: npm run test:e2e
|
|
env:
|
|
DATABASE_URL: postgresql://basil:basil@postgres:5432/basil?schema=public
|
|
|
|
- name: Upload test results
|
|
if: always()
|
|
uses: actions/upload-artifact@v3
|
|
with:
|
|
name: playwright-report
|
|
path: playwright-report/
|
|
retention-days: 14
|
|
|
|
# ============================================================================
|
|
# STAGE 4: DOCKER BUILD & DEPLOYMENT (main branch only)
|
|
# ============================================================================
|
|
|
|
docker-build-and-push:
|
|
name: Build & Push Docker Images
|
|
runs-on: ubuntu-latest
|
|
needs: build # Changed from e2e-tests since E2E is temporarily disabled
|
|
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
|
outputs:
|
|
image_tag: ${{ steps.meta.outputs.tag }}
|
|
steps:
|
|
- name: Checkout code
|
|
uses: actions/checkout@v4
|
|
|
|
- name: Install Docker CLI
|
|
run: |
|
|
mkdir -p /tmp/docker-cli
|
|
curl -fsSL "https://download.docker.com/linux/static/stable/x86_64/docker-27.4.1.tgz" | tar -xz -C /tmp/docker-cli --strip-components=1
|
|
export PATH="/tmp/docker-cli:$PATH"
|
|
echo "/tmp/docker-cli" >> $GITHUB_PATH
|
|
docker --version
|
|
|
|
- name: Set up Docker Buildx
|
|
uses: docker/setup-buildx-action@v3
|
|
|
|
- name: Log in to Harbor
|
|
uses: docker/login-action@v3
|
|
with:
|
|
registry: ${{ env.HARBOR_REGISTRY }}
|
|
username: ${{ secrets.HARBOR_USERNAME }}
|
|
password: ${{ secrets.HARBOR_PASSWORD }}
|
|
|
|
- name: Extract metadata
|
|
id: meta
|
|
run: |
|
|
SHA_SHORT=$(echo $GITHUB_SHA | cut -c1-7)
|
|
echo "tag=main-${SHA_SHORT}" >> $GITHUB_OUTPUT
|
|
echo "date=$(date -u +'%Y-%m-%dT%H:%M:%SZ')" >> $GITHUB_OUTPUT
|
|
|
|
- name: Build and push API image
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: ./packages/api/Dockerfile
|
|
push: true
|
|
tags: |
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:${{ steps.meta.outputs.tag }}
|
|
labels: |
|
|
org.opencontainers.image.created=${{ steps.meta.outputs.date }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
|
|
|
- name: Build and push Web image
|
|
uses: docker/build-push-action@v5
|
|
with:
|
|
context: .
|
|
file: ./packages/web/Dockerfile
|
|
push: true
|
|
tags: |
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:${{ steps.meta.outputs.tag }}
|
|
labels: |
|
|
org.opencontainers.image.created=${{ steps.meta.outputs.date }}
|
|
org.opencontainers.image.revision=${{ github.sha }}
|
|
org.opencontainers.image.source=${{ github.server_url }}/${{ github.repository }}
|
|
|
|
- name: Scan API image for vulnerabilities
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
aquasec/trivy:latest image \
|
|
--exit-code 0 \
|
|
--severity HIGH,CRITICAL \
|
|
--no-progress \
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest || true
|
|
|
|
- name: Scan Web image for vulnerabilities
|
|
run: |
|
|
docker run --rm \
|
|
-v /var/run/docker.sock:/var/run/docker.sock \
|
|
aquasec/trivy:latest image \
|
|
--exit-code 0 \
|
|
--severity HIGH,CRITICAL \
|
|
--no-progress \
|
|
${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest || true
|
|
|
|
- name: Image build summary
|
|
run: |
|
|
echo "### Docker Images Built & Pushed 🐳" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**API Image:**" >> $GITHUB_STEP_SUMMARY
|
|
echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:latest\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-api:${{ steps.meta.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Web Image:**" >> $GITHUB_STEP_SUMMARY
|
|
echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:latest\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "- \`${{ env.HARBOR_REGISTRY }}/${{ env.HARBOR_PROJECT }}/basil-web:${{ steps.meta.outputs.tag }}\`" >> $GITHUB_STEP_SUMMARY
|
|
|
|
trigger-deployment:
|
|
name: Trigger Deployment
|
|
runs-on: ubuntu-latest
|
|
needs: docker-build-and-push
|
|
# Skip deployment if commit message contains [skip-deploy] or [dev]
|
|
if: success() && !contains(github.event.head_commit.message, '[skip-deploy]') && !contains(github.event.head_commit.message, '[dev]')
|
|
steps:
|
|
- name: Trigger webhook
|
|
run: |
|
|
# Install required packages
|
|
apt-get update -qq && apt-get install -y -qq jq iproute2 > /dev/null 2>&1
|
|
|
|
# Get Docker host IP (gateway of the container network)
|
|
HOST_IP=$(ip route | grep default | awk '{print $3}')
|
|
|
|
# Fallback to common Docker gateway IPs if not found
|
|
if [ -z "$HOST_IP" ]; then
|
|
HOST_IP="172.17.0.1"
|
|
fi
|
|
|
|
echo "Using host IP: $HOST_IP"
|
|
|
|
# Construct JSON payload with jq to properly escape multiline commit messages
|
|
PAYLOAD=$(jq -n \
|
|
--arg branch "main" \
|
|
--arg commit "${{ github.sha }}" \
|
|
--arg message "${{ github.event.head_commit.message }}" \
|
|
--arg tag "${{ needs.docker-build-and-push.outputs.image_tag }}" \
|
|
'{branch: $branch, commit: $commit, message: $message, tag: $tag}')
|
|
|
|
# Trigger webhook on Docker host
|
|
curl -X POST "http://${HOST_IP}:9000/hooks/basil-deploy" \
|
|
-H "Content-Type: application/json" \
|
|
-H "X-Webhook-Secret: ${{ secrets.WEBHOOK_SECRET }}" \
|
|
-d "$PAYLOAD" || echo "Webhook call failed, but continuing..."
|
|
|
|
- name: Deployment triggered
|
|
run: |
|
|
echo "### Deployment Triggered 🚀" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "The deployment webhook has been called." >> $GITHUB_STEP_SUMMARY
|
|
echo "Check the server logs to verify deployment status:" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`bash" >> $GITHUB_STEP_SUMMARY
|
|
echo "tail -f /srv/docker-compose/basil/deploy.log" >> $GITHUB_STEP_SUMMARY
|
|
echo "\`\`\`" >> $GITHUB_STEP_SUMMARY
|
|
echo "" >> $GITHUB_STEP_SUMMARY
|
|
echo "**Application URL:** https://basil.pkartchner.com" >> $GITHUB_STEP_SUMMARY
|