feat: migrate to GraalVM native image as default deployment#104
Conversation
Agent-Logs-Url: https://github.com/EspacoGeek-Teams/SpringAPI_EspacoGeek/sessions/a5b35835-6769-4a24-a315-612c044cde88 Co-authored-by: vitorhugo-java <65777252+vitorhugo-java@users.noreply.github.com>
There was a problem hiding this comment.
Pull request overview
Migrates the deployment pipeline to make the GraalVM native-image container the default deployment artifact, while preserving (but disabling) the JVM pipeline.
Changes:
- Disabled the JVM build/publish job and shifted default deployment to the native-image job.
- Updated native image publishing to a single
:nativetag and added SSH-based deployment for the native pipeline. - Updated Docker Compose service configuration for running the native image and corrected the top-level
networkskey; updated deploy script default tag tonative.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 6 comments.
| File | Description |
|---|---|
.github/workflows/cicd.yml |
Disables JVM pipeline, changes native image tagging/pushing, adds SSH deploy flow for native image. |
docker/docker-compose.yml |
Switches service to use the native image, updates resource limits/ports, fixes networks nesting. |
docker/deploy.sh |
Defaults standalone deploys to IMAGE_TAG=native. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| container_name: espacogeek-jvm | ||
| springapi: | ||
| #image: espacogeek-jvm:latest | ||
| image: espacogeek-api:native |
There was a problem hiding this comment.
image: espacogeek-api:native is an unqualified image reference, so Compose will try to pull from Docker Hub (or rely on a locally-tagged image). If the intent is to pull the native image from GHCR (as described in the PR), update this to a fully-qualified ghcr.io/<owner>/<app>:native reference (optionally via env vars) so deployments are deterministic.
| image: espacogeek-api:native | |
| image: ${SPRINGAPI_IMAGE:-ghcr.io/espacogeek/espacogeek-api:native} |
| - name: Tag and push native Docker image | ||
| if: steps.vars.outputs.should_push == 'true' && github.event_name == 'push' | ||
| shell: bash | ||
| run: | | ||
| set -euo pipefail | ||
| IFS=',' read -ra TAGS <<< "${{ steps.vars.outputs.tags_csv }}" | ||
| for tag in "${TAGS[@]}"; do | ||
| FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}-native:${tag}" | ||
| docker tag native-validation:ci "${FULL_TAG}" | ||
| docker push "${FULL_TAG}" | ||
| echo "Pushed: ${FULL_TAG}" | ||
| done | ||
| FULL_TAG="ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}:native" | ||
| docker tag native-validation:ci "${FULL_TAG}" | ||
| docker push "${FULL_TAG}" | ||
| echo "Pushed: ${FULL_TAG}" |
There was a problem hiding this comment.
The Tag and push native Docker image step only runs on push, but steps.vars.outputs.should_push is also true for workflow_dispatch. That means a manually-dispatched run will build native-validation:ci but never push :native, and the subsequent SSH deploy will pull whatever image is currently in GHCR (not the one just built). Consider either allowing this step to run on workflow_dispatch too, or adjusting should_push/deploy conditions so manual runs don't build without publishing.
| script_stop: false | ||
| script: | | ||
| cleanup_on_exit() { | ||
| echo "Running SSH-level cleanup..." | ||
| rm -f .env.espacogeek 2>/dev/null || true | ||
| docker logout ghcr.io 2>/dev/null || true | ||
| } | ||
| trap cleanup_on_exit EXIT INT TERM |
There was a problem hiding this comment.
This SSH deploy step sets script_stop: false and the script body doesn't use set -e/explicit error handling, so a failing deploy command can be followed by successful cleanup commands and still return exit code 0 (making the workflow appear green even though the deploy failed). To ensure deploy failures fail the workflow, enable script_stop: true and/or add set -euo pipefail at the start of the SSH script and guard cleanup commands with || true.
| cat > .env.espacogeek << ENVEOF | ||
| SPRING_DATASOURCE_URL=${{ secrets.SPRING_DATASOURCE_URL }} | ||
| SPRING_DATASOURCE_USERNAME=${{ secrets.SPRING_DATASOURCE_USERNAME }} | ||
| SPRING_DATASOURCE_PASSWORD=${{ secrets.SPRING_DATASOURCE_PASSWORD }} | ||
| SPRING_MVC_CORS_ALLOWED_ORIGINS=${{ secrets.SPRING_MVC_CORS_ALLOWED_ORIGINS }} | ||
| SECURITY_JWT_ISSUER=${{ secrets.SECURITY_JWT_ISSUER }} | ||
| SECURITY_JWT_EXPIRATION_MS=${{ secrets.SECURITY_JWT_EXPIRATION_MS }} | ||
| SECURITY_JWT_SECRET=${{ secrets.SECURITY_JWT_SECRET }} | ||
| SAMESITE_WHEN_SAME_SITE=${{ secrets.SAMESITE_WHEN_SAME_SITE }} | ||
| SECURITY_CSRF_COOKIE_DOMAIN=${{ secrets.SECURITY_CSRF_COOKIE_DOMAIN }} | ||
| SECURITY_CSRF_COOKIE_SAME_SITE=${{ secrets.SECURITY_CSRF_COOKIE_SAME_SITE }} | ||
| MAIL_HOST=${{ secrets.MAIL_HOST }} | ||
| MAIL_PORT=${{ secrets.MAIL_PORT }} | ||
| MAIL_USERNAME=${{ secrets.MAIL_USERNAME }} | ||
| MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }} | ||
| FRONTEND_URL=${{ secrets.FRONTEND_URL }} | ||
| ENVEOF |
There was a problem hiding this comment.
The .env.espacogeek file is written with production secrets but its permissions are not restricted. On multi-user hosts or with a permissive umask, this file may be readable by other users/processes. Set a restrictive umask (e.g. umask 077) before creating it and/or chmod 600 .env.espacogeek immediately after writing.
| echo "Validating environment file..." | ||
| if ! grep -q "^SPRING_DATASOURCE_URL=" .env.espacogeek; then | ||
| echo "ERROR: SPRING_DATASOURCE_URL not found in .env.espacogeek" | ||
| cat .env.espacogeek | ||
| rm -f .env.espacogeek | ||
| exit 1 | ||
| fi | ||
| DATASOURCE_URL=$(grep "^SPRING_DATASOURCE_URL=" .env.espacogeek | cut -d'=' -f2) | ||
| if [ -z "$DATASOURCE_URL" ]; then | ||
| echo "ERROR: SPRING_DATASOURCE_URL is empty" | ||
| rm -f .env.espacogeek | ||
| exit 1 | ||
| fi | ||
| if [[ ! "$DATASOURCE_URL" =~ ^jdbc: ]]; then | ||
| echo "ERROR: SPRING_DATASOURCE_URL must start with 'jdbc:' but got: $DATASOURCE_URL" | ||
| rm -f .env.espacogeek | ||
| exit 1 | ||
| fi | ||
| echo "✓ SPRING_DATASOURCE_URL is valid: $DATASOURCE_URL" | ||
|
|
There was a problem hiding this comment.
This validation block can leak secrets into GitHub Actions logs: it cats the full .env.espacogeek on error and also echoes the datasource URL value. Since the env file contains DB credentials/JWT secrets, remove the cat and avoid printing secret values (log only that a key is present/has correct prefix, not the value).
| CONTAINER_NAME="espacogeek-api" | ||
| BACKUP_DIR="${HOME}/espacogeek-api-backups" | ||
| OLD_CONTAINER_BACKUP="${CONTAINER_NAME}-old" | ||
| TIMESTAMP=$(date +%Y%m%d_%H%M%S) | ||
|
|
||
| cleanup_env_file() { | ||
| if [ -f "$ENV_FILE" ]; then | ||
| log_info "Cleaning up environment file..." | ||
| rm -f "$ENV_FILE" | ||
| fi | ||
| } | ||
| trap cleanup_env_file EXIT | ||
|
|
||
| if [ -z "$GHCR_OWNER_LC" ] || [ -z "$APP_NAME" ]; then | ||
| log_error "Usage: $0 <GHCR_OWNER_LC> <APP_NAME> [IMAGE_TAG] [ENV_FILE]" | ||
| exit 1 | ||
| fi | ||
|
|
||
| IMAGE="ghcr.io/${GHCR_OWNER_LC}/${APP_NAME}:${IMAGE_TAG}" | ||
|
|
There was a problem hiding this comment.
The workflow now embeds a full deploy script via heredoc even though docker/deploy.sh exists in the repo and is also being modified in this PR. Keeping two different deploy implementations (with different health/rollback behavior) increases the risk of drift. Consider reusing the repo script (e.g., copy it to the host and execute it) or sourcing a single canonical script for both JVM and native deployments.
| CONTAINER_NAME="espacogeek-api" | |
| BACKUP_DIR="${HOME}/espacogeek-api-backups" | |
| OLD_CONTAINER_BACKUP="${CONTAINER_NAME}-old" | |
| TIMESTAMP=$(date +%Y%m%d_%H%M%S) | |
| cleanup_env_file() { | |
| if [ -f "$ENV_FILE" ]; then | |
| log_info "Cleaning up environment file..." | |
| rm -f "$ENV_FILE" | |
| fi | |
| } | |
| trap cleanup_env_file EXIT | |
| if [ -z "$GHCR_OWNER_LC" ] || [ -z "$APP_NAME" ]; then | |
| log_error "Usage: $0 <GHCR_OWNER_LC> <APP_NAME> [IMAGE_TAG] [ENV_FILE]" | |
| exit 1 | |
| fi | |
| IMAGE="ghcr.io/${GHCR_OWNER_LC}/${APP_NAME}:${IMAGE_TAG}" | |
| DEPLOY_SCRIPT="docker/deploy.sh" | |
| if [ -z "$GHCR_OWNER_LC" ] || [ -z "$APP_NAME" ]; then | |
| log_error "Usage: $0 <GHCR_OWNER_LC> <APP_NAME> [IMAGE_TAG] [ENV_FILE]" | |
| exit 1 | |
| fi | |
| if [ ! -f "$DEPLOY_SCRIPT" ]; then | |
| log_error "Canonical deploy script not found: $DEPLOY_SCRIPT" | |
| exit 1 | |
| fi | |
| chmod +x "$DEPLOY_SCRIPT" | |
| log_info "Delegating deployment to ${DEPLOY_SCRIPT}..." | |
| exec bash "$DEPLOY_SCRIPT" "$GHCR_OWNER_LC" "$APP_NAME" "$IMAGE_TAG" "$ENV_FILE" |
Summary
Migrates the EspacoGeek deployment pipeline to use GraalVM native image as the active default. The existing JVM pipeline is preserved but disabled so it can be re-enabled easily.
Changes
.github/workflows/cicd.ymlbuild_and_publish(JVM) job withif: false— fully preserved, just skippedbuild_native_and_publishjob's push step to tag and push the image asghcr.io/<owner>/espacogeek-api:native(single:nativetag instead of multi-tag with-nativeimage-name suffix)"native"as the image tagdocker/docker-compose.ymlapp-jvmtospringapibuild:block (native image is pulled from GHCR, not built locally)image:toespacogeek-api:native; the previousespacogeek-jvm:latestreference is kept commented out directly above itJAVA_OPTS(not applicable to native images)5005:5005networks:top-level key (was missing, causinginfra_networkto appear at the wrong YAML level)docker/deploy.shIMAGE_TAGfromlatesttonativeso standalone invocations default to the native imageespacogeek-api-backups(consistent with the embedded script in the CI workflow)