name: Build, Tag, Push, and Release to GitHub Container Registry on: push: branches: - 'master' - 'develop' pull_request: branches: - '**' jobs: check-for-migrations: name: Check for DB Migrations if: github.event_name == 'pull_request' && (github.base_ref == 'master' || github.base_ref == 'develop') runs-on: ubuntu-latest outputs: has_migrations: ${{ steps.check_migrations.outputs.has_migrations }} steps: - name: Checkout Repository for Diff uses: actions/checkout@v6 with: fetch-depth: 0 - name: Detect Flyway Migration Changes id: check_migrations run: | # Compare PR head with the target base branch if git diff --name-only origin/${{ github.base_ref }}...HEAD | grep -q "booklore-api/src/main/resources/db/migration/V.*.sql"; then echo "Migration file changes detected. Proceeding with migration preview." echo "has_migrations=true" >> $GITHUB_OUTPUT else echo "No migration file changes detected. Skipping migration preview." echo "has_migrations=false" >> $GITHUB_OUTPUT fi flyway-migration-preview: name: Flyway DB Migration Preview needs: [check-for-migrations] if: needs.check-for-migrations.outputs.has_migrations == 'true' runs-on: ubuntu-latest services: mariadb: image: mariadb:10.6 env: MYSQL_ROOT_PASSWORD: root MYSQL_DATABASE: booklore_test ports: - 3306:3306 options: >- --health-cmd="mysqladmin ping --silent" --health-interval=5s --health-timeout=5s --health-retries=10 steps: - name: Checkout Base Branch uses: actions/checkout@v6 with: ref: '${{ github.base_ref }}' - name: Apply Migrations from Base Branch run: | echo "Applying migrations from '${{ github.base_ref }}' branch..." docker run --network host \ -v ${{ github.workspace }}:/flyway/sql \ flyway/flyway:11.19.0-alpine \ -url=jdbc:mariadb://127.0.0.1:3306/booklore_test \ -user=root -password=root \ -locations=filesystem:/flyway/sql/booklore-api/src/main/resources/db/migration \ migrate - name: Checkout Pull Request Branch uses: actions/checkout@v6 - name: Apply Migrations from PR Branch run: | echo "Applying new migrations from PR branch..." docker run --network host \ -v ${{ github.workspace }}:/flyway/sql \ flyway/flyway:11.19.0-alpine \ -url=jdbc:mariadb://127.0.0.1:3306/booklore_test \ -user=root -password=root \ -locations=filesystem:/flyway/sql/booklore-api/src/main/resources/db/migration \ migrate - name: Confirm Flyway Dry Run Success run: echo "✅ Flyway migration preview successful. Migrations can be applied cleanly." build-and-push: needs: [check-for-migrations, flyway-migration-preview] if: always() && (needs.flyway-migration-preview.result == 'success' || needs.flyway-migration-preview.result == 'skipped') runs-on: ubuntu-latest permissions: contents: write packages: write issues: read checks: write pull-requests: write steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Authenticate to Docker Hub if: github.event_name == 'push' uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Authenticate to GitHub Container Registry if: github.event_name == 'push' uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} password: ${{ github.token }} - name: Set Up QEMU for Multi-Architecture Builds uses: docker/setup-qemu-action@v3 - name: Set Up Docker Buildx uses: docker/setup-buildx-action@v3 - name: Set Up JDK 21 uses: actions/setup-java@v5 with: java-version: '21' distribution: 'temurin' cache: 'gradle' - name: Execute Backend Tests id: backend_tests working-directory: ./booklore-api run: | echo "Running backend tests with testcontainers..." ./gradlew test --no-daemon --parallel --build-cache continue-on-error: true - name: Publish Backend Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: files: booklore-api/build/test-results/**/*.xml check_name: Backend Test Results comment_title: Backend Test Results report_individual_runs: true report_suite_logs: 'any' - name: Upload Backend Test Reports uses: actions/upload-artifact@v6 if: always() with: name: test-reports path: | booklore-api/build/reports/tests/ booklore-api/build/test-results/ retention-days: 30 - name: Validate Backend Test Results if: steps.backend_tests.outcome == 'failure' run: | echo "❌ Backend tests failed! Check the test results above." exit 1 - name: Retrieve Latest Master Version Tag id: get_version run: | latest_tag=$(git tag --list "v*" --sort=-v:refname | head -n 1) latest_tag=${latest_tag:-"v0.0.0"} echo "latest_tag=$latest_tag" >> $GITHUB_ENV echo "Latest master tag: $latest_tag" - name: Determine Version Bump (Master Only) if: github.ref == 'refs/heads/master' id: determine_bump env: GH_TOKEN: ${{ github.token }} run: | echo "Determining version bump from PR labels..." # Extract PR number from merge commit pr_number=$(git log -1 --pretty=%B | grep -oE 'Merge pull request #[0-9]+' | grep -oE '[0-9]+') || true if [ -z "$pr_number" ]; then pr_number=$(gh pr list --state merged --base master --limit 1 --json number --jq '.[0].number') fi echo "PR number: $pr_number" labels=$(gh pr view "$pr_number" --json labels --jq '.labels[].name' || echo "") echo "PR labels: $labels" if echo "$labels" | grep -q 'bump:major'; then bump="major" elif echo "$labels" | grep -q 'bump:minor'; then bump="minor" elif echo "$labels" | grep -q 'bump:patch'; then bump="patch" else last_commit_msg=$(git log -1 --pretty=%B) if echo "$last_commit_msg" | grep -iq '#major'; then bump="major" elif echo "$last_commit_msg" | grep -iq '#minor'; then bump="minor" elif echo "$last_commit_msg" | grep -iq '#patch'; then bump="patch" else bump="patch" fi fi # Calculate next version semver=$(echo ${{ env.latest_tag }} | sed 's/^v//') major=$(echo $semver | cut -d. -f1) minor=$(echo $semver | cut -d. -f2) patch=$(echo $semver | cut -d. -f3) case "$bump" in major) major=$((major+1)); minor=0; patch=0 ;; minor) minor=$((minor+1)); patch=0 ;; patch) patch=$((patch+1)) ;; esac next_version="v${major}.${minor}.${patch}" echo "Version bump type: $bump" echo "Next version: $next_version" echo "bump=$bump" >> $GITHUB_ENV echo "new_tag=$next_version" >> $GITHUB_ENV - name: Generate Image Tag id: set_image_tag run: | branch="${GITHUB_REF#refs/heads/}" if [[ "$branch" == "master" ]]; then image_tag="${{ env.new_tag }}" elif [[ "$branch" == "develop" ]]; then short_sha=$(git rev-parse --short HEAD) image_tag="${{ env.latest_tag }}-develop-${short_sha}" else short_sha=$(git rev-parse --short HEAD) image_tag="${short_sha}" fi echo "image_tag=$image_tag" >> $GITHUB_ENV echo "Image tag: $image_tag" - name: Build and push Docker image if: github.event_name == 'push' uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | booklore/booklore:${{ env.image_tag }} ghcr.io/booklore-app/booklore:${{ env.image_tag }} build-args: | APP_VERSION=${{ env.image_tag }} APP_REVISION=${{ github.sha }} cache-from: | type=gha type=registry,ref=ghcr.io/booklore-app/booklore:buildcache cache-to: | type=gha,mode=max type=registry,ref=ghcr.io/booklore-app/booklore:buildcache,mode=max - name: Push Latest Tag (Master Only) if: github.ref == 'refs/heads/master' && github.event_name == 'push' uses: docker/build-push-action@v6 with: context: . platforms: linux/amd64,linux/arm64 push: true tags: | booklore/booklore:latest booklore/booklore:${{ env.new_tag }} ghcr.io/booklore-app/booklore:latest ghcr.io/booklore-app/booklore:${{ env.new_tag }} build-args: | APP_VERSION=${{ env.new_tag }} APP_REVISION=${{ github.sha }} cache-from: type=gha - name: Update GitHub Release Draft (Master Only) if: github.ref == 'refs/heads/master' uses: release-drafter/release-drafter@v6 with: tag: ${{ env.new_tag }} name: "Release ${{ env.new_tag }}" env: GITHUB_TOKEN: ${{ github.token }} - name: Publish GitHub Draft Release (Master Only) if: github.ref == 'refs/heads/master' env: GITHUB_TOKEN: ${{ github.token }} run: | gh release edit ${{ env.new_tag }} --draft=true