From 0c1bdf76dd75dcdead75fb5d2e937b2ab11b0b30 Mon Sep 17 00:00:00 2001 From: iryabov Date: Thu, 2 Apr 2026 15:52:55 +0200 Subject: [PATCH] feat: add a separate endpoint to persist build info (Git metadata) --- admin-app/src/main/resources/openapi.yml | 38 +++++- .../drill/admin/metrics/DataIngestClient.kt | 2 +- .../admin/writer/rawdata/entity/Build.kt | 8 +- .../rawdata/repository/BuildRepository.kt | 3 +- .../repository/impl/BuildRepositoryImpl.kt | 27 +++- .../rawdata/route/RawDataWriterRoutes.kt | 13 ++ .../writer/rawdata/service/RawDataWriter.kt | 1 + .../service/impl/RawDataServiceImpl.kt | 28 ++++- .../admin/writer/rawdata/BuildsApiTest.kt | 115 +++++++++++++++++- 9 files changed, 212 insertions(+), 23 deletions(-) diff --git a/admin-app/src/main/resources/openapi.yml b/admin-app/src/main/resources/openapi.yml index 818882c6a..1d32331d8 100644 --- a/admin-app/src/main/resources/openapi.yml +++ b/admin-app/src/main/resources/openapi.yml @@ -71,12 +71,11 @@ paths: # Data Ingest Endpoints /api/data-ingest/builds: put: - summary: Persist application build metadata + summary: Persist application build identity description: | - Saves application build information, including build version and associated Git commit metadata. - This endpoint captures critical build data required for change tracking, impact analysis, risk assessment, and - correlation with test coverage metrics. The service ingests build metadata from instrumented applications to enable - detailed analysis of code changes, test recommendations, and coverage reports across different application versions. + Saves application build identity information, including groupId, appId, commitSha, and buildVersion. + This endpoint only persists the build identity fields. To save additional Git metadata (branch, commitDate, + commitMessage, commitAuthor), use the PUT /api/data-ingest/builds/info endpoint. operationId: putBuild tags: - data-ingest @@ -98,6 +97,35 @@ paths: application/json: schema: $ref: '#/components/schemas/MessageResponse' + /api/data-ingest/builds/info: + put: + summary: Persist application build Git metadata + description: | + Saves Git metadata (branch, commitDate, commitMessage, commitAuthor) for an application build. + The build is identified by groupId, appId, commitSha, and buildVersion. If a build with the given + identity does not exist, a new build is created. If it already exists, only the Git metadata fields + are updated while preserving the existing identity and instance data. + operationId: putBuildInfo + tags: + - data-ingest + security: + - apiKeyAuth: [ ] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/BuildPayload' + application/protobuf: + schema: + $ref: '#/components/schemas/BuildPayload' + responses: + '200': + description: Build info saved + content: + application/json: + schema: + $ref: '#/components/schemas/MessageResponse' /api/data-ingest/instances: put: summary: Persist application instance metadata diff --git a/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/DataIngestClient.kt b/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/DataIngestClient.kt index c3a0a4b9e..fb9f28d5a 100644 --- a/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/DataIngestClient.kt +++ b/admin-metrics/src/test/kotlin/com/epam/drill/admin/metrics/DataIngestClient.kt @@ -98,7 +98,7 @@ val TestDetails.definitionId: String } suspend fun HttpClient.putBuild(payload: BuildPayload): HttpResponse { - return put("/data-ingest/builds") { + return put("/data-ingest/builds/info") { setBody(payload) }.assertSuccessStatus() } diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/entity/Build.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/entity/Build.kt index f3cbd9535..a96cf6d12 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/entity/Build.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/entity/Build.kt @@ -23,9 +23,9 @@ class Build( val appId: String, val commitSha: String?, val buildVersion: String?, - val branch: String?, val instanceId: String?, - val commitDate: LocalDateTime?, - val commitMessage: String?, - val commitAuthor: String? + val branch: String? = null, + val commitDate: LocalDateTime? = null, + val commitMessage: String? = null, + val commitAuthor: String? = null ) diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/BuildRepository.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/BuildRepository.kt index 8614384bf..fa8764c40 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/BuildRepository.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/BuildRepository.kt @@ -19,7 +19,8 @@ import com.epam.drill.admin.writer.rawdata.entity.Build import java.time.LocalDate interface BuildRepository { - suspend fun create(build: Build) + suspend fun saveBuildInfo(build: Build) + suspend fun saveBuildId(build: Build) suspend fun existsById(groupId: String, appId: String, buildId: String): Boolean suspend fun deleteAllCreatedBefore(groupId: String, createdBefore: LocalDate) suspend fun deleteByBuildId(groupId: String, appId: String, buildId: String) diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/impl/BuildRepositoryImpl.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/impl/BuildRepositoryImpl.kt index b1f159d9c..bc16dd0fd 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/impl/BuildRepositoryImpl.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/repository/impl/BuildRepositoryImpl.kt @@ -27,9 +27,11 @@ import org.jetbrains.exposed.sql.upsert import java.time.LocalDate class BuildRepositoryImpl: BuildRepository { - override suspend fun create(build: Build) { + override suspend fun saveBuildInfo(build: Build) { BuildTable.upsert( - onUpdateExclude = listOf(BuildTable.createdAt), + onUpdateExclude = listOf( + BuildTable.createdAt, + ), ) { it[id] = build.id it[groupId] = build.groupId @@ -44,6 +46,27 @@ class BuildRepositoryImpl: BuildRepository { it[updatedAt] = org.jetbrains.exposed.sql.javatime.CurrentDateTime } } + + override suspend fun saveBuildId(build: Build) { + BuildTable.upsert( + onUpdateExclude = listOf( + BuildTable.createdAt, + BuildTable.branch, + BuildTable.committedAt, + BuildTable.commitAuthor, + BuildTable.commitMessage + ), + ) { + it[id] = build.id + it[groupId] = build.groupId + it[appId] = build.appId + it[commitSha] = build.commitSha + it[buildVersion] = build.buildVersion + it[instanceId] = build.instanceId + it[updatedAt] = org.jetbrains.exposed.sql.javatime.CurrentDateTime + } + } + override suspend fun existsById(groupId: String, appId: String, buildId: String): Boolean { return BuildTable.selectAll().where { (BuildTable.groupId eq groupId) and diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt index d02cf23f9..09b3bf4ad 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/route/RawDataWriterRoutes.kt @@ -49,6 +49,9 @@ private val logger = KotlinLogging.logger {} @Resource("builds") class BuildsRoute() +@Resource("builds/info") +class BuildsInfoRoute() + @Resource("instances") class InstancesRoute() @@ -81,6 +84,7 @@ class MethodIgnoreRulesRoute() { fun Route.dataIngestRoutes() { route("/data-ingest") { putBuilds() + putBuildsInfo() putInstances() postCoverage() putMethods() @@ -104,6 +108,15 @@ fun Route.putBuilds() { } } +fun Route.putBuildsInfo() { + val rawDataWriter by closestDI().instance() + + put { + rawDataWriter.saveBuildInfo(call.decompressAndReceive()) + call.ok("Build info saved") + } +} + fun Route.putInstances() { val rawDataWriter by closestDI().instance() diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt index 853f4ffcb..ebdca25d3 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/RawDataWriter.kt @@ -21,6 +21,7 @@ import com.epam.drill.admin.writer.rawdata.views.MethodIgnoreRuleView interface RawDataWriter { suspend fun saveBuild(buildPayload: BuildPayload) + suspend fun saveBuildInfo(buildPayload: BuildPayload) suspend fun saveInstance(instancePayload: InstancePayload) suspend fun saveMethods(methodsPayload: MethodsPayload) suspend fun saveCoverage(coveragePayload: CoveragePayload) diff --git a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt index 3231a4843..2cf4314d8 100644 --- a/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt +++ b/admin-writer/src/main/kotlin/com/epam/drill/admin/writer/rawdata/service/impl/RawDataServiceImpl.kt @@ -47,6 +47,26 @@ class RawDataServiceImpl( ) : RawDataWriter { override suspend fun saveBuild(buildPayload: BuildPayload) { + val build = Build( + id = generateBuildId( + buildPayload.groupId, + buildPayload.appId, + "", + buildPayload.commitSha, + buildPayload.buildVersion + ), + groupId = buildPayload.groupId, + appId = buildPayload.appId, + instanceId = null, + commitSha = buildPayload.commitSha, + buildVersion = buildPayload.buildVersion, + ) + transaction { + buildRepository.saveBuildId(build) + } + } + + override suspend fun saveBuildInfo(buildPayload: BuildPayload) { val build = Build( id = generateBuildId( buildPayload.groupId, @@ -66,7 +86,7 @@ class RawDataServiceImpl( commitAuthor = buildPayload.commitAuthor ) transaction { - buildRepository.create(build) + buildRepository.saveBuildInfo(build) } } @@ -94,12 +114,8 @@ class RawDataServiceImpl( instanceId = instancePayload.instanceId, commitSha = instancePayload.commitSha, buildVersion = instancePayload.buildVersion, - branch = null, - commitDate = null, - commitMessage = null, - commitAuthor = null ) - buildRepository.create(build) + buildRepository.saveBuildId(build) } instanceRepository.create(instance) } diff --git a/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/BuildsApiTest.kt b/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/BuildsApiTest.kt index 23f6c4d00..7260a0a54 100644 --- a/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/BuildsApiTest.kt +++ b/admin-writer/src/test/kotlin/com/epam/drill/admin/writer/rawdata/BuildsApiTest.kt @@ -16,6 +16,7 @@ package com.epam.drill.admin.writer.rawdata import com.epam.drill.admin.writer.rawdata.route.putBuilds +import com.epam.drill.admin.writer.rawdata.route.putBuildsInfo import com.epam.drill.admin.writer.rawdata.table.BuildTable import com.epam.drill.admin.test.* import com.epam.drill.admin.writer.rawdata.config.RawDataWriterDatabaseConfig @@ -28,6 +29,7 @@ import java.time.LocalDateTime import kotlin.test.Test import kotlin.test.assertEquals import kotlin.test.assertNotNull +import kotlin.test.assertNull import kotlin.test.assertTrue class BuildsApiTest : DatabaseTests({ RawDataWriterDatabaseConfig.init(it) }) { @@ -50,8 +52,56 @@ class BuildsApiTest : DatabaseTests({ RawDataWriterDatabaseConfig.init(it) }) { "groupId": "$testGroup", "appId": "$testApp", "buildVersion": "$testBuildVersion", + "commitSha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2" + } + """.trimIndent() + ) + }.apply { + assertEquals(HttpStatusCode.OK, status) + assertJsonEquals( + """ + { + "message": "Build saved" + } + """.trimIndent(), bodyAsText() + ) + } + + val savedBuilds = BuildTable.selectAll() + .filter { it[BuildTable.groupId] == testGroup } + .filter { it[BuildTable.appId] == testApp } + .filter { it[BuildTable.buildVersion] == testBuildVersion } + assertEquals(1, savedBuilds.size) + savedBuilds.forEach { + assertNull(it[BuildTable.branch]) + assertNotNull(it[BuildTable.commitSha]) + assertNull(it[BuildTable.commitAuthor]) + assertNull(it[BuildTable.commitMessage]) + assertNull(it[BuildTable.committedAt]) + assertTrue(it[BuildTable.createdAt] >= timeBeforeTest) + } + } + + @Test + fun `given new build, put builds info should create new build with info fields and return OK`() = withRollback { + val testGroup = "test-group" + val testApp = "test-app" + val testBuildVersion = "2.0.0" + val timeBeforeTest = LocalDateTime.now() + val app = drillApplication(rawDataServicesDIModule) { + putBuildsInfo() + } + + app.client.put("/builds/info") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """ + { + "groupId": "$testGroup", + "appId": "$testApp", + "buildVersion": "$testBuildVersion", + "commitSha": "a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2", "branch": "main", - "commitSha": "d3516472fd72cd0f9ccb7a1dc4b5e7b80a014fd2", "commitMessage": "Initial commit", "commitDate": "Thu Feb 27 10:06:24 2025 +0100", "commitAuthor": "John Doe" @@ -63,7 +113,7 @@ class BuildsApiTest : DatabaseTests({ RawDataWriterDatabaseConfig.init(it) }) { assertJsonEquals( """ { - "message": "Build saved" + "message": "Build info saved" } """.trimIndent(), bodyAsText() ) @@ -75,12 +125,69 @@ class BuildsApiTest : DatabaseTests({ RawDataWriterDatabaseConfig.init(it) }) { .filter { it[BuildTable.buildVersion] == testBuildVersion } assertEquals(1, savedBuilds.size) savedBuilds.forEach { - assertNotNull(it[BuildTable.branch]) + assertEquals("main", it[BuildTable.branch]) assertNotNull(it[BuildTable.commitSha]) + assertEquals("John Doe", it[BuildTable.commitAuthor]) + assertEquals("Initial commit", it[BuildTable.commitMessage]) + assertNotNull(it[BuildTable.committedAt]) + assertTrue(it[BuildTable.createdAt] >= timeBeforeTest) + } + } + + @Test + fun `given existing build info, put builds should not update info fields and return OK`() = withRollback { + val testGroup = "test-group" + val testApp = "test-app" + val testBuildVersion = "3.0.0" + val testCommitSha = "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3" + val app = drillApplication(rawDataServicesDIModule) { + putBuildsInfo() + putBuilds() + } + app.client.put("/builds/info") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """ + { + "groupId": "$testGroup", + "appId": "$testApp", + "buildVersion": "$testBuildVersion", + "commitSha": "$testCommitSha", + "branch": "develop", + "commitMessage": "Feature commit", + "commitDate": "Thu Feb 27 10:06:24 2025 +0100", + "commitAuthor": "Jane Doe" + } + """.trimIndent() + ) + } + + app.client.put("/builds") { + header(HttpHeaders.ContentType, ContentType.Application.Json.toString()) + setBody( + """ + { + "groupId": "$testGroup", + "appId": "$testApp", + "buildVersion": "$testBuildVersion", + "commitSha": "$testCommitSha" + } + """.trimIndent() + ) + }.apply { + assertEquals(HttpStatusCode.OK, status) + } + + val buildsBeforeInfo = BuildTable.selectAll() + .filter { it[BuildTable.groupId] == testGroup } + .filter { it[BuildTable.appId] == testApp } + .filter { it[BuildTable.buildVersion] == testBuildVersion } + assertEquals(1, buildsBeforeInfo.size) + buildsBeforeInfo.forEach { + assertNotNull(it[BuildTable.branch]) assertNotNull(it[BuildTable.commitAuthor]) assertNotNull(it[BuildTable.commitMessage]) assertNotNull(it[BuildTable.committedAt]) - assertTrue(it[BuildTable.createdAt] >= timeBeforeTest) } }