From 95215b6a420fd526df1aa395f9b087556c8ad03b Mon Sep 17 00:00:00 2001 From: Janis Eccarius Date: Mon, 15 Jun 2026 22:30:31 +0200 Subject: [PATCH] feat(analytics): add Dockerfile, CI workflow, and stable jar name for K8s deployment - Pin jar output to analytics.jar (no version suffix) so Dockerfile COPY is stable - Add Dockerfile based on apache/spark:3.5.4-scala2.13-java17-ubuntu - Add versions.env (0.1.0) matching GitOps overlay image tag - Add analytics-image.yml CI workflow following native-image.yml conventions Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/analytics-image.yml | 130 +++++++++++++++++++ modules/analytics/build.gradle.kts | 4 + modules/analytics/src/main/docker/Dockerfile | 9 ++ modules/analytics/versions.env | 3 + 4 files changed, 146 insertions(+) create mode 100644 .github/workflows/analytics-image.yml create mode 100644 modules/analytics/src/main/docker/Dockerfile create mode 100644 modules/analytics/versions.env diff --git a/.github/workflows/analytics-image.yml b/.github/workflows/analytics-image.yml new file mode 100644 index 0000000..e30992c --- /dev/null +++ b/.github/workflows/analytics-image.yml @@ -0,0 +1,130 @@ +name: Build & Push Analytics Image + +on: + push: + branches: + - main + paths: + - 'modules/analytics/**' + workflow_dispatch: + +jobs: + check-actor: + runs-on: ubuntu-latest + outputs: + allowed: ${{ steps.check.outputs.allowed }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - id: check + run: | + if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then + echo "Triggered manually — allowing build" + echo "allowed=true" >> "$GITHUB_OUTPUT" + else + COMMIT_AUTHOR=$(git log -1 --format='%an') + COMMIT_SHA=$(git log -1 --format='%H') + COMMIT_MSG=$(git log -1 --format='%s') + echo "Commit: ${COMMIT_SHA}" + echo "Author: ${COMMIT_AUTHOR}" + echo "Message: ${COMMIT_MSG}" + if [[ "$COMMIT_AUTHOR" == "TeamCity" ]]; then + echo "Author is TeamCity — allowing build" + echo "allowed=true" >> "$GITHUB_OUTPUT" + else + echo "Author is not TeamCity — skipping build" + echo "allowed=false" >> "$GITHUB_OUTPUT" + fi + fi + + build-and-push: + needs: check-actor + if: needs.check-actor.outputs.allowed == 'true' + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - uses: actions/checkout@v4 + + - name: Read version from versions.env + id: version + run: | + source modules/analytics/versions.env + echo "version=${MAJOR}.${MINOR}.${PATCH}" >> "$GITHUB_OUTPUT" + + - name: Check if image exists in GHCR + id: image-check + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + PACKAGE="now-chess-systems%2Fanalytics" + VERSION="${{ steps.version.outputs.version }}" + EXISTING_TAGS=$(gh api "orgs/now-chess/packages/container/${PACKAGE}/versions" \ + --jq '.[].metadata.container.tags[]' 2>/dev/null || echo "") + echo "Existing tags: $(echo "${EXISTING_TAGS}" | tr '\n' ' ' | xargs)" + if echo "${EXISTING_TAGS}" | grep -qx "${VERSION}"; then + echo "Image ${VERSION} already exists — skipping build" + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "Image ${VERSION} not found — will build" + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Set up JDK 17 + if: steps.image-check.outputs.exists == 'false' + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + + - name: Cache Gradle packages + if: steps.image-check.outputs.exists == 'false' + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: gradle-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: gradle-${{ runner.os }}- + + - name: Build fat jar + if: steps.image-check.outputs.exists == 'false' + run: ./gradlew :modules:analytics:jar --no-daemon + + - name: Set up Docker Buildx + if: steps.image-check.outputs.exists == 'false' + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + if: steps.image-check.outputs.exists == 'false' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + if: steps.image-check.outputs.exists == 'false' + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/now-chess/now-chess-systems/analytics + tags: | + type=raw,value=${{ steps.version.outputs.version }} + type=raw,value=latest + + - name: Build and push + if: steps.image-check.outputs.exists == 'false' + uses: docker/build-push-action@v6 + with: + context: modules/analytics + file: modules/analytics/src/main/docker/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/modules/analytics/build.gradle.kts b/modules/analytics/build.gradle.kts index 88219a7..94cbeea 100644 --- a/modules/analytics/build.gradle.kts +++ b/modules/analytics/build.gradle.kts @@ -79,7 +79,11 @@ application { // Fat jar: includes runtimeClasspath (our code + pg driver + scala3-library) // but NOT compileOnly Spark jars. +// archiveVersion is cleared so the output is always "analytics.jar" — stable +// name required by the Dockerfile COPY instruction. tasks.jar { + archiveBaseName.set("analytics") + archiveVersion.set("") manifest { attributes["Main-Class"] = "de.nowchess.analytics.OpeningBookJob" } diff --git a/modules/analytics/src/main/docker/Dockerfile b/modules/analytics/src/main/docker/Dockerfile new file mode 100644 index 0000000..6f5590a --- /dev/null +++ b/modules/analytics/src/main/docker/Dockerfile @@ -0,0 +1,9 @@ +FROM apache/spark:3.5.4-scala2.13-java17-ubuntu + +USER root + +# analytics.jar = fat jar containing app code + PostgreSQL JDBC driver + Scala 3 runtime. +# Spark itself is provided by the base image at /opt/spark — it is NOT included in the jar. +COPY build/libs/analytics.jar /app/analytics.jar + +USER spark diff --git a/modules/analytics/versions.env b/modules/analytics/versions.env new file mode 100644 index 0000000..0e288f2 --- /dev/null +++ b/modules/analytics/versions.env @@ -0,0 +1,3 @@ +MAJOR=0 +MINOR=1 +PATCH=0