Skip to content

Instantly share code, notes, and snippets.

@joelio
Created May 1, 2025 14:06
Show Gist options
  • Save joelio/8d6432c405daec5e70c7c5242fba1a6b to your computer and use it in GitHub Desktop.
Save joelio/8d6432c405daec5e70c7c5242fba1a6b to your computer and use it in GitHub Desktop.
Java Deployment Test
stages:
- validate
- build
- deploy
- notify
- rollback
variables:
# Java and application settings
JAVA_HOME: "/usr/lib/jvm/java-17-openjdk-amd64"
APP_NAME: "java-application"
# User settings (can be overridden per environment)
APP_USER: "appuser"
# Path settings (will be adjusted based on APP_USER)
BASE_PATH: "/home/${APP_USER}"
DEPLOY_DIR: "${BASE_PATH}/deployments"
CURRENT_LINK: "${BASE_PATH}/app/current"
BACKUP_DIR: "${BASE_PATH}/backups"
# Deployment and retention settings
MAX_BACKUPS: 5
HEALTH_CHECK_URL: "http://localhost:8080/healthcheck"
HEALTH_CHECK_RETRIES: 10
HEALTH_CHECK_DELAY: 5
# Notification settings
NOTIFICATION_EMAIL: "[email protected]"
NOTIFICATION_METHOD: "email" # Options: email, slack, teams, etc.
# Validate that we're on a protected branch
validate_branch:
stage: validate
script:
- |
if [[ "$CI_COMMIT_REF_NAME" != "main" &&
"$CI_COMMIT_REF_NAME" != "master" &&
"$CI_COMMIT_REF_NAME" != "develop" &&
"$CI_COMMIT_REF_NAME" != "production" &&
"$CI_COMMIT_REF_NAME" != "release/"* ]]; then
echo "Deployments are only allowed from protected branches (main, master, develop, production, release/*)"
exit 1
fi
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: '$CI_COMMIT_BRANCH'
# Build the application
build:
stage: build
image: maven:3.8-openjdk-17
script:
- mvn clean package -DskipTests
artifacts:
paths:
- target/*.jar
expire_in: 1 week
rules:
- if: '$CI_PIPELINE_SOURCE == "merge_request_event"'
when: never
- if: '$CI_COMMIT_BRANCH'
# Deploy the application
deploy:
stage: deploy
script:
- |
# Create necessary directories if they don't exist
ssh ${APP_USER}@$DEPLOY_HOST "mkdir -p $DEPLOY_DIR $BACKUP_DIR ${BASE_PATH}/app ${BASE_PATH}/tmp"
# Backup existing deployment if it exists
if ssh ${APP_USER}@$DEPLOY_HOST "test -L $CURRENT_LINK"; then
CURRENT_TARGET=$(ssh ${APP_USER}@$DEPLOY_HOST "readlink -f $CURRENT_LINK")
BACKUP_NAME="backup-$(date +%Y%m%d%H%M%S)-$CI_JOB_ID"
echo "Backing up current deployment to $BACKUP_DIR/$BACKUP_NAME"
ssh ${APP_USER}@$DEPLOY_HOST "cp -r $CURRENT_TARGET $BACKUP_DIR/$BACKUP_NAME"
fi
# Clean up old backups according to retention policy
ssh ${APP_USER}@$DEPLOY_HOST "ls -1t $BACKUP_DIR | tail -n +$((MAX_BACKUPS+1)) | xargs -I {} rm -rf $BACKUP_DIR/{}"
# Create new deployment directory with build ID
NEW_DEPLOY_DIR="$DEPLOY_DIR/${APP_NAME}-${CI_JOB_ID}"
ssh ${APP_USER}@$DEPLOY_HOST "mkdir -p $NEW_DEPLOY_DIR"
# Atomic upload: First to temp, then move
scp target/*.jar ${APP_USER}@$DEPLOY_HOST:${BASE_PATH}/tmp/${APP_NAME}-${CI_JOB_ID}.jar
ssh ${APP_USER}@$DEPLOY_HOST "mv ${BASE_PATH}/tmp/${APP_NAME}-${CI_JOB_ID}.jar $NEW_DEPLOY_DIR/"
# Create systemd service file for the application
cat > ${APP_NAME}.service << EOF
[Unit]
Description=${APP_NAME} Java Application
After=network.target
[Service]
Type=simple
User=${APP_USER}
WorkingDirectory=${BASE_PATH}/app
ExecStart=/usr/bin/java -jar %h/app/current/${APP_NAME}-${CI_JOB_ID}.jar
Environment="BUILD_ID=${CI_JOB_ID}"
Environment="ENV=${CI_ENVIRONMENT_NAME}"
SuccessExitStatus=143
TimeoutStopSec=10
Restart=on-failure
RestartSec=5
[Install]
WantedBy=default.target
EOF
# Upload and install service file
ssh ${APP_USER}@$DEPLOY_HOST "mkdir -p ${BASE_PATH}/.config/systemd/user/"
scp ${APP_NAME}.service ${APP_USER}@$DEPLOY_HOST:${BASE_PATH}/.config/systemd/user/
# Gracefully stop the current service if running
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user stop ${APP_NAME}.service || echo 'No service running or failed to stop gracefully'"
# Update symlink atomically
ssh ${APP_USER}@$DEPLOY_HOST "ln -sfn $NEW_DEPLOY_DIR $CURRENT_LINK"
# Ensure linger is enabled for user service persistence
ssh root@$DEPLOY_HOST "loginctl enable-linger ${APP_USER}"
# Start the service and enable it
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user daemon-reload && systemctl --user enable ${APP_NAME}.service && systemctl --user start ${APP_NAME}.service"
# Health check loop
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
echo "Performing health check attempt $i of $HEALTH_CHECK_RETRIES..."
if ssh ${APP_USER}@$DEPLOY_HOST "curl -s -f $HEALTH_CHECK_URL"; then
echo "Health check passed!"
# Store current deployment ID as the last successful one
ssh ${APP_USER}@$DEPLOY_HOST "echo $CI_JOB_ID > ${BASE_PATH}/app/last_successful_deploy"
exit 0
fi
sleep $HEALTH_CHECK_DELAY
done
# If we get here, health check failed
echo "Health check failed after $HEALTH_CHECK_RETRIES attempts. Initiating rollback..."
exit 1
environment:
name: production
variables:
DEPLOY_HOST: "your-deployment-server.example.com"
# You can override any of the main variables here for environment-specific settings
# APP_USER: "prod-appuser"
# Java and other environment settings can be customized per environment
# JAVA_OPTS: "-Xmx512m -Dspring.profiles.active=production"
rules:
- if: '$CI_COMMIT_BRANCH == "main" || $CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "production"'
- if: '$CI_COMMIT_BRANCH == "develop" || $CI_COMMIT_BRANCH =~ /^release\//'
when: manual
dependencies:
- build
needs:
- build
- validate_branch
allow_failure: false
retry:
max: 1
when: script_failure
# Manual rollback job that can be triggered if needed
rollback_manual:
stage: rollback
script:
- |
# Get list of available backups
BACKUPS=$(ssh ${APP_USER}@$DEPLOY_HOST "ls -1t $BACKUP_DIR")
if [ -z "$BACKUPS" ]; then
echo "No backups available for rollback"
exit 1
fi
LATEST_BACKUP=$(ssh ${APP_USER}@$DEPLOY_HOST "ls -1t $BACKUP_DIR | head -n 1")
echo "Rolling back to $LATEST_BACKUP"
# Stop current service
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user stop ${APP_NAME}.service"
# Update symlink to point to the backup
ssh ${APP_USER}@$DEPLOY_HOST "ln -sfn $BACKUP_DIR/$LATEST_BACKUP $CURRENT_LINK"
# Start service again
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user start ${APP_NAME}.service"
# Health check
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
echo "Performing health check attempt $i of $HEALTH_CHECK_RETRIES..."
if ssh ${APP_USER}@$DEPLOY_HOST "curl -s -f $HEALTH_CHECK_URL"; then
echo "Rollback successful and health check passed!"
exit 0
fi
sleep $HEALTH_CHECK_DELAY
done
echo "Health check failed after rollback. Please check the application logs."
exit 1
environment:
name: production
variables:
DEPLOY_HOST: "your-deployment-server.example.com"
when: manual
needs: []
# Auto rollback when deployment fails - triggered by deploy job failure
rollback_auto:
stage: rollback
script:
- |
echo "Automatic rollback triggered due to deployment failure"
# Get last successful deploy ID
LAST_DEPLOY_ID=$(ssh ${APP_USER}@$DEPLOY_HOST "cat ${BASE_PATH}/app/last_successful_deploy || echo ''")
if [ -z "$LAST_DEPLOY_ID" ]; then
# Try to get latest backup if no last successful deploy ID
LATEST_BACKUP=$(ssh ${APP_USER}@$DEPLOY_HOST "ls -1t $BACKUP_DIR | head -n 1")
if [ -z "$LATEST_BACKUP" ]; then
echo "No backups or last successful deploy ID available for rollback"
exit 1
fi
echo "Rolling back to backup $LATEST_BACKUP"
# Stop current service
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user stop ${APP_NAME}.service"
# Update symlink to point to the backup
ssh ${APP_USER}@$DEPLOY_HOST "ln -sfn $BACKUP_DIR/$LATEST_BACKUP $CURRENT_LINK"
else
echo "Rolling back to previous successful deployment: $LAST_DEPLOY_ID"
PREVIOUS_DEPLOY_DIR="$DEPLOY_DIR/${APP_NAME}-${LAST_DEPLOY_ID}"
# Stop current service
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user stop ${APP_NAME}.service"
# Update symlink to point to the previous successful deployment
ssh ${APP_USER}@$DEPLOY_HOST "ln -sfn $PREVIOUS_DEPLOY_DIR $CURRENT_LINK"
fi
# Start service again
ssh ${APP_USER}@$DEPLOY_HOST "systemctl --user start ${APP_NAME}.service"
# Health check
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
echo "Performing rollback health check attempt $i of $HEALTH_CHECK_RETRIES..."
if ssh ${APP_USER}@$DEPLOY_HOST "curl -s -f $HEALTH_CHECK_URL"; then
echo "Rollback successful and health check passed!"
exit 0
fi
sleep $HEALTH_CHECK_DELAY
done
echo "Health check failed after rollback. Please check the application logs."
exit 1
environment:
name: production
variables:
DEPLOY_HOST: "your-deployment-server.example.com"
when: on_failure
needs:
- deploy
# Clean up old deployments according to retention policy
cleanup:
stage: deploy
script:
- |
# Get all deployments except the current one
CURRENT=$(ssh ${APP_USER}@$DEPLOY_HOST "readlink -f $CURRENT_LINK || echo ''")
ALL_DEPLOYMENTS=$(ssh ${APP_USER}@$DEPLOY_HOST "find $DEPLOY_DIR -maxdepth 1 -type d -name '${APP_NAME}-*' | sort -r")
# Keep track of how many we've kept
KEPT=0
# Process each deployment
for DEPLOYMENT in $ALL_DEPLOYMENTS; do
# Skip if this is the current deployment
if [ "$DEPLOYMENT" = "$CURRENT" ]; then
continue
fi
# Keep if we haven't reached our limit
if [ $KEPT -lt $MAX_BACKUPS ]; then
KEPT=$((KEPT+1))
continue
fi
# Otherwise, delete it
echo "Removing old deployment: $DEPLOYMENT"
ssh ${APP_USER}@$DEPLOY_HOST "rm -rf $DEPLOYMENT"
done
environment:
name: production
variables:
DEPLOY_HOST: "your-deployment-server.example.com"
when: on_success
needs:
- deploy
# Notification on success
notify_success:
stage: notify
script:
- |
if [ "$NOTIFICATION_METHOD" == "email" ]; then
echo "Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) was successful." |
mail -s "[SUCCESS] $APP_NAME Deployment to $CI_ENVIRONMENT_NAME" $NOTIFICATION_EMAIL
elif [ "$NOTIFICATION_METHOD" == "slack" ]; then
# Example for Slack notification - requires webhook URL to be set
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"✅ Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) to $CI_ENVIRONMENT_NAME was successful.\"}" $SLACK_WEBHOOK_URL
else
echo "SLACK_WEBHOOK_URL not set. Skipping Slack notification."
fi
else
echo "Notification method $NOTIFICATION_METHOD not configured. Defaulting to console output."
echo "✅ Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) to $CI_ENVIRONMENT_NAME was successful."
fi
when: on_success
needs:
- deploy
# Notification on failure
notify_failure:
stage: notify
script:
- |
if [ "$NOTIFICATION_METHOD" == "email" ]; then
echo "Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) to $CI_ENVIRONMENT_NAME failed." |
mail -s "[FAILURE] $APP_NAME Deployment" $NOTIFICATION_EMAIL
elif [ "$NOTIFICATION_METHOD" == "slack" ]; then
# Example for Slack notification - requires webhook URL to be set
if [ -n "$SLACK_WEBHOOK_URL" ]; then
curl -X POST -H 'Content-type: application/json' --data "{\"text\":\"❌ Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) to $CI_ENVIRONMENT_NAME failed.\"}" $SLACK_WEBHOOK_URL
else
echo "SLACK_WEBHOOK_URL not set. Skipping Slack notification."
fi
else
echo "Notification method $NOTIFICATION_METHOD not configured. Defaulting to console output."
echo "❌ Deployment of $APP_NAME version $CI_COMMIT_SHA (Job ID: $CI_JOB_ID) to $CI_ENVIRONMENT_NAME failed."
fi
when: on_failure
needs:
- deploy
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment