Skip to content

CI/CD

CI/CD #5

Workflow file for this run

name: CI/CD
on:
# Runs on PRs to master and on pushes (merge) to master
pull_request:
branches: [ "master" ]
push:
branches: [ "master" ]
workflow_dispatch:
# Minimal permissions + permission to publish packages to GHCR
permissions:
contents: read
packages: write
concurrency:
group: ci-${{ github.ref }}
cancel-in-progress: true
jobs:
tests:
name: Tests
runs-on: ubuntu-latest
steps:
# Checkout code
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0 # better for versioning (tags/commits)
# Set up Java 21 (Temurin) for tests
- name: Set up Java 21 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# Set up Gradle cache automatically
- name: Setup Gradle (with caching)
uses: gradle/actions/setup-gradle@v3
# Ensure wrapper has execute permission
- name: Make Gradle wrapper executable
run: chmod +x gradlew
# Run test suite
- name: Run unit tests
run: ./gradlew --no-daemon clean test
# Upload test reports always, even on failure
- name: Upload test reports (always)
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
build/test-results/test
build/reports/tests/test
build_and_publish:
name: Build, Docker and Push
runs-on: ubuntu-latest
needs: tests # Only runs if tests pass
if: ${{ needs.tests.result == 'success' }}
env:
# Base image name (adjust as needed)
APP_NAME: espacogeek-backend
# .env variables (use Secrets in GitHub Actions)
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 }}
ALLOWED_ORIGINS: ${{ secrets.ALLOWED_ORIGINS }}
MAIL_HOST: ${{ secrets.MAIL_HOST }}
MAIL_PORT: ${{ secrets.MAIL_PORT }}
MAIL_USERNAME: ${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD: ${{ secrets.MAIL_PASSWORD }}
APP_NAME_ENV: ${{ secrets.APP_NAME }}
FRONTEND_URL: ${{ secrets.FRONTEND_URL }}
steps:
# Checkout code (with full history for versioning)
- name: Check out code
uses: actions/checkout@v4
with:
fetch-depth: 0
# Normalize owner/repo to lowercase (GHCR requirement)
- name: Normalize GHCR owner/repo
shell: bash
run: |
OWNER_LC="${GITHUB_REPOSITORY_OWNER,,}"
REPO_LC="${GITHUB_REPOSITORY#*/}"
REPO_LC="${REPO_LC,,}"
echo "GHCR_OWNER_LC=${OWNER_LC}" >> "$GITHUB_ENV"
echo "GHCR_REPO_LC=${REPO_LC}" >> "$GITHUB_ENV"
# Set up Java 21 (Temurin) for WAR build
- name: Set up Java 21 (Temurin)
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: '21'
# Set up Gradle cache for builds
- name: Setup Gradle (with caching)
uses: gradle/actions/setup-gradle@v3
# Ensure wrapper has execute permission (Linux runners)
- name: Make Gradle wrapper executable
run: chmod +x gradlew
# Read the project version defined in build.gradle (project.version)
- name: Read Gradle project version
id: gradle_version
shell: bash
run: |
echo "version=$(./gradlew -q properties | sed -n 's/^version: \(.*\)$/\1/p')" >> "$GITHUB_OUTPUT"
# Generate version tags for Docker images and artifacts
# - On PR: pr-<number>-<short-sha>, v<version>-pr-<number>
# - On push to master: latest, sha-<short-sha>, <yyyyMMdd>, v<version>
- name: Compute version tags
id: vars
shell: bash
run: |
set -euo pipefail
SHORT_SHA="$(git rev-parse --short HEAD)"
DATE_TAG="$(date +%Y%m%d)"
VERSION="${{ steps.gradle_version.outputs.version }}"
if [[ "${GITHUB_EVENT_NAME}" == "pull_request" ]]; then
PR_NUMBER="${{ github.event.pull_request.number }}"
TAGS="pr-${PR_NUMBER}-${SHORT_SHA},v${VERSION}-pr-${PR_NUMBER}"
SHOULD_PUSH="false" # do not push on PRs by default
else
# push to master
# Includes the 'last-release' tag (Docker does not allow spaces in tags)
TAGS="latest,last-release,sha-${SHORT_SHA},${DATE_TAG},v${VERSION}"
SHOULD_PUSH="true"
fi
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
echo "date_tag=${DATE_TAG}" >> "$GITHUB_OUTPUT"
echo "tags_csv=${TAGS}" >> "$GITHUB_OUTPUT"
echo "should_push=${SHOULD_PUSH}" >> "$GITHUB_OUTPUT"
# Set up Docker Buildx and layer cache
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
# Log in to GHCR (uses GITHUB_TOKEN)
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
# Normalize the computed tags to a multiline format for docker/buildx
- name: Prepare tag list (multiline)
id: tags
shell: bash
run: |
IFS=',' read -ra T <<< "${{ steps.vars.outputs.tags_csv }}"
printf "jvm_tags<<EOF\n" >> "$GITHUB_OUTPUT"
for tag in "${T[@]}"; do
# Use APP_NAME as the JVM image name
echo "ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}:$tag" >> "$GITHUB_OUTPUT"
done
printf "EOF\n" >> "$GITHUB_OUTPUT"
printf "native_tags<<EOF\n" >> "$GITHUB_OUTPUT"
for tag in "${T[@]}"; do
# Use '-native' suffix to differentiate native image (adjust if needed)
echo "ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}-native:$tag" >> "$GITHUB_OUTPUT"
done
printf "EOF\n" >> "$GITHUB_OUTPUT"
# Build Docker image for the JVM app using the existing Dockerfile
- name: Build JVM Docker image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.jvm
platforms: linux/amd64
push: ${{ steps.vars.outputs.should_push == 'true' && github.event_name == 'push' }}
tags: ${{ steps.tags.outputs.jvm_tags }}
labels: |
org.opencontainers.image.version=${{ steps.gradle_version.outputs.version }}
org.opencontainers.image.revision=${{ steps.vars.outputs.short_sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Build Docker image for the app as a Native Image (uses existing Dockerfile)
- name: Build Native Docker image
uses: docker/build-push-action@v6
with:
context: .
file: docker/Dockerfile.native
platforms: linux/amd64
push: ${{ steps.vars.outputs.should_push == 'true' && github.event_name == 'push' }}
tags: ${{ steps.tags.outputs.native_tags }}
labels: |
org.opencontainers.image.version=${{ steps.gradle_version.outputs.version }}
org.opencontainers.image.revision=${{ steps.vars.outputs.short_sha }}
org.opencontainers.image.source=https://github.com/${{ github.repository }}
cache-from: type=gha
cache-to: type=gha,mode=max
# Deploy to Hostinger via SSH (expects secrets: HOSTINGER (private key), HOSTINGER_HOST, HOSTINGER_USER, optional HOSTINGER_PORT)
- name: Deploy to Hostinger via SSH
uses: appleboy/ssh-action@v0.1.9
if: ${{ needs.tests.result == 'success' && steps.vars.outputs.should_push == 'true' && github.event_name == 'push' }}
with:
host: ${{ secrets.HOSTINGER_HOST }}
username: ${{ secrets.HOSTINGER_USER }}
key: ${{ secrets.HOSTINGER }}
port: ${{ secrets.HOSTINGER_PORT }}
script: |
set -e
docker pull
MAIL_USERNAME=${{ secrets.MAIL_USERNAME }}
MAIL_PASSWORD=${{ secrets.MAIL_PASSWORD }}
APP_NAME=${{ secrets.APP_NAME }}
FRONTEND_URL=${{ secrets.FRONTEND_URL }}
EOF
docker run -d --name espacogeek \
-p 8080:8080 \
--restart unless-stopped \
--env-file .env.espacogeek \
ghcr.io/${{ env.GHCR_OWNER_LC }}/${{ env.APP_NAME }}:latest
# (Optional) remove file after starting the container (keeps it only in the container)
rm -f .env.espacogeek
# Final summary with generated tags
- name: Summary
if: always()
run: |
echo "Project version: ${{ steps.gradle_version.outputs.version }}"
echo "Tags generated: ${{ steps.vars.outputs.tags_csv }}"
echo "Push enabled: ${{ steps.vars.outputs.should_push }}"