Skip to content

Commit 338bd7e

Browse files
feat: migrate to GraalVM native image as default deployment (#104)
Agent-Logs-Url: https://github.com/EspacoGeek-Teams/SpringAPI_EspacoGeek/sessions/a5b35835-6769-4a24-a315-612c044cde88 Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
1 parent 0312d7f commit 338bd7e

3 files changed

Lines changed: 224 additions & 24 deletions

File tree

.github/workflows/cicd.yml

Lines changed: 214 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,8 @@ jobs:
6060
build_and_publish:
6161
name: Build, Docker and Push
6262
runs-on: ubuntu-latest
63-
needs: tests # Only runs if tests pass
64-
if: ${{ needs.tests.result == 'success' }}
63+
needs: tests
64+
if: false
6565
env:
6666
# Base image name (adjust as needed)
6767
APP_NAME: espacogeek-api
@@ -592,22 +592,224 @@ jobs:
592592
username: ${{ github.actor }}
593593
password: ${{ secrets.GITHUB_TOKEN }}
594594

595-
# Tag and push the validated native image to GHCR.
596-
# Only runs after a successful health check and only on pushes to master.
597595
- name: Tag and push native Docker image
598596
if: steps.vars.outputs.should_push == 'true' && github.event_name == 'push'
599597
shell: bash
600598
run: |
601599
set -euo pipefail
602-
IFS=',' read -ra TAGS <<< "${{ steps.vars.outputs.tags_csv }}"
603-
for tag in "${TAGS[@]}"; do
604-
FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}-native:${tag}"
605-
docker tag native-validation:ci "${FULL_TAG}"
606-
docker push "${FULL_TAG}"
607-
echo "Pushed: ${FULL_TAG}"
608-
done
600+
FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}:native"
601+
docker tag native-validation:ci "${FULL_TAG}"
602+
docker push "${FULL_TAG}"
603+
echo "Pushed: ${FULL_TAG}"
604+
605+
- name: Deploy to Hostinger via SSH
606+
uses: appleboy/ssh-action@v0.1.9
607+
if: ${{ needs.tests.result == 'success' && steps.vars.outputs.should_push == 'true' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') }}
608+
with:
609+
host: ${{ secrets.HOSTINGER_HOST }}
610+
username: ${{ secrets.HOSTINGER_USER }}
611+
key: ${{ secrets.HOSTINGER }}
612+
port: ${{ secrets.HOSTINGER_PORT }}
613+
envs: GHCR_OWNER_LC,APP_NAME
614+
script_stop: false
615+
script: |
616+
cleanup_on_exit() {
617+
echo "Running SSH-level cleanup..."
618+
rm -f .env.espacogeek 2>/dev/null || true
619+
docker logout ghcr.io 2>/dev/null || true
620+
}
621+
trap cleanup_on_exit EXIT INT TERM
622+
623+
DEPLOY_SCRIPT="/opt/espacogeek/deploy.sh"
624+
625+
mkdir -p "$(dirname "$DEPLOY_SCRIPT")"
626+
627+
cat > "$DEPLOY_SCRIPT" << 'DEPLOY_SCRIPT_EOF'
628+
#!/bin/bash
629+
set -euo pipefail
630+
631+
RED='\033[0;31m'
632+
GREEN='\033[0;32m'
633+
YELLOW='\033[1;33m'
634+
BLUE='\033[0;34m'
635+
NC='\033[0m'
636+
637+
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
638+
log_success() { echo -e "${GREEN}[✓]${NC} $*"; }
639+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
640+
log_error() { echo -e "${RED}[✗]${NC} $*"; }
641+
642+
GHCR_OWNER_LC="${1:-}"
643+
APP_NAME="${2:-}"
644+
IMAGE_TAG="${3:-native}"
645+
ENV_FILE="${4:-.env.espacogeek}"
646+
CONTAINER_NAME="espacogeek-api"
647+
BACKUP_DIR="${HOME}/espacogeek-api-backups"
648+
OLD_CONTAINER_BACKUP="${CONTAINER_NAME}-old"
649+
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
650+
651+
cleanup_env_file() {
652+
if [ -f "$ENV_FILE" ]; then
653+
log_info "Cleaning up environment file..."
654+
rm -f "$ENV_FILE"
655+
fi
656+
}
657+
trap cleanup_env_file EXIT
658+
659+
if [ -z "$GHCR_OWNER_LC" ] || [ -z "$APP_NAME" ]; then
660+
log_error "Usage: $0 <GHCR_OWNER_LC> <APP_NAME> [IMAGE_TAG] [ENV_FILE]"
661+
exit 1
662+
fi
663+
664+
IMAGE="ghcr.io/${GHCR_OWNER_LC}/${APP_NAME}:${IMAGE_TAG}"
665+
666+
container_exists() { docker ps -a --format '{{.Names}}' | grep -q "^${1}$"; }
667+
668+
if container_exists "$CONTAINER_NAME"; then
669+
log_info "Creating backup of old container..."
670+
mkdir -p "$BACKUP_DIR"
671+
BACKUP_FILE="${BACKUP_DIR}/${CONTAINER_NAME}_backup_${TIMESTAMP}.tar"
672+
if docker export "$CONTAINER_NAME" > "$BACKUP_FILE"; then
673+
log_success "Container backup created"
674+
ls -t "${BACKUP_DIR}/${CONTAINER_NAME}_backup_"*.tar 2>/dev/null | tail -n +6 | xargs rm -f 2>/dev/null || true
675+
else
676+
log_error "Failed to backup container"
677+
exit 1
678+
fi
679+
fi
680+
681+
if container_exists "$CONTAINER_NAME"; then
682+
log_info "Stopping old container..."
683+
docker stop "$CONTAINER_NAME" 2>/dev/null || true
684+
docker rename "$CONTAINER_NAME" "${OLD_CONTAINER_BACKUP}" 2>/dev/null || true
685+
log_success "Old container renamed to ${OLD_CONTAINER_BACKUP}"
686+
fi
687+
688+
log_info "Pulling new image..."
689+
if ! docker pull "$IMAGE"; then
690+
log_error "Failed to pull image"
691+
if container_exists "${OLD_CONTAINER_BACKUP}"; then
692+
log_warn "Restoring old container..."
693+
docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true
694+
docker start "$CONTAINER_NAME" 2>/dev/null || true
695+
fi
696+
exit 1
697+
fi
698+
log_success "Image pulled successfully"
699+
700+
log_info "Starting new container..."
701+
if ! docker run -d --name "$CONTAINER_NAME" --network infra_network -p 8080:8080 --restart unless-stopped --env-file "$ENV_FILE" "$IMAGE"; then
702+
log_error "Failed to start container"
703+
if container_exists "${OLD_CONTAINER_BACKUP}"; then
704+
log_warn "Restoring old container..."
705+
docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true
706+
docker start "$CONTAINER_NAME" 2>/dev/null || true
707+
fi
708+
exit 1
709+
fi
710+
log_success "Container started successfully"
711+
712+
log_info "Waiting for application to initialize (up to 30s)..."
713+
MAX_RETRIES=6
714+
SLEEP_SECONDS=5
715+
retries=0
716+
HEALTHY=0
717+
while [ "$retries" -lt "$MAX_RETRIES" ]; do
718+
if ! container_exists "$CONTAINER_NAME"; then
719+
log_error "Health check: container '$CONTAINER_NAME' is not running"
720+
break
721+
fi
722+
723+
STATUS="$(docker inspect -f '{{.State.Status}}' "$CONTAINER_NAME" 2>/dev/null || echo "unknown")"
724+
RESTARTING="$(docker inspect -f '{{.State.Restarting}}' "$CONTAINER_NAME" 2>/dev/null || echo "false")"
725+
726+
if [ "$STATUS" = "running" ] && [ "$RESTARTING" = "false" ]; then
727+
log_success "Health check passed: container is running (status=$STATUS, restarting=$RESTARTING)"
728+
HEALTHY=1
729+
break
730+
fi
731+
732+
log_info "Health check: status=$STATUS, restarting=$RESTARTING. Retrying in ${SLEEP_SECONDS}s..."
733+
sleep "$SLEEP_SECONDS"
734+
retries=$((retries + 1))
735+
done
736+
737+
if [ "$HEALTHY" -ne 1 ]; then
738+
log_error "Container failed health check after $((MAX_RETRIES * SLEEP_SECONDS))s"
739+
if container_exists "$CONTAINER_NAME"; then
740+
log_warn "Stopping and removing failed new container..."
741+
docker rm -f "$CONTAINER_NAME" 2>/dev/null || true
742+
fi
743+
if container_exists "${OLD_CONTAINER_BACKUP}"; then
744+
log_warn "Restoring old container..."
745+
docker rename "${OLD_CONTAINER_BACKUP}" "$CONTAINER_NAME" 2>/dev/null || true
746+
docker start "$CONTAINER_NAME" 2>/dev/null || true
747+
fi
748+
exit 1
749+
fi
750+
if container_exists "${OLD_CONTAINER_BACKUP}"; then
751+
log_info "Removing old container backup..."
752+
docker rm -f "${OLD_CONTAINER_BACKUP}" 2>/dev/null || true
753+
log_success "Old container removed"
754+
fi
755+
756+
log_success ""
757+
log_success "=== DEPLOYMENT SUCCESSFUL ==="
758+
docker ps -a --filter name="$CONTAINER_NAME"
759+
log_info ""
760+
log_info "Recent logs:"
761+
docker logs --tail 10 "$CONTAINER_NAME" || true
762+
log_success "============================="
763+
DEPLOY_SCRIPT_EOF
764+
765+
chmod +x "$DEPLOY_SCRIPT"
766+
767+
cat > .env.espacogeek << ENVEOF
768+
SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }}
769+
SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }}
770+
SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }}
771+
SPRING_MVC_CORS_ALLOWED_ORIGINS=${{ secrets.SPRING_MVC_CORS_ALLOWED_ORIGINS }}
772+
SECURITY_JWT_ISSUER=${{ secrets.SECURITY_JWT_ISSUER }}
773+
SECURITY_JWT_EXPIRATION_MS=${{ secrets.SECURITY_JWT_EXPIRATION_MS }}
774+
SECURITY_JWT_SECRET=${{ secrets.SECURITY_JWT_SECRET }}
775+
SAMESITE_WHEN_SAME_SITE=${{ secrets.SAMESITE_WHEN_SAME_SITE }}
776+
SECURITY_CSRF_COOKIE_DOMAIN=${{ secrets.SECURITY_CSRF_COOKIE_DOMAIN }}
777+
SECURITY_CSRF_COOKIE_SAME_SITE=${{ secrets.SECURITY_CSRF_COOKIE_SAME_SITE }}
778+
MAIL_HOST=${{ secrets.MAIL_HOST }}
779+
MAIL_PORT=${{ secrets.MAIL_PORT }}
780+
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
781+
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
782+
FRONTEND_URL=${{ secrets.FRONTEND_URL }}
783+
ENVEOF
784+
785+
echo "Validating environment file..."
786+
if ! grep -q "^SPRING_DATASOURCE_URL=" .env.espacogeek; then
787+
echo "ERROR: SPRING_DATASOURCE_URL not found in .env.espacogeek"
788+
cat .env.espacogeek
789+
rm -f .env.espacogeek
790+
exit 1
791+
fi
792+
DATASOURCE_URL=$(grep "^SPRING_DATASOURCE_URL=" .env.espacogeek | cut -d'=' -f2)
793+
if [ -z "$DATASOURCE_URL" ]; then
794+
echo "ERROR: SPRING_DATASOURCE_URL is empty"
795+
rm -f .env.espacogeek
796+
exit 1
797+
fi
798+
if [[ ! "$DATASOURCE_URL" =~ ^jdbc: ]]; then
799+
echo "ERROR: SPRING_DATASOURCE_URL must start with 'jdbc:' but got: $DATASOURCE_URL"
800+
rm -f .env.espacogeek
801+
exit 1
802+
fi
803+
echo "✓ SPRING_DATASOURCE_URL is valid: $DATASOURCE_URL"
804+
805+
echo "${{ secrets.GHCR_TOKEN }}" | docker login ghcr.io -u "${{ secrets.GHCR_USER }}" --password-stdin
806+
807+
"$DEPLOY_SCRIPT" "${GHCR_OWNER_LC}" "${APP_NAME}" "native" ".env.espacogeek"
808+
809+
rm -f .env.espacogeek
810+
811+
docker logout ghcr.io
609812
610-
# Always clean up the validation container
611813
- name: Cleanup validation container
612814
if: always()
613815
run: docker rm -f native-validation 2>/dev/null || true

docker/deploy.sh

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@ on_error() {
5454
# Arguments
5555
GHCR_OWNER_LC="${1:-}"
5656
APP_NAME="${2:-}"
57-
IMAGE_TAG="${3:-latest}"
57+
IMAGE_TAG="${3:-native}"
5858
ENV_FILE="${4:-.env.espacogeek}"
5959
CONTAINER_NAME="espacogeek-api"
6060
BACKUP_DIR="${HOME}/espacogeek-api-backups"

docker/docker-compose.yml

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,19 @@
11
services:
2-
app-jvm:
3-
build:
4-
context: ..
5-
dockerfile: docker/Dockerfile.jvm
6-
image: espacogeek-jvm:latest
7-
container_name: espacogeek-jvm
2+
springapi:
3+
#image: espacogeek-jvm:latest
4+
image: espacogeek-api:native
5+
container_name: espacogeek-api
86
environment:
97
SPRING_DATASOURCE_URL: ${SPRING_DATASOURCE_URL}
108
SPRING_DATASOURCE_USERNAME: ${SPRING_DATASOURCE_USERNAME}
119
SPRING_DATASOURCE_PASSWORD: ${SPRING_DATASOURCE_PASSWORD}
12-
JAVA_OPTS: "-XX:InitialRAMPercentage=75.0 -XX:MaxRAMPercentage=75.0 -XX:+UseSerialGC"
10+
#JAVA_OPTS: "-XX:InitialRAMPercentage=75.0 -XX:MaxRAMPercentage=75.0 -XX:+UseSerialGC"
1311
SPRING_MVC_CORS_ALLOWED_ORIGINS: ${SPRING_MVC_CORS_ALLOWED_ORIGINS:http://localhost:3000}
1412
SECURITY_JWT_EXPIRATION_MS: ${SECURITY_JWT_EXPIRATION_MS:604800000}
1513
networks:
1614
- infra_network
1715
ports:
1816
- "8080:8080"
19-
- "5005:5005"
2017
healthcheck:
2118
test: ["CMD", "curl", "-f", "http://localhost:8081/actuator/health"]
2219
interval: 10s
@@ -27,12 +24,13 @@ services:
2724
resources:
2825
limits:
2926
cpus: '0.8'
30-
memory: '3500M'
27+
memory: '512M'
3128
reservations:
32-
memory: 2000M
29+
memory: 256M
3330
volumes:
3431
- /etc/localtime:/etc/localtime:ro
3532
- /etc/timezone:/etc/timezone:ro
3633

37-
infra_network:
34+
networks:
35+
infra_network:
3836
external: true

0 commit comments

Comments
 (0)