Skip to content

Merge pull request #44 from Buffden/ci-cd/setup-pipeline-for-deployment #7

Merge pull request #44 from Buffden/ci-cd/setup-pipeline-for-deployment

Merge pull request #44 from Buffden/ci-cd/setup-pipeline-for-deployment #7

Workflow file for this run

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"