diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0dcb5855..409198f4 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -1,37 +1,24 @@ -name: Deploy to Production +name: Release on: push: branches: [main] - workflow_dispatch: - inputs: - validate_image: - description: 'Run container validation (normally skipped - PR CI validates)' - required: false - default: false - type: boolean -# Ensure only one deployment runs at a time concurrency: - group: production-deploy + group: release cancel-in-progress: false -env: - AWS_REGION: us-east-1 - ECR_REPOSITORY: genealogy-frontend - jobs: - # Semantic release FIRST - creates version, changelog, and GitHub release release: + name: Semantic Release runs-on: ubuntu-latest permissions: contents: write issues: write pull-requests: write outputs: - new_version: ${{ steps.get-version.outputs.version }} released: ${{ steps.get-version.outputs.released }} - sha: ${{ steps.get-version.outputs.sha }} + version: ${{ steps.get-version.outputs.version }} steps: - name: Checkout @@ -50,7 +37,6 @@ jobs: run: npm ci - name: Run semantic-release - id: semantic env: GITHUB_TOKEN: ${{ secrets.GH_TOKEN }} run: npx semantic-release @@ -58,269 +44,31 @@ jobs: - name: Get release info id: get-version run: | - # Fetch any new tags/commits created by semantic-release git fetch --tags - git fetch origin main - # Get the latest commit SHA (may be a new release commit) - LATEST_SHA=$(git rev-parse origin/main) - # Get the latest semantic version tag - LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "v0.0.0") - VERSION=${LATEST_TAG#v} - echo "sha=$LATEST_SHA" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "released=true" >> $GITHUB_OUTPUT - echo "Using version: $VERSION (sha: $LATEST_SHA)" - - build-and-deploy: - runs-on: ubuntu-latest - needs: release - permissions: - id-token: write - contents: read - - steps: - - name: Checkout latest code (includes release commit) - uses: actions/checkout@v4 - with: - # Use the SHA from release job - this includes the version bump commit - ref: ${{ needs.release.outputs.sha || github.sha }} - fetch-depth: 0 - - - name: Setup Node.js with cache - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: ${{ env.AWS_REGION }} - - - name: Login to Amazon ECR - id: login-ecr - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Log version from package.json - run: | - echo "Building with version from package.json:" - node -p "require('./package.json').version" - - - name: Build and push Docker image - uses: docker/build-push-action@v5 - with: - context: . - push: true - tags: | - ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:${{ github.sha }} - ${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:latest - build-args: | - NEXTAUTH_URL=${{ secrets.NEXTAUTH_URL }} - cache-from: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache - cache-to: type=registry,ref=${{ steps.login-ecr.outputs.registry }}/${{ env.ECR_REPOSITORY }}:buildcache,mode=max - provenance: false - sbom: false - - - name: Validate Docker image - if: ${{ github.event.inputs.validate_image == 'true' }} - env: - ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} - IMAGE_TAG: ${{ github.sha }} - run: | - echo "Starting container for health check validation..." - docker pull $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG - CONTAINER_ID=$(docker run -d -p 3000:3000 \ - -e DATABASE_URL="postgresql://test:test@localhost:5432/test" \ - -e NEXTAUTH_URL="http://localhost:3000" \ - -e NEXTAUTH_SECRET="test-secret-for-validation" \ - $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG) - - echo "Waiting for container to start..." - sleep 10 - - # Check if container is still running - if ! docker ps -q --filter "id=$CONTAINER_ID" | grep -q .; then - echo "Container exited unexpectedly!" - docker logs $CONTAINER_ID - exit 1 - fi - - echo "Testing health endpoint..." - HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:3000/api/health || echo "000") - docker stop $CONTAINER_ID > /dev/null && docker rm $CONTAINER_ID > /dev/null - - if [ "$HEALTH_STATUS" = "000" ]; then - echo "Health check failed - container not responding" - exit 1 - fi - echo "Image validation passed (health endpoint returned $HEALTH_STATUS)" - - - name: Deploy to App Runner - run: | - # Check if service exists - SERVICE_ARN=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='genealogy-frontend'].ServiceArn" --output text) - - if [ -z "$SERVICE_ARN" ]; then - echo "Creating new App Runner service WITHOUT VPC connector first..." - aws apprunner create-service \ - --service-name genealogy-frontend \ - --source-configuration '{ - "AuthenticationConfiguration": { - "AccessRoleArn": "${{ secrets.APPRUNNER_ECR_ROLE_ARN }}" - }, - "ImageRepository": { - "ImageIdentifier": "${{ steps.login-ecr.outputs.registry }}/genealogy-frontend:latest", - "ImageRepositoryType": "ECR", - "ImageConfiguration": { - "Port": "3000", - "RuntimeEnvironmentVariables": { - "DATABASE_URL": "${{ secrets.DATABASE_URL }}", - "NEXTAUTH_URL": "${{ secrets.NEXTAUTH_URL }}", - "NEXTAUTH_SECRET": "${{ secrets.NEXTAUTH_SECRET }}", - "GOOGLE_CLIENT_ID": "${{ secrets.GOOGLE_CLIENT_ID }}", - "GOOGLE_CLIENT_SECRET": "${{ secrets.GOOGLE_CLIENT_SECRET }}", - "AUTH_TRUST_HOST": "true" - } - } - } - }' \ - --instance-configuration '{ - "Cpu": "1024", - "Memory": "2048", - "InstanceRoleArn": "${{ secrets.APPRUNNER_INSTANCE_ROLE_ARN }}" - }' \ - --health-check-configuration '{ - "Protocol": "HTTP", - "Path": "/api/health", - "Interval": 10, - "Timeout": 5, - "HealthyThreshold": 1, - "UnhealthyThreshold": 5 - }' \ - --tags Key=Project,Value=genealogy Key=Environment,Value=production + LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "") + if [[ -n "$LATEST_TAG" ]]; then + echo "released=true" >> $GITHUB_OUTPUT + echo "version=${LATEST_TAG}" >> $GITHUB_OUTPUT + echo "New release: $LATEST_TAG" else - echo "Updating service with new image and ensuring env vars are set..." - MAX_ATTEMPTS=120 # 10 minutes max (120 x 5s) - DEPLOYED=false - - for i in $(seq 1 $MAX_ATTEMPTS); do - STATUS=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='genealogy-frontend'].Status" --output text) - echo "Attempt $i/$MAX_ATTEMPTS - Service status: $STATUS" - - if [ "$STATUS" = "RUNNING" ]; then - echo "Service is RUNNING, updating service configuration..." - # Use update-service instead of start-deployment to ensure env vars are always set - aws apprunner update-service \ - --service-arn "$SERVICE_ARN" \ - --source-configuration '{ - "AuthenticationConfiguration": { - "AccessRoleArn": "${{ secrets.APPRUNNER_ECR_ROLE_ARN }}" - }, - "ImageRepository": { - "ImageIdentifier": "${{ steps.login-ecr.outputs.registry }}/genealogy-frontend:latest", - "ImageRepositoryType": "ECR", - "ImageConfiguration": { - "Port": "3000", - "RuntimeEnvironmentVariables": { - "DATABASE_URL": "${{ secrets.DATABASE_URL }}", - "NEXTAUTH_URL": "${{ secrets.NEXTAUTH_URL }}", - "NEXTAUTH_SECRET": "${{ secrets.NEXTAUTH_SECRET }}", - "GOOGLE_CLIENT_ID": "${{ secrets.GOOGLE_CLIENT_ID }}", - "GOOGLE_CLIENT_SECRET": "${{ secrets.GOOGLE_CLIENT_SECRET }}", - "AUTH_TRUST_HOST": "true" - } - } - } - }' && DEPLOYED=true && break - echo "Update failed, service may have changed state. Retrying..." - fi - - sleep 5 - done - - if [ "$DEPLOYED" != "true" ]; then - echo "Error: Could not update service after $MAX_ATTEMPTS attempts" - exit 1 - fi + echo "released=false" >> $GITHUB_OUTPUT + echo "version=" >> $GITHUB_OUTPUT + echo "No new release" fi - - name: Wait for deployment to complete - run: | - SERVICE_ARN=$(aws apprunner list-services --query "ServiceSummaryList[?ServiceName=='genealogy-frontend'].ServiceArn" --output text) - echo "Waiting for deployment to complete..." - - MAX_WAIT=120 # 10 minutes max (120 x 5s) - for i in $(seq 1 $MAX_WAIT); do - STATUS=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" --query 'Service.Status' --output text) - echo "[$i/$MAX_WAIT] Service status: $STATUS" - - if [ "$STATUS" = "RUNNING" ]; then - echo "✅ Deployment completed successfully!" - - # Verify health endpoint - SERVICE_URL=$(aws apprunner describe-service --service-arn "$SERVICE_ARN" --query 'Service.ServiceUrl' --output text) - echo "Verifying health at https://$SERVICE_URL/api/health" - HEALTH_STATUS=$(curl -s -o /dev/null -w "%{http_code}" "https://$SERVICE_URL/api/health" || echo "000") - - if [ "$HEALTH_STATUS" = "200" ]; then - echo "✅ Health check passed!" - exit 0 - else - echo "⚠️ Health check returned $HEALTH_STATUS (may still be warming up)" - exit 0 # Don't fail - App Runner health checks will handle this - fi - elif [ "$STATUS" = "OPERATION_IN_PROGRESS" ]; then - echo "Deployment in progress..." - elif [ "$STATUS" = "CREATE_FAILED" ] || [ "$STATUS" = "DELETE_FAILED" ]; then - echo "❌ Deployment failed with status: $STATUS" - exit 1 - fi - - sleep 5 - done - - echo "❌ Timeout waiting for deployment to complete" - exit 1 - - # Cleanup runs as a separate job to not block deployment - cleanup: + dispatch-deploy: + name: Trigger Deploy runs-on: ubuntu-latest - needs: build-and-deploy - if: success() - permissions: - id-token: write - contents: read + needs: release + if: needs.release.outputs.released == 'true' steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} - aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} - aws-region: us-east-1 - - - name: Cleanup old ECR images + - name: Dispatch to kindred-infra run: | - echo "Cleaning up old ECR images (keeping last 10)..." - IMAGES_TO_DELETE=$(aws ecr describe-images \ - --repository-name genealogy-frontend \ - --query 'sort_by(imageDetails,&imagePushedAt)[:-10].imageDigest' \ - --output text) - - if [ -z "$IMAGES_TO_DELETE" ] || [ "$IMAGES_TO_DELETE" = "None" ]; then - echo "No old images to delete" - else - for DIGEST in $IMAGES_TO_DELETE; do - echo "Deleting image: $DIGEST" - aws ecr batch-delete-image \ - --repository-name genealogy-frontend \ - --image-ids imageDigest=$DIGEST \ - || echo "Failed to delete $DIGEST (may be in use)" - done - echo "Cleanup complete" - fi + echo "Triggering deploy of ${{ needs.release.outputs.version }} in kindred-infra" + curl -sf -X POST \ + -H "Authorization: token ${{ secrets.GH_TOKEN }}" \ + -H "Accept: application/vnd.github.v3+json" \ + "https://api.github.com/repos/pmilano1/kindred-infra/dispatches" \ + -d "{\"event_type\":\"deploy\",\"client_payload\":{\"tag\":\"${{ needs.release.outputs.version }}\",\"sha\":\"${{ github.sha }}\"}}" + echo "Deploy dispatched for ${{ needs.release.outputs.version }}"