feat(version): answer the build commit and time
Build, Test & Push Auth Service / quality-check (push) Successful in 31s
Build, Test & Push Auth Service / docker (push) Successful in 1m21s

GET /api/version is public. CI passes GIT_SHA and BUILD_TIME as Docker build args; a local build answers dev.
This commit is contained in:
lobo committed 2026-09-27 10:27:01 +02:00
1 parent 4ec47f1b2c
commit a1763addce
9 files changed
+76

No files matched your search

+4
View File
@@ -52,6 +52,7 @@ jobs:
BRANCH="$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]')" BRANCH="$(echo "$BRANCH" | tr '[:upper:]' '[:lower:]')"
echo "branch=$BRANCH" >> "$GITHUB_OUTPUT" echo "branch=$BRANCH" >> "$GITHUB_OUTPUT"
echo "sha=${GITHUB_SHA::12}" >> "$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 - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -69,6 +70,9 @@ jobs:
with: with:
context: . context: .
push: true push: true
build-args: |
GIT_SHA=${{ github.sha }}
BUILD_TIME=${{ steps.prep.outputs.build_time }}
tags: | tags: |
git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }} git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }}
git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }} git.andreas-dahm.eu/lobo_corp/tbp-auth-service:${{ steps.prep.outputs.branch }}-${{ steps.prep.outputs.sha }}
+5
View File
@@ -25,6 +25,11 @@ FROM eclipse-temurin:25-jre-alpine
WORKDIR /app 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 the port your Spring Boot app runs on
EXPOSE 8080 EXPOSE 8080
+2
View File
@@ -56,6 +56,7 @@ To Be Played is split into three repositories:
| `PUT` | `/api/users/settings/profile-stats` (`{groups}`: `PILE`, `TOP_GAMES`, `TASTE`, `HABITS`, `FINISHED`, `TREND`; `204`) | access token | | `PUT` | `/api/users/settings/profile-stats` (`{groups}`: `PILE`, `TOP_GAMES`, `TASTE`, `HABITS`, `FINISHED`, `TREND`; `204`) | access token |
| `GET` | `/api/public/profiles/{displayName}` | public | | `GET` | `/api/public/profiles/{displayName}` | public |
| `GET` | `/api/stats/public` | public | | `GET` | `/api/stats/public` | public |
| `GET` | `/api/version` (`{commit, builtAt}` of the running image, from the CI build; `dev` locally) | public |
Endpoints use Spring's API versioning with a query parameter: `?version=1.0` (default `1.0`). Endpoints use Spring's API versioning with a query parameter: `?version=1.0` (default `1.0`).
@@ -80,6 +81,7 @@ layers:
| Persistence | `persistence` | JPA entities, Spring Data repositories, `EntityMapper`. Entities never reach the API layer. | | Persistence | `persistence` | JPA entities, Spring Data repositories, `EntityMapper`. Entities never reach the API layer. |
`profile` and `stats` only read data. They have no entities and use `JdbcTemplate` queries in a `repo` package. `profile` and `stats` only read data. They have no entities and use `JdbcTemplate` queries in a `repo` package.
`version` has only an `api` package: `GET /api/version` answers the commit and build time of the running image.
ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer ArchUnit enforces these rules in `src/test/java/auth_service/architecture/ArchitectureTest.java`, so a layer
violation fails the build. It also keeps the modules independent of each other and free of cycles. Test base violation fails the build. It also keeps the modules independent of each other and free of cycles. Test base
@@ -44,6 +44,7 @@ public class SecurityConfig {
"/api/steam-link/callback", // Steam's redirect back; bound to the browser by a cookie, see SteamLinkService "/api/steam-link/callback", // Steam's redirect back; bound to the browser by a cookie, see SteamLinkService
"/actuator/health", // Docker health check; details are off "/actuator/health", // Docker health check; details are off
"/.well-known/jwks.json", // Public verify keys for Core "/.well-known/jwks.json", // Public verify keys for Core
"/api/version", // Build of the running image, shown in the settings
"/error" // Default Spring Boot error page routing "/error" // Default Spring Boot error page routing
}; };
@@ -0,0 +1,22 @@
package auth_service.modules.version.api;
import auth_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<VersionDto> getVersion() {
return ResponseEntity.ok(version);
}
}
@@ -0,0 +1,7 @@
package auth_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) {}
+4
View File
@@ -45,6 +45,10 @@ security:
# Custom properties for CORS # Custom properties for CORS
app: app:
build:
# Filled from the CI run by the Dockerfile.
commit: ${APP_BUILD_COMMIT:dev}
time: ${APP_BUILD_TIME:}
cors: cors:
# Production default: strictly allow only the frontend domain # Production default: strictly allow only the frontend domain
allowed-origins: "https://tbp.andreas-dahm.eu" allowed-origins: "https://tbp.andreas-dahm.eu"
@@ -42,4 +42,11 @@ class PublicEndpointsTest extends AbstractIntegrationTest {
int status = mockMvc.perform(get("/actuator/prometheus")).andReturn().getResponse().getStatus(); int status = mockMvc.perform(get("/actuator/prometheus")).andReturn().getResponse().getStatus();
assertTrue(Set.of(401, 403).contains(status), "/actuator/prometheus answered " + status); assertTrue(Set.of(401, 403).contains(status), "/actuator/prometheus answered " + status);
} }
@Test
void versionIsPublic() throws Exception {
mockMvc.perform(get("/api/version"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.commit").isString());
}
} }
@@ -0,0 +1,24 @@
package auth_service.modules.version.api;
import auth_service.modules.version.api.dto.VersionDto;
import auth_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);
}
}