# FILE: .gitea/workflows/ci.yml # Lint → Test → Build → Scan → Push → Deploy (compose up) name: CI/CD Pipeline on: push: branches: [main, develop] pull_request: branches: [main] release: types: [published] env: REGISTRY: registry.local IMAGE_PREFIX: ai-tax-agent jobs: lint: name: Code Quality & Linting runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v4 with: python-version: "3.12" - name: Set up Node.js 20 uses: actions/setup-node@v4 with: node-version: "20" - name: Install Python dependencies run: | python -m pip install --upgrade pip pip install ruff mypy safety bandit find apps -name requirements.txt -exec pip install -r {} \; - name: Install Node.js dependencies run: | find apps -name package.json -execdir npm install \; - name: Python linting with ruff run: | ruff check apps/ ruff format --check apps/ - name: Python type checking with mypy run: | find apps -name "*.py" -path "*/svc-*/*" -exec mypy {} \; - name: TypeScript linting run: | find apps -name "*.ts" -o -name "*.tsx" -execdir npx eslint {} \; || true - name: YAML linting run: | pip install yamllint yamllint -d relaxed . - name: Docker linting run: | docker run --rm -i hadolint/hadolint < apps/svc-extract/Dockerfile || true - name: Security linting run: | bandit -r apps/ -f json -o bandit-report.json || true safety check --json --output safety-report.json || true - name: Upload lint reports uses: actions/upload-artifact@v3 with: name: lint-reports path: | bandit-report.json safety-report.json policy-validate: name: Policy Validation runs-on: ubuntu-latest needs: lint services: neo4j: image: neo4j:5.15-community env: NEO4J_AUTH: neo4j/testpass ports: - 7687:7687 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip pip install yamllint jsonschema pyyaml pip install -r libs/requirements.txt - name: YAML lint coverage policy run: | yamllint config/coverage.yaml - name: Validate policy schema run: | python -c " import yaml import json from jsonschema import validate # Load policy with open('config/coverage.yaml', 'r') as f: policy = yaml.safe_load(f) # Load schema with open('libs/coverage_schema.json', 'r') as f: schema = json.load(f) # Validate validate(instance=policy, schema=schema) print('✅ Policy schema validation passed') " - name: Validate box references (mock) run: | python -c " import yaml # Load policy with open('config/coverage.yaml', 'r') as f: policy = yaml.safe_load(f) # Extract all box references boxes = set() for schedule in policy.get('schedules', {}).values(): for evidence in schedule.get('evidence', []): boxes.update(evidence.get('boxes', [])) print(f'Found {len(boxes)} unique box references') # Mock validation - in production this would check against KG invalid_boxes = [box for box in boxes if not box.startswith('SA')] if invalid_boxes: print(f'❌ Invalid box format: {invalid_boxes}') exit(1) else: print('✅ Box format validation passed') " test: name: Test Suite runs-on: ubuntu-latest needs: lint services: postgres: image: postgres:15-alpine env: POSTGRES_PASSWORD: postgres POSTGRES_DB: test_db options: >- --health-cmd pg_isready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 5432:5432 redis: image: redis:7-alpine options: >- --health-cmd "redis-cli ping" --health-interval 10s --health-timeout 5s --health-retries 5 ports: - 6379:6379 neo4j: image: neo4j:5.15-community env: NEO4J_AUTH: neo4j/testpass ports: - 7687:7687 steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Python 3.12 uses: actions/setup-python@v4 with: python-version: "3.12" - name: Install dependencies run: | python -m pip install --upgrade pip pip install pytest pytest-cov pytest-asyncio find apps -name requirements.txt -exec pip install -r {} \; - name: Run unit tests env: POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/test_db REDIS_URL: redis://localhost:6379 NEO4J_URI: bolt://localhost:7687 NEO4J_USER: neo4j NEO4J_PASSWORD: testpass run: | pytest apps/ -v --cov=apps --cov-report=xml --cov-report=html - name: Run integration tests env: POSTGRES_URL: postgresql://postgres:postgres@localhost:5432/test_db REDIS_URL: redis://localhost:6379 NEO4J_URI: bolt://localhost:7687 NEO4J_USER: neo4j NEO4J_PASSWORD: testpass run: | pytest tests/integration/ -v - name: Upload coverage reports uses: actions/upload-artifact@v3 with: name: coverage-reports path: | coverage.xml htmlcov/ build: name: Build Docker Images runs-on: ubuntu-latest needs: [test, policy-validate] strategy: matrix: service: - svc-ingestion - svc-rpa - svc-ocr - svc-extract - svc-normalize-map - svc-kg - svc-rag-indexer - svc-rag-retriever - svc-reason - svc-forms - svc-hmrc - svc-firm-connectors - svc-coverage - ui-review steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Log in to registry uses: docker/login-action@v3 with: registry: ${{ env.REGISTRY }} username: ${{ secrets.REGISTRY_USERNAME }} password: ${{ secrets.REGISTRY_PASSWORD }} - name: Extract metadata id: meta uses: docker/metadata-action@v5 with: images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }} tags: | type=ref,event=branch type=ref,event=pr type=semver,pattern={{version}} type=semver,pattern={{major}}.{{minor}} type=sha,prefix={{branch}}- - name: Build and push uses: docker/build-push-action@v5 with: context: apps/${{ matrix.service }} platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max security-scan: name: Security Scanning runs-on: ubuntu-latest needs: build strategy: matrix: service: - svc-extract - svc-kg - svc-rag-retriever - svc-coverage - ui-review steps: - name: Checkout code uses: actions/checkout@v4 - name: Run Trivy vulnerability scanner uses: aquasecurity/trivy-action@master with: image-ref: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }} format: "sarif" output: "trivy-results-${{ matrix.service }}.sarif" - name: Upload Trivy scan results uses: actions/upload-artifact@v3 with: name: trivy-results-${{ matrix.service }} path: trivy-results-${{ matrix.service }}.sarif - name: Run Snyk security scan uses: snyk/actions/docker@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: image: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/${{ matrix.service }}:${{ github.sha }} args: --severity-threshold=high continue-on-error: true sbom: name: Generate SBOM runs-on: ubuntu-latest needs: build steps: - name: Checkout code uses: actions/checkout@v4 - name: Install Syft run: | curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin - name: Generate SBOM for key services run: | syft ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/svc-extract:${{ github.sha }} -o spdx-json=sbom-svc-extract.json syft ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/svc-kg:${{ github.sha }} -o spdx-json=sbom-svc-kg.json syft ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}/ui-review:${{ github.sha }} -o spdx-json=sbom-ui-review.json - name: Upload SBOM artifacts uses: actions/upload-artifact@v3 with: name: sbom-reports path: sbom-*.json deploy-staging: name: Deploy to Staging runs-on: ubuntu-latest needs: [security-scan, sbom] if: github.ref == 'refs/heads/develop' environment: staging steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up Docker Compose run: | sudo curl -L "https://github.com/docker/compose/releases/download/v2.21.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose sudo chmod +x /usr/local/bin/docker-compose - name: Deploy to staging env: DOCKER_HOST: ${{ secrets.STAGING_DOCKER_HOST }} DOCKER_CERT_PATH: ${{ secrets.STAGING_DOCKER_CERT_PATH }} DOCKER_TLS_VERIFY: 1 run: | cd infra/compose cp env.example .env sed -i 's/local/staging.local/g' .env docker-compose -f docker-compose.local.yml pull docker-compose -f docker-compose.local.yml up -d - name: Run smoke tests run: | sleep 60 # Wait for services to start curl -f https://api.staging.local/health || exit 1 curl -f https://review.staging.local || exit 1 deploy-production: name: Deploy to Production runs-on: ubuntu-latest needs: [security-scan, sbom] if: github.event_name == 'release' environment: production steps: - name: Checkout code uses: actions/checkout@v4 - name: Deploy to production env: KUBECONFIG: ${{ secrets.KUBECONFIG }} run: | echo "🚀 Production deployment would happen here" echo "📝 TODO: Implement Kubernetes deployment with ArgoCD" echo "🏷️ Release tag: ${{ github.event.release.tag_name }}" notify: name: Notifications runs-on: ubuntu-latest needs: [deploy-staging, deploy-production] if: always() steps: - name: Notify on success if: ${{ needs.deploy-staging.result == 'success' || needs.deploy-production.result == 'success' }} run: | echo "✅ Deployment successful!" # Add Slack/Teams notification here - name: Notify on failure if: ${{ needs.deploy-staging.result == 'failure' || needs.deploy-production.result == 'failure' }} run: | echo "❌ Deployment failed!" # Add Slack/Teams notification here