Merge pull request #44 from Buffden/ci-cd/setup-pipeline-for-deployment #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy API | |
| on: | |
| push: | |
| branches: [main] | |
| paths: | |
| - 'tinyurl/**' | |
| - 'docker-compose.prod.yml' | |
| - 'infra/**' | |
| - '.github/workflows/deploy.yml' | |
| workflow_dispatch: | |
| jobs: | |
| build-test: | |
| name: Build and Test | |
| runs-on: ubuntu-latest | |
| defaults: | |
| run: | |
| working-directory: tinyurl | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Set up Java 21 | |
| uses: actions/setup-java@v4 | |
| with: | |
| distribution: temurin | |
| java-version: '21' | |
| cache: gradle | |
| - name: Make Gradle wrapper executable | |
| run: chmod +x gradlew | |
| - name: Run unit tests | |
| run: ./gradlew --no-daemon clean test | |
| - name: Build executable JAR | |
| run: ./gradlew --no-daemon bootJar | |
| compose-smoke: | |
| name: Compose Smoke Test | |
| needs: build-test | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Generate ephemeral CI credentials | |
| run: | | |
| PG_PASS=$(openssl rand -hex 16) | |
| APP_PASS=$(openssl rand -hex 16) | |
| echo "::add-mask::$PG_PASS" | |
| echo "::add-mask::$APP_PASS" | |
| echo "POSTGRES_USER=tinyurl_ci" >> $GITHUB_ENV | |
| echo "POSTGRES_PASSWORD<<EOF" >> $GITHUB_ENV | |
| echo "$PG_PASS" >> $GITHUB_ENV | |
| echo "EOF" >> $GITHUB_ENV | |
| echo "SPRING_DATASOURCE_USERNAME=tinyurl_appuser_ci" >> $GITHUB_ENV | |
| echo "SPRING_DATASOURCE_PASSWORD<<EOF" >> $GITHUB_ENV | |
| echo "$APP_PASS" >> $GITHUB_ENV | |
| echo "EOF" >> $GITHUB_ENV | |
| echo "SPRING_FLYWAY_USER=tinyurl_ci" >> $GITHUB_ENV | |
| echo "SPRING_FLYWAY_PASSWORD<<EOF" >> $GITHUB_ENV | |
| echo "$PG_PASS" >> $GITHUB_ENV | |
| echo "EOF" >> $GITHUB_ENV | |
| - name: Build and start stack | |
| run: docker compose up -d --build | |
| - name: Wait for nginx route health | |
| run: | | |
| for i in {1..40}; do | |
| if curl -fsS http://localhost:8080/actuator/health >/dev/null; then | |
| exit 0 | |
| fi | |
| sleep 3 | |
| done | |
| echo "Service did not become healthy in time" | |
| docker compose logs --no-color | |
| exit 1 | |
| - name: Stop stack | |
| if: always() | |
| run: docker compose down -v | |
| deploy: | |
| name: Deploy to EC2 via SSM | |
| needs: [build-test, compose-smoke] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| id-token: write # Required for OIDC | |
| contents: read | |
| packages: write # Required for GHCR push | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Configure AWS credentials (OIDC) | |
| uses: aws-actions/configure-aws-credentials@v4 | |
| with: | |
| role-to-assume: ${{ secrets.AWS_ROLE_ARN }} | |
| aws-region: us-east-1 | |
| - name: Fetch parameters from AWS Parameter Store | |
| run: | | |
| EC2_INSTANCE_ID=$(aws ssm get-parameter \ | |
| --name "/tinyurl/cicd/ec2-instance-id" \ | |
| --with-decryption \ | |
| --query "Parameter.Value" --output text 2>/dev/null) \ | |
| || { echo "Failed to fetch instance ID from SSM"; exit 1; } | |
| RDS_ENDPOINT=$(aws ssm get-parameter \ | |
| --name "/tinyurl/cicd/rds-endpoint" \ | |
| --with-decryption \ | |
| --query "Parameter.Value" --output text 2>/dev/null) \ | |
| || { echo "Failed to fetch RDS endpoint from SSM"; exit 1; } | |
| echo "::add-mask::$EC2_INSTANCE_ID" | |
| echo "::add-mask::$RDS_ENDPOINT" | |
| echo "EC2_INSTANCE_ID=$EC2_INSTANCE_ID" >> $GITHUB_ENV | |
| echo "RDS_ENDPOINT=$RDS_ENDPOINT" >> $GITHUB_ENV | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Build and push Docker image | |
| env: | |
| IMAGE_TAG: ${{ github.sha }} | |
| run: | | |
| docker build -t ghcr.io/buffden/tinyurl-api:$IMAGE_TAG tinyurl/ | |
| docker push ghcr.io/buffden/tinyurl-api:$IMAGE_TAG | |
| - name: Start EC2 and wait for SSM agent | |
| run: | | |
| STATE=$(aws ec2 describe-instances \ | |
| --instance-ids "$EC2_INSTANCE_ID" \ | |
| --query "Reservations[0].Instances[0].State.Name" \ | |
| --output text) | |
| if [ "$STATE" != "running" ]; then | |
| echo "Instance is $STATE — starting it" | |
| aws ec2 start-instances --instance-ids "$EC2_INSTANCE_ID" | |
| aws ec2 wait instance-running --instance-ids "$EC2_INSTANCE_ID" | |
| echo "STARTED_BY_PIPELINE=true" >> $GITHUB_ENV | |
| else | |
| echo "Instance already running — will not stop after deploy" | |
| fi | |
| echo "Waiting for SSM agent to come online..." | |
| for i in {1..20}; do | |
| SSM_STATUS=$(aws ssm describe-instance-information \ | |
| --filters "Key=InstanceIds,Values=$EC2_INSTANCE_ID" \ | |
| --query "InstanceInformationList[0].PingStatus" \ | |
| --output text 2>/dev/null) | |
| echo "SSM agent status: $SSM_STATUS ($i/20)" | |
| if [ "$SSM_STATUS" = "Online" ]; then | |
| echo "SSM agent is online" | |
| exit 0 | |
| fi | |
| sleep 15 | |
| done | |
| echo "SSM agent did not come online in time" | |
| exit 1 | |
| - name: Deploy via SSM RunCommand | |
| env: | |
| IMAGE_TAG: ${{ github.sha }} | |
| run: | | |
| COMMAND_ID=$(aws ssm send-command \ | |
| --instance-ids "$EC2_INSTANCE_ID" \ | |
| --document-name "AWS-RunShellScript" \ | |
| --parameters "commands=[ | |
| \"export IMAGE_TAG=$IMAGE_TAG\", | |
| \"export RDS_ENDPOINT=$RDS_ENDPOINT\", | |
| \"cd /app\", | |
| \"docker compose -f docker-compose.prod.yml pull\", | |
| \"docker compose -f docker-compose.prod.yml up -d\" | |
| ]" \ | |
| --output text \ | |
| --query "Command.CommandId") | |
| echo "SSM Command ID: $COMMAND_ID" | |
| for i in {1..18}; do | |
| STATUS=$(aws ssm get-command-invocation \ | |
| --command-id "$COMMAND_ID" \ | |
| --instance-id "$EC2_INSTANCE_ID" \ | |
| --query "Status" \ | |
| --output text) | |
| echo "Status: $STATUS ($i/18)" | |
| if [ "$STATUS" = "Success" ]; then | |
| echo "Deploy succeeded" | |
| exit 0 | |
| elif [ "$STATUS" = "Failed" ] || [ "$STATUS" = "TimedOut" ] || [ "$STATUS" = "Cancelled" ]; then | |
| echo "Deploy failed with status: $STATUS" | |
| aws ssm get-command-invocation \ | |
| --command-id "$COMMAND_ID" \ | |
| --instance-id "$EC2_INSTANCE_ID" \ | |
| --query "StandardErrorContent" \ | |
| --output text | |
| exit 1 | |
| fi | |
| sleep 10 | |
| done | |
| echo "Deploy timed out waiting for SSM" | |
| exit 1 | |
| - name: Stop EC2 instance | |
| if: always() && env.STARTED_BY_PIPELINE == 'true' | |
| run: | | |
| aws ec2 stop-instances --instance-ids "$EC2_INSTANCE_ID" | |
| echo "EC2 instance stopped" |