Skip to content

Complete CI/CD Pipeline #230

Complete CI/CD Pipeline

Complete CI/CD Pipeline #230

name: Complete CI/CD Pipeline
# Comprehensive workflow combining code quality, security, Docker build/publish, and release management
# Optimized to eliminate duplicate builds and utilize best practices from all source workflows
on:
push:
branches: [main, develop]
tags: ['v*.*.*']
pull_request:
branches: [main, develop]
schedule:
# Daily build at 4:22 AM UTC for security updates
- cron: '22 4 * * *'
workflow_dispatch:
# Default permissions for all jobs
permissions:
contents: read
env:
NODE_VERSION: '20.18'
REGISTRY: ghcr.io
# Fix case sensitivity issue: convert repository name to lowercase
IMAGE_NAME: ${{ github.repository_owner }}/subpilot-app
jobs:
# Job 1: Code Quality, Build & Test
build-and-test:
name: Build, Test & Quality Checks
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
outputs:
build-success: ${{ steps.build-status.outputs.success }}
package-version: ${{ steps.package-version.outputs.version }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Extract version from package.json
id: package-version
run: |
VERSION=$(node -p "require('./package.json').version")
echo "version=$VERSION" >> $GITHUB_OUTPUT
echo "📦 Package version: $VERSION"
- name: Install dependencies
run: npm ci
- name: Validate project configuration
run: |
echo "🔍 Validating project configuration..."
test -f tsconfig.json || { echo "❌ tsconfig.json missing"; exit 1; }
test -f next.config.js || { echo "❌ next.config.js missing"; exit 1; }
test -f package.json || { echo "❌ package.json missing"; exit 1; }
test -f Dockerfile || { echo "❌ Dockerfile missing"; exit 1; }
echo "✅ Project configuration valid"
- name: Run security audit
run: |
echo "🔒 Running npm security audit..."
if npm audit --audit-level=moderate; then
echo "✅ No security vulnerabilities found"
else
echo "⚠️ Security vulnerabilities detected"
npm audit --audit-level=moderate --json | jq '.metadata.vulnerabilities' || true
echo "ℹ️ Consider running 'npm audit fix' to address fixable issues"
fi
continue-on-error: true
- name: Run filesystem security scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-fs-results.sarif'
- name: Upload filesystem scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-fs-results.sarif'
- name: Run code quality checks
env:
SKIP_ENV_VALIDATION: true
NEXTAUTH_SECRET: 'ci-test-secret'
NEXTAUTH_URL: 'http://localhost:3000'
run: |
echo "🔍 Running code quality checks..."
# TypeScript check
if npm run type-check; then
echo "✅ TypeScript compilation successful"
else
echo "❌ TypeScript compilation failed"
exit 1
fi
# Linting (non-blocking for development)
if npm run lint; then
echo "✅ No linting errors"
else
echo "⚠️ Linting issues found (continuing in development mode)"
echo "🔧 Run 'npm run lint:fix' to auto-fix issues"
fi
# Formatting check (non-blocking for development)
if npm run format:check; then
echo "✅ Code formatting is correct"
else
echo "⚠️ Formatting issues found (continuing in development mode)"
echo "🔧 Run 'npm run format' to fix formatting"
fi
continue-on-error: ${{ github.event_name != 'push' || github.ref != 'refs/heads/main' }}
- name: Restore Next.js cache
uses: actions/cache@v4
with:
path: |
${{ github.workspace }}/.next/cache
key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
restore-keys: |
${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
- name: Build application
run: |
echo "🏗️ Building Next.js application..."
export SKIP_ENV_VALIDATION=true
export NODE_ENV=production
export NEXTAUTH_SECRET="ci-test-secret"
export NEXTAUTH_URL="http://localhost:3000"
npm run build:ci
echo "✅ Build completed successfully"
- name: Create build artifacts
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo "📦 Creating build artifacts..."
mkdir -p artifacts
# Create production build archive
tar -czf artifacts/subpilot-${{ github.ref_name }}-build.tar.gz \
.next public package.json package-lock.json prisma next.config.js tsconfig.json
# Create source code archive
git archive --format=tar.gz --prefix=subpilot-${{ github.ref_name }}/ \
-o artifacts/subpilot-${{ github.ref_name }}-source.tar.gz HEAD
# Create checksums
cd artifacts && sha256sum *.tar.gz > checksums.sha256
echo "✅ Artifacts created successfully"
- name: Upload build artifacts
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: release-artifacts
path: artifacts/
retention-days: 30
- name: Set build status
id: build-status
run: echo "success=true" >> $GITHUB_OUTPUT
# Job 2: Docker Build, Test & Publish
docker-build-publish:
name: Docker Build & Publish
runs-on: ubuntu-latest
needs: [build-and-test]
if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) || github.event_name == 'schedule'
permissions:
contents: read
packages: write
security-events: write
id-token: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Install cosign for image signing
if: github.event_name != 'pull_request'
uses: sigstore/cosign-installer@59acb6260d9c0ba8f4a2f9d9b48431a222b68e20 #v3.5.0
with:
cosign-release: 'v2.2.4'
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@f95db51fddba0c2d1ec667646a06c2ce06100226 # v3.0.0
with:
platforms: linux/amd64,linux/arm64
- name: Log into GitHub Container Registry
uses: docker/login-action@343f7c4344506bcbf9b4de18042ae17996df046d # v3.0.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@96383f45573cb7f253c731d3b3ab81c87ef81934 # v5.0.0
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=sha,enable=${{ github.event_name != 'release' }}
type=raw,value=latest,enable={{is_default_branch}}
labels: |
org.opencontainers.image.title=SubPilot
org.opencontainers.image.description=Your command center for recurring finances
org.opencontainers.image.vendor=SubPilot
org.opencontainers.image.licenses=MIT
maintainer=SubPilot Team
org.opencontainers.image.documentation=https://github.com/doublegate/SubPilot-App
org.opencontainers.image.source=https://github.com/doublegate/SubPilot-App
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@0565240e2d4ab88bba5387d719585280857ece09 # v5.0.0
with:
context: .
file: ./Dockerfile
# Only build multi-platform for releases, single platform for regular pushes
platforms: ${{ startsWith(github.ref, 'refs/tags/v') && 'linux/amd64,linux/arm64' || 'linux/amd64' }}
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
build-args: |
SKIP_ENV_VALIDATION=true
NODE_ENV=production
DATABASE_URL=postgresql://placeholder:password@localhost:5432/placeholder
NEXTAUTH_URL=http://localhost:3000
BUILD_AUTH_TOKEN=placeholder-token-for-build
cache-from: type=gha
cache-to: type=gha,mode=max
provenance: true
sbom: true
- name: Test Docker image functionality
run: |
echo "🧪 Testing Docker image functionality..."
# Extract the first tag from the metadata output for testing
FIRST_TAG=$(echo "${{ steps.meta.outputs.tags }}" | head -n 1)
echo "Testing with tag: $FIRST_TAG"
# Pull the built image using the first available tag from the build
docker pull "$FIRST_TAG"
# Run container with unique name to avoid conflicts
CONTAINER_NAME="subpilot-test-${{ github.run_id }}"
docker run -d \
--name "$CONTAINER_NAME" \
-p 3000:3000 \
-e SKIP_ENV_VALIDATION=true \
-e NODE_ENV=production \
-e DATABASE_URL=postgresql://test:test@localhost:5432/test \
-e NEXTAUTH_URL=http://localhost:3000 \
-e NEXTAUTH_SECRET=test-secret-for-docker-test-${{ github.run_id }} \
-e DOCKER_HEALTH_CHECK_MODE=basic \
"$FIRST_TAG"
# Wait for container to become healthy with timeout
echo "⏳ Waiting for container to become healthy..."
timeout 120 bash -c "
while true; do
HEALTH=\$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME 2>/dev/null || echo 'none')
echo \"Health status: \$HEALTH\"
if [[ \"\$HEALTH\" == \"healthy\" ]]; then
break
fi
if [[ \"\$HEALTH\" == \"unhealthy\" ]]; then
echo \"❌ Container became unhealthy\"
docker logs $CONTAINER_NAME
exit 1
fi
sleep 3
done
"
# Test health endpoint with retries
echo "🔍 Testing health endpoint..."
sleep 5
MAX_ATTEMPTS=10
for i in $(seq 1 $MAX_ATTEMPTS); do
echo "Health check attempt $i/$MAX_ATTEMPTS..."
if curl -f -s http://localhost:3000/api/health > /dev/null; then
echo "✅ Health check passed on attempt $i"
break
fi
if [ $i -eq $MAX_ATTEMPTS ]; then
echo "❌ Health check failed after $MAX_ATTEMPTS attempts"
echo "Container logs:"
docker logs "$CONTAINER_NAME"
exit 1
fi
sleep 5
done
echo "✅ All Docker image tests passed successfully!"
# Cleanup
docker stop "$CONTAINER_NAME" || true
docker rm "$CONTAINER_NAME" || true
- name: Run container security scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ fromJSON(steps.meta.outputs.json).tags[0] }}
format: 'sarif'
output: 'trivy-image-results.sarif'
- name: Upload container scan results
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: 'trivy-image-results.sarif'
- name: Sign published Docker image
env:
TAGS: ${{ steps.meta.outputs.tags }}
DIGEST: ${{ steps.build-and-push.outputs.digest }}
run: |
echo "🔐 Signing Docker image for supply chain security..."
echo "${TAGS}" | xargs -I {} cosign sign --yes {}@${DIGEST}
echo "✅ Image signing completed"
- name: Export Docker artifacts for releases
if: startsWith(github.ref, 'refs/tags/v')
run: |
echo "🐳 Creating Docker release artifacts..."
mkdir -p docker-artifacts
# Create Docker compose file
cat > docker-artifacts/docker-compose.yml << 'EOF'
version: '3.8'
services:
app:
image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
ports:
- "3000:3000"
environment:
- DATABASE_URL=${DATABASE_URL}
- NEXTAUTH_SECRET=${NEXTAUTH_SECRET}
- NEXTAUTH_URL=${NEXTAUTH_URL}
- SKIP_ENV_VALIDATION=true
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/api/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
EOF
# Create deployment README
cat > docker-artifacts/README.md << 'EOF'
# SubPilot Docker Deployment
Version: ${{ github.ref_name }}
Image: `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}`
## Quick Start
1. Create a `.env` file with required variables:
```bash
DATABASE_URL=postgresql://username:password@host:5432/database
NEXTAUTH_SECRET=your-secret-key
NEXTAUTH_URL=https://your-domain.com
```
2. Run with docker-compose:
```bash
docker-compose up -d
```
## Direct Docker Run
```bash
docker run -d \
--name subpilot \
-p 3000:3000 \
-e DATABASE_URL=your-database-url \
-e NEXTAUTH_SECRET=your-secret \
-e NEXTAUTH_URL=https://your-domain.com \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
```
## Health Check
```bash
curl http://localhost:3000/api/health
```
EOF
echo "✅ Docker artifacts created successfully"
- name: Upload Docker artifacts
if: startsWith(github.ref, 'refs/tags/v')
uses: actions/upload-artifact@v4
with:
name: docker-artifacts
path: docker-artifacts/
retention-days: 30
# Job 3: Deployment
deploy:
name: Deploy to Environments
runs-on: ubuntu-latest
needs: [build-and-test, docker-build-publish]
if: success() && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
strategy:
matrix:
include:
- environment: staging
condition: github.ref == 'refs/heads/main'
image_tag: latest
- environment: production
condition: startsWith(github.ref, 'refs/tags/v')
image_tag: ${{ github.ref_name }}
environment: ${{ matrix.environment }}
steps:
- name: Deploy to ${{ matrix.environment }}
if: ${{ matrix.condition }}
run: |
echo "🚀 Deploying to ${{ matrix.environment }}"
echo "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.image_tag }}"
echo "Environment: ${{ matrix.environment }}"
echo "This deployment would typically involve:"
if [[ "${{ matrix.environment }}" == "staging" ]]; then
echo " - Rolling deployment to staging cluster"
echo " - Running smoke tests"
echo " - Updating staging monitoring dashboards"
else
echo " - Blue/green deployment strategy"
echo " - Database migrations (if needed)"
echo " - Health checks and rollback capability"
echo " - Updating production monitoring"
fi
# Placeholder for actual deployment commands:
# kubectl set image deployment/subpilot-app subpilot-app=${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ matrix.image_tag }}
# helm upgrade subpilot ./chart --set image.tag=${{ matrix.image_tag }}
echo "✅ ${{ matrix.environment }} deployment completed"
# Job 4: Release Management
release:
name: Create GitHub Release
runs-on: ubuntu-latest
needs: [build-and-test, docker-build-publish, deploy]
if: startsWith(github.ref, 'refs/tags/v')
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: release-artifacts
path: artifacts/
- name: Download Docker artifacts
uses: actions/download-artifact@v4
with:
name: docker-artifacts
path: docker-artifacts/
continue-on-error: true
- name: Check if release exists
id: check_release
run: |
if gh release view ${{ github.ref_name }} &>/dev/null; then
echo "exists=true" >> $GITHUB_OUTPUT
else
echo "exists=false" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Create or update GitHub Release
uses: softprops/action-gh-release@v2
with:
body: |
🚀 **SubPilot Release ${{ github.ref_name }}**
## 📦 What's Included
- ✅ Production-ready application build
- 🐳 Multi-platform Docker images (amd64, arm64)
- 🔒 Security-scanned and signed container images
- 📋 Complete deployment documentation
## 🚀 Quick Start
### Docker Deployment
```bash
docker run -d \
--name subpilot \
-p 3000:3000 \
-e DATABASE_URL=your-database-url \
-e NEXTAUTH_SECRET=your-secret \
-e NEXTAUTH_URL=https://your-domain.com \
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}
```
### Source Installation
```bash
git clone https://github.com/doublegate/SubPilot-App.git
cd SubPilot-App
git checkout ${{ github.ref_name }}
npm install && npm run build
```
## 🔍 Release Assets
- **Source Archives**: Complete source code in tar.gz and zip formats
- **Build Artifacts**: Pre-built application files
- **Docker Compose**: Ready-to-use deployment configuration
- **Documentation**: Setup and deployment guides
- **Checksums**: SHA256 verification for all artifacts
## 📋 Technical Details
- **Package Version**: v${{ needs.build-and-test.outputs.package-version }}
- **Docker Images**: Available at `${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}`
- **Security**: All images are vulnerability-scanned and signed
- **Platforms**: Supports linux/amd64 and linux/arm64
---
For detailed setup instructions, see the [README](https://github.com/doublegate/SubPilot-App#readme).
For release notes and changelog, see [CHANGELOG.md](https://github.com/doublegate/SubPilot-App/blob/main/CHANGELOG.md).
draft: false
prerelease: ${{ contains(github.ref, '-') }}
files: |
artifacts/subpilot-${{ github.ref_name }}-build.tar.gz
artifacts/subpilot-${{ github.ref_name }}-source.tar.gz
artifacts/checksums.sha256
docker-artifacts/docker-compose.yml
docker-artifacts/README.md
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Job 5: Cleanup and Monitoring
cleanup:
name: Cleanup & Monitoring
runs-on: ubuntu-latest
needs: [deploy, release]
if: always() && (success() || failure())
steps:
- name: Generate pipeline summary
run: |
echo "## 🎯 CI/CD Pipeline Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Repository:** ${{ github.repository }}" >> $GITHUB_STEP_SUMMARY
echo "**Branch/Tag:** ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "**Package Version:** v${{ needs.build-and-test.outputs.package-version || 'N/A' }}" >> $GITHUB_STEP_SUMMARY
echo "**Commit SHA:** ${{ github.sha }}" >> $GITHUB_STEP_SUMMARY
echo "**Trigger:** ${{ github.event_name }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Pipeline Status:**" >> $GITHUB_STEP_SUMMARY
echo "- Build & Test: ${{ needs.build-and-test.result == 'success' && '✅' || '❌' }} ${{ needs.build-and-test.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Docker Build: ${{ needs.docker-build-publish.result == 'success' && '✅' || needs.docker-build-publish.result == 'skipped' && '⏭️' || '❌' }} ${{ needs.docker-build-publish.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Deployment: ${{ needs.deploy.result == 'success' && '✅' || needs.deploy.result == 'skipped' && '⏭️' || '❌' }} ${{ needs.deploy.result }}" >> $GITHUB_STEP_SUMMARY
echo "- Release: ${{ needs.release.result == 'success' && '✅' || needs.release.result == 'skipped' && '⏭️' || '❌' }} ${{ needs.release.result }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" == "refs/heads/main" ]]; then
echo "**Docker Images Published:**" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest\`" >> $GITHUB_STEP_SUMMARY
echo "- \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:main-${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
elif [[ "${{ github.event_name }}" == "push" && "${{ github.ref }}" =~ ^refs/tags/v ]]; then
echo "**Release Published:**" >> $GITHUB_STEP_SUMMARY
echo "- Tag: ${{ github.ref_name }}" >> $GITHUB_STEP_SUMMARY
echo "- Docker: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY
echo "- GitHub Release: [View Release](https://github.com/${{ github.repository }}/releases/tag/${{ github.ref_name }})" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Build Time:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_STEP_SUMMARY
echo "**Workflow:** [${{ github.workflow }}](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> $GITHUB_STEP_SUMMARY
- name: Container registry maintenance
if: github.event_name == 'schedule'
run: |
echo "🧹 Container registry maintenance would run here"
echo "This scheduled job would typically:"
echo " - Remove images older than 30 days"
echo " - Keep the latest 10 versions for rollback capability"
echo " - Clean up untagged images and manifests"
echo " - Generate maintenance reports"
echo "✅ Maintenance planning completed"