From 88aae0101f7e359a939e7d253dbf021e18174635 Mon Sep 17 00:00:00 2001 From: lobo Date: Sun, 27 Sep 2026 10:27:01 +0200 Subject: [PATCH] feat(version): answer the build commit and time GET /api/version is public. CI passes GIT_SHA and BUILD_TIME as Docker build args; a local build answers dev. --- .gitea/workflows/publish-core.yml | 4 ++++ Dockerfile | 5 ++++ readme.md | 3 +++ .../security/SecurityConfig.java | 3 ++- .../version/api/VersionController.java | 22 +++++++++++++++++ .../modules/version/api/dto/VersionDto.java | 7 ++++++ src/main/resources/application.yml | 4 ++++ .../security/PublicEndpointsTest.java | 7 ++++++ .../version/api/VersionControllerTest.java | 24 +++++++++++++++++++ 9 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 src/main/java/core_service/modules/version/api/VersionController.java create mode 100644 src/main/java/core_service/modules/version/api/dto/VersionDto.java create mode 100644 src/test/java/core_service/modules/version/api/VersionControllerTest.java diff --git a/.gitea/workflows/publish-core.yml b/.gitea/workflows/publish-core.yml index 765a8ce..5cb49ad 100644 --- a/.gitea/workflows/publish-core.yml +++ b/.gitea/workflows/publish-core.yml @@ -52,6 +52,7 @@ jobs: BRANCH="$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]')" echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "sha=${GITHUB_SHA::12}" >> "$GITHUB_OUTPUT" + echo "build_time=$(date -u +%Y-%m-%dT%H:%M:%SZ)" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx uses: docker/setup-buildx-action@v3 @@ -69,6 +70,9 @@ jobs: with: context: . push: true + build-args: | + GIT_SHA=${{ github.sha }} + BUILD_TIME=${{ steps.prep.outputs.build_time }} tags: | git.andreas-dahm.eu/lobo_corp/tbp-core-service:${{ steps.prep.outputs.branch }} git.andreas-dahm.eu/lobo_corp/tbp-core-service:${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }} diff --git a/Dockerfile b/Dockerfile index 5b79e05..d086b1a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -25,6 +25,11 @@ FROM eclipse-temurin:25-jre-alpine WORKDIR /app +# Passed by the CI workflow; shown by GET /api/version +ARG GIT_SHA=dev +ARG BUILD_TIME= +ENV APP_BUILD_COMMIT=$GIT_SHA APP_BUILD_TIME=$BUILD_TIME + # Expose the port your Spring Boot app runs on EXPOSE 8090 diff --git a/readme.md b/readme.md index b7ea9b1..0a11d02 100644 --- a/readme.md +++ b/readme.md @@ -93,6 +93,7 @@ accepted only by `GET /api/public/profiles/library` in the `X-Profile-Ticket` he | `GET` | `/api/library/sync-status`, `/summary`, `/activity`, `/wishlist`, `/steam-profile`, `/games-count` | JWT | | `GET` | `/api/public/profiles/library` | profile ticket | | `GET` | `/api/stats/public` | public | +| `GET` | `/api/version` (`{commit, builtAt}` of the running image, from the CI build; `dev` locally) | public | ## Tech stack @@ -118,6 +119,7 @@ flowchart LR LibApi["api"] --> LibService["service"] --> LibRepo["repo"] end Stats["stats"] + Version["version"] Steam["steam_adapter"] Igdb["igdb_adapter"] Speedrun["speedrun_adapter"] @@ -140,6 +142,7 @@ flowchart LR |---|---| | `library` | Sync, backlog, manual games, playtime history, wishlist, achievements, public profile data | | `stats` | Public numbers for the landing page | +| `version` | `GET /api/version`: commit and build time of the running image | | `steam_adapter` | Steam Web API client, availability check of Steam CDN pictures | | `igdb_adapter` | IGDB client (Twitch OAuth): time to beat, game tags for moods, release years, game search | | `speedrun_adapter` | speedrun.com client: game search by name, verified runs | diff --git a/src/main/java/core_service/configuration/security/SecurityConfig.java b/src/main/java/core_service/configuration/security/SecurityConfig.java index c40d4a4..95d8989 100644 --- a/src/main/java/core_service/configuration/security/SecurityConfig.java +++ b/src/main/java/core_service/configuration/security/SecurityConfig.java @@ -32,7 +32,8 @@ public class SecurityConfig { "/actuator/health", // Docker health check; details are off "/error", "/api/stats/public", - "/api/public/**" // Public profiles; PublicProfileController checks the ticket + "/api/public/**", // Public profiles; PublicProfileController checks the ticket + "/api/version" // Build of the running image, shown in the settings }; private final OncePerRequestFilter jwtAuthFilter; diff --git a/src/main/java/core_service/modules/version/api/VersionController.java b/src/main/java/core_service/modules/version/api/VersionController.java new file mode 100644 index 0000000..1534260 --- /dev/null +++ b/src/main/java/core_service/modules/version/api/VersionController.java @@ -0,0 +1,22 @@ +package core_service.modules.version.api; + +import core_service.modules.version.api.dto.VersionDto; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +public class VersionController { + + private final VersionDto version; + + public VersionController(@Value("${app.build.commit}") String commit, @Value("${app.build.time}") String builtAt) { + this.version = new VersionDto(commit.isBlank() ? "dev" : commit, builtAt.isBlank() ? null : builtAt); + } + + @GetMapping("/api/version") + public ResponseEntity getVersion() { + return ResponseEntity.ok(version); + } +} diff --git a/src/main/java/core_service/modules/version/api/dto/VersionDto.java b/src/main/java/core_service/modules/version/api/dto/VersionDto.java new file mode 100644 index 0000000..ac74e2f --- /dev/null +++ b/src/main/java/core_service/modules/version/api/dto/VersionDto.java @@ -0,0 +1,7 @@ +package core_service.modules.version.api.dto; + +/** + * @param commit full git hash the image was built from, "dev" outside the CI build + * @param builtAt ISO-8601 UTC build time, null outside the CI build + */ +public record VersionDto(String commit, String builtAt) {} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index b4e616d..6bfcb48 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -38,6 +38,10 @@ security: public-key: ${JWT_PUBLIC_KEY:} jwks-url: ${JWT_JWKS_URL:} app: + build: + # Filled from the CI run by the Dockerfile. + commit: ${APP_BUILD_COMMIT:dev} + time: ${APP_BUILD_TIME:} cors: allowed-origins: "https://tbp.andreas-dahm.eu" steam: diff --git a/src/test/java/core_service/configuration/security/PublicEndpointsTest.java b/src/test/java/core_service/configuration/security/PublicEndpointsTest.java index 9ea7ed2..7a647a0 100644 --- a/src/test/java/core_service/configuration/security/PublicEndpointsTest.java +++ b/src/test/java/core_service/configuration/security/PublicEndpointsTest.java @@ -90,4 +90,11 @@ class PublicEndpointsTest { mockMvc.perform(get("/api/public/profiles/library")).andExpect(status().isUnauthorized()) .andExpect(jsonPath("$.code").value("INVALID_TICKET")); } + + @Test + void versionIsPublic() throws Exception { + mockMvc.perform(get("/api/version")) + .andExpect(status().isOk()) + .andExpect(jsonPath("$.commit").isString()); + } } diff --git a/src/test/java/core_service/modules/version/api/VersionControllerTest.java b/src/test/java/core_service/modules/version/api/VersionControllerTest.java new file mode 100644 index 0000000..51c8228 --- /dev/null +++ b/src/test/java/core_service/modules/version/api/VersionControllerTest.java @@ -0,0 +1,24 @@ +package core_service.modules.version.api; + +import core_service.modules.version.api.dto.VersionDto; +import core_service.shared.test.AbstractUnitTest; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class VersionControllerTest extends AbstractUnitTest { + + @Test + void answersTheBuildOfTheImage() { + VersionDto version = new VersionController("4ec47f1aa0b1", "2026-09-27T08:05:00Z").getVersion().getBody(); + + assertEquals(new VersionDto("4ec47f1aa0b1", "2026-09-27T08:05:00Z"), version); + } + + @Test + void emptyBuildArgsCountAsLocalBuild() { + VersionDto version = new VersionController("", "").getVersion().getBody(); + + assertEquals(new VersionDto("dev", null), version); + } +}