name: Master - Build, Tag, Push, and Release on: push: branches: - 'master' jobs: get-base-ref: name: Get Base Ref runs-on: ubuntu-latest outputs: base_ref: ${{ steps.get_base.outputs.base_ref }} steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 2 - name: Get Base Ref id: get_base run: echo "base_ref=$(git rev-parse HEAD~1)" >> $GITHUB_OUTPUT migration-check: name: Flyway Migration Check on Master needs: [ get-base-ref ] uses: ./.github/workflows/migrations-check.yml with: base_ref: ${{ needs.get-base-ref.outputs.base_ref }} head_ref: ${{ github.sha }} backend-tests: name: Backend Tests needs: [ migration-check ] if: needs.migration-check.result == 'success' || needs.migration-check.result == 'skipped' runs-on: ubuntu-latest permissions: contents: read checks: write pull-requests: write steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Set Up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'temurin' cache: gradle - name: Execute Backend Tests id: backend_tests working-directory: ./booklore-api run: | echo "Running backend tests..." ./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 - name: Upload Backend Test Reports uses: actions/upload-artifact@v6 if: always() with: name: backend-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" exit 1 frontend-tests: name: Frontend Tests needs: [ migration-check ] if: needs.migration-check.result == 'success' || needs.migration-check.result == 'skipped' runs-on: ubuntu-latest permissions: contents: read checks: write pull-requests: write steps: - name: Checkout Repository uses: actions/checkout@v6 - name: Set Up Node.js uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' cache-dependency-path: booklore-ui/package-lock.json - name: Install Frontend Dependencies working-directory: ./booklore-ui run: npm ci --force - name: Execute Frontend Tests id: frontend_tests working-directory: ./booklore-ui run: | echo "Running frontend tests..." npx ng test continue-on-error: true - name: Publish Frontend Test Results uses: EnricoMi/publish-unit-test-result-action@v2 if: always() with: files: booklore-ui/test-results/vitest-results.xml check_name: Frontend Test Results - name: Upload Frontend Test Reports uses: actions/upload-artifact@v6 if: always() with: name: frontend-test-reports path: | booklore-ui/test-results/vitest-results.xml retention-days: 30 - name: Validate Frontend Test Results if: steps.frontend_tests.outcome == 'failure' run: | echo "❌ Frontend tests failed" exit 1 build-and-release: needs: [ backend-tests, frontend-tests ] if: needs.backend-tests.result == 'success' && needs.frontend-tests.result == 'success' runs-on: ubuntu-latest permissions: contents: write packages: write pull-requests: read steps: - name: Checkout Repository uses: actions/checkout@v6 with: fetch-depth: 0 - name: Authenticate to Docker Hub uses: docker/login-action@v3 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - name: Authenticate to GitHub Container Registry 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: 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 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 # ---------------------------------------- # Native Angular build # ---------------------------------------- - name: Set Up Node.js uses: actions/setup-node@v6 with: node-version: '22' cache: 'npm' cache-dependency-path: booklore-ui/package-lock.json - name: Install Frontend Dependencies working-directory: ./booklore-ui run: npm ci --force - name: Build Angular App working-directory: ./booklore-ui run: npx ng build --configuration=production # ---------------------------------------- # Native Gradle build # ---------------------------------------- - name: Set Up JDK 25 uses: actions/setup-java@v5 with: java-version: '25' distribution: 'temurin' cache: gradle - name: Copy Angular Dist to Spring Boot Resources run: cp -r booklore-ui/dist/booklore/browser/. booklore-api/src/main/resources/static/ - name: Inject Version into application.yaml env: APP_VERSION: ${{ env.new_tag }} run: | sudo wget -qO /usr/local/bin/yq https://github.com/mikefarah/yq/releases/download/v4.52.2/yq_linux_amd64 sudo chmod +x /usr/local/bin/yq yq eval '.app.version = strenv(APP_VERSION)' -i booklore-api/src/main/resources/application.yaml - name: Build Spring Boot JAR working-directory: ./booklore-api run: ./gradlew clean build -x test --no-daemon --parallel --build-cache # ---------------------------------------- # Docker build & push # ---------------------------------------- - name: Build and push Docker image uses: docker/build-push-action@v6 with: context: . file: Dockerfile.ci platforms: linux/amd64,linux/arm64 push: true tags: | booklore/booklore:${{ env.new_tag }} booklore/booklore:latest ghcr.io/booklore-app/booklore:${{ env.new_tag }} ghcr.io/booklore-app/booklore:latest build-args: | APP_VERSION=${{ env.new_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: Update GitHub Release Draft 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 env: GITHUB_TOKEN: ${{ github.token }} run: | gh release edit ${{ env.new_tag }} --draft=true