Skip to content

Commit 31622fc

Browse files
authored
[Feature] Get backend build version (#555)
* build(version): generate git and build metadata during maven build * feat(version): add public endpoint for backend build version * test(version): cover version service and endpoint
1 parent 644048a commit 31622fc

9 files changed

Lines changed: 359 additions & 1 deletion

File tree

pom.xml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@
281281
<plugin>
282282
<groupId>org.springframework.boot</groupId>
283283
<artifactId>spring-boot-maven-plugin</artifactId>
284+
<executions>
285+
<execution>
286+
<id>build-info</id>
287+
<goals>
288+
<goal>build-info</goal>
289+
</goals>
290+
</execution>
291+
</executions>
284292
<configuration>
285293
<excludes>
286294
<exclude>
@@ -290,6 +298,23 @@
290298
</excludes>
291299
</configuration>
292300
</plugin>
301+
<plugin>
302+
<groupId>io.github.git-commit-id</groupId>
303+
<artifactId>git-commit-id-maven-plugin</artifactId>
304+
<configuration>
305+
<failOnNoGitDirectory>false</failOnNoGitDirectory>
306+
<failOnUnableToExtractRepoInfo>false</failOnUnableToExtractRepoInfo>
307+
<verbose>false</verbose>
308+
<dateFormat>yyyy-MM-dd'T'HH:mm:ssXXX</dateFormat>
309+
<dateFormatTimeZone>UTC</dateFormatTimeZone>
310+
<includeOnlyProperties>
311+
<includeOnlyProperty>^git.branch$</includeOnlyProperty>
312+
<includeOnlyProperty>^git.commit.id$</includeOnlyProperty>
313+
<includeOnlyProperty>^git.commit.id.abbrev$</includeOnlyProperty>
314+
<includeOnlyProperty>^git.commit.time$</includeOnlyProperty>
315+
</includeOnlyProperties>
316+
</configuration>
317+
</plugin>
293318
</plugins>
294319
</build>
295320
</project>

src/main/java/com/itasocialacademy/oitassist/security/config/SecurityConfig.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,8 @@ public SecurityFilterChain configure(HttpSecurity http) {
8383
"/api/v1/user-activation/verify",
8484
"/ui",
8585
"/ui/**",
86-
"/uploads/news/**")
86+
"/uploads/news/**",
87+
"/api/v1/version")
8788
.permitAll()
8889
.requestMatchers(
8990
"/index.html",
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.itasocialacademy.oitassist.version.controller;
2+
3+
import com.itasocialacademy.oitassist.version.dao.dto.response.VersionResponse;
4+
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
5+
import io.swagger.v3.oas.annotations.Operation;
6+
import io.swagger.v3.oas.annotations.media.Content;
7+
import io.swagger.v3.oas.annotations.media.Schema;
8+
import io.swagger.v3.oas.annotations.responses.ApiResponse;
9+
import io.swagger.v3.oas.annotations.tags.Tag;
10+
import lombok.RequiredArgsConstructor;
11+
import org.springframework.http.MediaType;
12+
import org.springframework.web.bind.annotation.GetMapping;
13+
import org.springframework.web.bind.annotation.RequestMapping;
14+
import org.springframework.web.bind.annotation.RestController;
15+
16+
@RestController
17+
@RequiredArgsConstructor
18+
@RequestMapping("/api/v1/version")
19+
@Tag(name = "Version", description = "Public API for the build version of the running application")
20+
public class VersionController {
21+
private final VersionService versionService;
22+
23+
@GetMapping
24+
@Operation(
25+
summary = "Get application build version",
26+
description = """
27+
Returns the build version of the running application: the commit the backend
28+
was built from and the date the artifact was built.
29+
Values that were not available at build time are returned as null.
30+
Publicly accessible, no authentication required.
31+
""")
32+
@ApiResponse(
33+
responseCode = "200",
34+
description = "Build version retrieved successfully",
35+
content = @Content(
36+
mediaType = MediaType.APPLICATION_JSON_VALUE,
37+
schema = @Schema(implementation = VersionResponse.class)))
38+
public VersionResponse getVersion() {
39+
return versionService.getVersion();
40+
}
41+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package com.itasocialacademy.oitassist.version.dao.dto.response;
2+
3+
import io.swagger.v3.oas.annotations.media.Schema;
4+
import java.time.Instant;
5+
6+
@Schema(description = "Build version of the running application")
7+
public record VersionResponse(
8+
@Schema(description = "Backend build information") BackendVersion backend,
9+
@Schema(description = "Date and time when the artifact was built") Instant buildTime) {
10+
@Schema(description = "Backend build information")
11+
public record BackendVersion(
12+
@Schema(
13+
description = "Full hash of the commit the artifact was built from",
14+
example = "3534bab24568a602605078b9264711223f218dd2") String commitId,
15+
@Schema(
16+
description = "Short hash of the commit the artifact was built from",
17+
example = "3534bab") String shortCommitId,
18+
@Schema(description = "Date and time of the commit") Instant commitTime,
19+
@Schema(description = "Branch the artifact was built from", example = "dev") String branch,
20+
@Schema(description = "Artifact version", example = "0.0.1-SNAPSHOT") String version) {
21+
}
22+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
@org.springframework.modulith.ApplicationModule(
2+
displayName = "version",
3+
allowedDependencies = {})
4+
5+
package com.itasocialacademy.oitassist.version;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package com.itasocialacademy.oitassist.version.service;
2+
3+
import com.itasocialacademy.oitassist.version.dao.dto.response.VersionResponse;
4+
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
5+
import lombok.extern.slf4j.Slf4j;
6+
import org.jspecify.annotations.NonNull;
7+
import org.springframework.beans.factory.ObjectProvider;
8+
import org.springframework.boot.info.BuildProperties;
9+
import org.springframework.boot.info.GitProperties;
10+
import org.springframework.stereotype.Service;
11+
import java.time.Instant;
12+
13+
@Slf4j
14+
@Service
15+
public class VersionServiceImpl implements VersionService {
16+
private final GitProperties gitProperties;
17+
private final BuildProperties buildProperties;
18+
19+
public VersionServiceImpl(
20+
ObjectProvider<@NonNull GitProperties> gitPropertiesProvider,
21+
ObjectProvider<@NonNull BuildProperties> buildPropertiesProvider) {
22+
this.gitProperties = gitPropertiesProvider.getIfAvailable();
23+
this.buildProperties = buildPropertiesProvider.getIfAvailable();
24+
25+
if (this.gitProperties == null) {
26+
log.warn("git.properties is missing, commit data will not be reported");
27+
}
28+
if (this.buildProperties == null) {
29+
log.warn("build-info.properties is missing, build data will not be reported");
30+
}
31+
}
32+
33+
@Override
34+
public VersionResponse getVersion() {
35+
return new VersionResponse(getBackendVersion(), getBuildTime());
36+
}
37+
38+
private VersionResponse.BackendVersion getBackendVersion() {
39+
if (gitProperties == null) {
40+
return new VersionResponse.BackendVersion(null, null, null, null, null);
41+
}
42+
return new VersionResponse.BackendVersion(
43+
gitProperties.getCommitId(),
44+
gitProperties.getShortCommitId(),
45+
gitProperties.getCommitTime(),
46+
gitProperties.getBranch(),
47+
getArtifactVersion());
48+
}
49+
50+
private String getArtifactVersion() {
51+
return buildProperties == null ? null : buildProperties.getVersion();
52+
}
53+
54+
private Instant getBuildTime() {
55+
return buildProperties == null ? null : buildProperties.getTime();
56+
}
57+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package com.itasocialacademy.oitassist.version.service.interfaces;
2+
3+
import com.itasocialacademy.oitassist.version.dao.dto.response.VersionResponse;
4+
5+
public interface VersionService {
6+
VersionResponse getVersion();
7+
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
package com.itasocialacademy.oitassist.version.controller;
2+
3+
import static org.mockito.Mockito.when;
4+
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
5+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
6+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
7+
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
8+
9+
import com.itasocialacademy.oitassist.core.web.AppExceptionHttpStatusMapper;
10+
import com.itasocialacademy.oitassist.security.jwt.JwtFilter;
11+
import com.itasocialacademy.oitassist.version.dao.dto.response.VersionResponse;
12+
import com.itasocialacademy.oitassist.version.service.interfaces.VersionService;
13+
import java.time.Instant;
14+
import org.junit.jupiter.api.Test;
15+
import org.springframework.beans.factory.annotation.Autowired;
16+
import org.springframework.boot.test.context.TestConfiguration;
17+
import org.springframework.boot.webmvc.test.autoconfigure.WebMvcTest;
18+
import org.springframework.context.annotation.Bean;
19+
import org.springframework.context.annotation.ComponentScan;
20+
import org.springframework.context.annotation.FilterType;
21+
import org.springframework.context.annotation.Import;
22+
import org.springframework.http.MediaType;
23+
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
24+
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
25+
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
26+
import org.springframework.security.web.SecurityFilterChain;
27+
import org.springframework.test.context.bean.override.mockito.MockitoBean;
28+
import org.springframework.test.web.servlet.MockMvc;
29+
30+
@WebMvcTest(
31+
controllers = VersionController.class,
32+
excludeFilters = @ComponentScan.Filter(
33+
type = FilterType.ASSIGNABLE_TYPE,
34+
classes = JwtFilter.class))
35+
@Import(VersionControllerTest.SecurityTestConfiguration.class)
36+
class VersionControllerTest {
37+
38+
private static final String ENDPOINT = "/api/v1/version";
39+
private static final String COMMIT_ID = "3534bab24568a602605078b9264711223f218dd2";
40+
private static final String SHORT_COMMIT_ID = "3534bab";
41+
private static final String COMMIT_TIME = "2026-08-19T08:22:05Z";
42+
private static final String BUILD_TIME = "2026-08-19T19:55:29.490Z";
43+
44+
@Autowired
45+
private MockMvc mockMvc;
46+
47+
@MockitoBean
48+
private VersionService versionService;
49+
50+
@MockitoBean
51+
private AppExceptionHttpStatusMapper appExceptionHttpStatusMapper;
52+
53+
@Test
54+
void getVersion_ShouldReturnVersion_WhenCallerIsAnonymous() throws Exception {
55+
when(versionService.getVersion()).thenReturn(
56+
new VersionResponse(
57+
new VersionResponse.BackendVersion(
58+
COMMIT_ID,
59+
SHORT_COMMIT_ID,
60+
Instant.parse(COMMIT_TIME),
61+
"dev",
62+
"0.0.1-SNAPSHOT"),
63+
Instant.parse(BUILD_TIME)));
64+
65+
mockMvc.perform(get(ENDPOINT))
66+
.andExpect(status().isOk())
67+
.andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
68+
.andExpect(jsonPath("$.backend.commitId").value(COMMIT_ID))
69+
.andExpect(jsonPath("$.backend.shortCommitId").value(SHORT_COMMIT_ID))
70+
.andExpect(jsonPath("$.backend.commitTime").value(COMMIT_TIME))
71+
.andExpect(jsonPath("$.backend.branch").value("dev"))
72+
.andExpect(jsonPath("$.backend.version").value("0.0.1-SNAPSHOT"))
73+
.andExpect(jsonPath("$.buildTime").value(BUILD_TIME));
74+
}
75+
76+
@Test
77+
void getVersion_ShouldReturnOkWithEmptyValues_WhenBuildMetadataIsMissing() throws Exception {
78+
when(versionService.getVersion()).thenReturn(
79+
new VersionResponse(
80+
new VersionResponse.BackendVersion(null, null, null, null, null),
81+
null));
82+
83+
mockMvc.perform(get(ENDPOINT))
84+
.andExpect(status().isOk())
85+
.andExpect(jsonPath("$.backend").exists())
86+
.andExpect(jsonPath("$.backend.commitId").isEmpty())
87+
.andExpect(jsonPath("$.backend.commitTime").isEmpty())
88+
.andExpect(jsonPath("$.backend.branch").isEmpty())
89+
.andExpect(jsonPath("$.buildTime").isEmpty());
90+
}
91+
92+
@TestConfiguration(proxyBeanMethods = false)
93+
@EnableWebSecurity
94+
static class SecurityTestConfiguration {
95+
@Bean
96+
SecurityFilterChain securityFilterChain(HttpSecurity http) {
97+
return http
98+
.csrf(AbstractHttpConfigurer::disable)
99+
.authorizeHttpRequests(authorization -> authorization
100+
.anyRequest()
101+
.permitAll())
102+
.build();
103+
}
104+
}
105+
}
Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
package com.itasocialacademy.oitassist.version.service;
2+
3+
import static org.assertj.core.api.Assertions.assertThat;
4+
import static org.mockito.Mockito.when;
5+
6+
import com.itasocialacademy.oitassist.version.dao.dto.response.VersionResponse;
7+
import java.time.Instant;
8+
import java.util.Properties;
9+
import org.junit.jupiter.api.Test;
10+
import org.junit.jupiter.api.extension.ExtendWith;
11+
import org.mockito.Mock;
12+
import org.mockito.junit.jupiter.MockitoExtension;
13+
import org.springframework.beans.factory.ObjectProvider;
14+
import org.springframework.boot.info.BuildProperties;
15+
import org.springframework.boot.info.GitProperties;
16+
17+
@ExtendWith(MockitoExtension.class)
18+
class VersionServiceImplTest {
19+
20+
private static final String COMMIT_ID = "3534bab24568a602605078b9264711223f218dd2";
21+
private static final String SHORT_COMMIT_ID = "3534bab";
22+
private static final String BRANCH = "dev";
23+
private static final String ARTIFACT_VERSION = "0.0.1-SNAPSHOT";
24+
private static final Instant COMMIT_TIME = Instant.parse("2026-08-19T08:22:05Z");
25+
private static final Instant BUILD_TIME = Instant.parse("2026-08-19T19:55:29.490Z");
26+
27+
@Mock
28+
private ObjectProvider<GitProperties> gitPropertiesProvider;
29+
30+
@Mock
31+
private ObjectProvider<BuildProperties> buildPropertiesProvider;
32+
33+
@Test
34+
void getVersion_ShouldReturnFullVersion_WhenBuildMetadataIsPresent() {
35+
when(gitPropertiesProvider.getIfAvailable()).thenReturn(gitProperties());
36+
when(buildPropertiesProvider.getIfAvailable()).thenReturn(buildProperties());
37+
38+
VersionResponse version = createService().getVersion();
39+
40+
assertThat(version.buildTime()).isEqualTo(BUILD_TIME);
41+
assertThat(version.backend().commitId()).isEqualTo(COMMIT_ID);
42+
assertThat(version.backend().shortCommitId()).isEqualTo(SHORT_COMMIT_ID);
43+
assertThat(version.backend().commitTime()).isEqualTo(COMMIT_TIME);
44+
assertThat(version.backend().branch()).isEqualTo(BRANCH);
45+
assertThat(version.backend().version()).isEqualTo(ARTIFACT_VERSION);
46+
}
47+
48+
@Test
49+
void getVersion_ShouldReturnEmptyValues_WhenBuildMetadataIsMissing() {
50+
VersionResponse version = createService().getVersion();
51+
52+
assertThat(version.buildTime()).isNull();
53+
assertThat(version.backend()).isNotNull();
54+
assertThat(version.backend().commitId()).isNull();
55+
assertThat(version.backend().shortCommitId()).isNull();
56+
assertThat(version.backend().commitTime()).isNull();
57+
assertThat(version.backend().branch()).isNull();
58+
assertThat(version.backend().version()).isNull();
59+
}
60+
61+
@Test
62+
void getVersion_ShouldReturnCommitDataOnly_WhenBuildInfoIsMissing() {
63+
when(gitPropertiesProvider.getIfAvailable()).thenReturn(gitProperties());
64+
65+
VersionResponse version = createService().getVersion();
66+
67+
assertThat(version.backend().commitId()).isEqualTo(COMMIT_ID);
68+
assertThat(version.backend().commitTime()).isEqualTo(COMMIT_TIME);
69+
assertThat(version.backend().branch()).isEqualTo(BRANCH);
70+
assertThat(version.backend().version()).isNull();
71+
assertThat(version.buildTime()).isNull();
72+
}
73+
74+
private VersionServiceImpl createService() {
75+
return new VersionServiceImpl(gitPropertiesProvider, buildPropertiesProvider);
76+
}
77+
78+
private GitProperties gitProperties() {
79+
Properties entries = new Properties();
80+
entries.setProperty("branch", BRANCH);
81+
entries.setProperty("commit.id", COMMIT_ID);
82+
entries.setProperty("commit.id.abbrev", SHORT_COMMIT_ID);
83+
entries.setProperty("commit.time", COMMIT_TIME.toString());
84+
return new GitProperties(entries);
85+
}
86+
87+
private BuildProperties buildProperties() {
88+
Properties entries = new Properties();
89+
entries.setProperty("group", "com.ita-social-academy");
90+
entries.setProperty("artifact", "OITAssist");
91+
entries.setProperty("version", ARTIFACT_VERSION);
92+
entries.setProperty("time", BUILD_TIME.toString());
93+
return new BuildProperties(entries);
94+
}
95+
}

0 commit comments

Comments
 (0)