Files
basil/.gitea/workflows/main.yml
Paul R Kartchner 91146e1219
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
ci: automate Prisma migrations in pipeline and deploy [skip-deploy]
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>
2026-04-17 08:16:51 -06:00

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