diff --git a/distribution/build.gradle.kts b/distribution/build.gradle.kts index bba6d68..7abe644 100644 --- a/distribution/build.gradle.kts +++ b/distribution/build.gradle.kts @@ -60,6 +60,13 @@ dependencies { // the entry point because :distribution has no Kotlin sources of its own. springBoot { mainClass.set("org.vibeerp.platform.bootstrap.VibeErpApplicationKt") + // buildInfo() writes build metadata (group, artifact, version, build + // timestamp) into META-INF/build-info.properties at build time. Spring + // Boot's BuildInfoAutoConfiguration picks it up at runtime and exposes + // it as a BuildProperties bean that MetaController injects to fill in + // a real version/build time on GET /api/v1/_meta/info. Without this + // the fallback in MetaController was stuck at "0.1.0-SNAPSHOT". + buildInfo() } // The fat-jar produced here is what the Dockerfile copies into the diff --git a/gradle.properties b/gradle.properties index 2ec3ea3..d2007e3 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,6 +9,16 @@ kotlin.code.style=official kotlin.incremental=true # vibe_erp -vibeerp.version=0.1.0-SNAPSHOT +# Single source of truth for the framework version. Flows into: +# - Spring Boot's BuildProperties (via `springBoot { buildInfo() }` +# in :distribution), which MetaController returns as +# `implementationVersion` on GET /api/v1/_meta/info and +# OpenApiConfiguration renders as the OpenAPI info.version +# in Swagger UI + /v3/api-docs. +# - api-v1.jar manifest (Implementation-Version) via +# api/api-v1/build.gradle.kts. +# Bump this line in lockstep with PROGRESS.md's "Latest version" +# row when shipping a new working surface. +vibeerp.version=0.28.0-SNAPSHOT vibeerp.api.version=1.0.0-SNAPSHOT vibeerp.group=org.vibeerp diff --git a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt index 4378909..199a6f5 100644 --- a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt +++ b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/openapi/OpenApiConfiguration.kt @@ -8,6 +8,8 @@ import io.swagger.v3.oas.models.info.License import io.swagger.v3.oas.models.security.SecurityRequirement import io.swagger.v3.oas.models.security.SecurityScheme import io.swagger.v3.oas.models.servers.Server +import org.springframework.beans.factory.ObjectProvider +import org.springframework.boot.info.BuildProperties import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration @@ -42,18 +44,25 @@ import org.springframework.context.annotation.Configuration * behind a reverse proxy. A future multi-environment story can * grow this list from configuration. * - * **Version source.** [OPENAPI_INFO_VERSION] is a build-time - * constant kept in sync with the framework's `PROGRESS.md` at - * memory time; it's not parsed out of any manifest because the - * distribution fat-jar doesn't carry a semver yet. When a real - * version header lands this moves to a @Value on a config - * property, but hardcoding it now keeps the chunk small. + * **Version source.** The displayed version is read from Spring + * Boot's [BuildProperties] bean, which is itself populated at + * build time from `META-INF/build-info.properties` generated by + * `springBoot { buildInfo() }` in `:distribution/build.gradle.kts`. + * Single source of truth: `gradle.properties`' `vibeerp.version` + * → `build-info.properties` → [BuildProperties] → here. When the + * framework is bumped in `gradle.properties`, every surface that + * displays a version follows automatically. Injected via + * [ObjectProvider] so unit tests that instantiate the config + * without a build-info file don't fail. */ @Configuration -class OpenApiConfiguration { +class OpenApiConfiguration( + private val buildPropertiesProvider: ObjectProvider, +) { @Bean fun vibeErpOpenApi(): OpenAPI { + val version = buildPropertiesProvider.ifAvailable?.version ?: FALLBACK_VERSION val bearerScheme = SecurityScheme() .type(SecurityScheme.Type.HTTP) .scheme("bearer") @@ -67,7 +76,7 @@ class OpenApiConfiguration { .info( Info() .title("vibe_erp") - .version(OPENAPI_INFO_VERSION) + .version(version) .description( """ Auto-generated OpenAPI 3 spec for the vibe_erp framework core and every @@ -127,10 +136,13 @@ class OpenApiConfiguration { const val BEARER_SCHEME_NAME: String = "bearerAuth" /** - * Displayed in Swagger UI's header. Hardcoded for now; when - * a real build-time version header ships this becomes a - * configuration property. + * Emitted when `META-INF/build-info.properties` is not on + * the classpath — only happens in unit-test classloaders + * for platform-bootstrap itself, which don't package + * through the Spring Boot fat-jar pipeline. A bootable + * distribution always has a real version and this + * fallback is unreachable at runtime. */ - const val OPENAPI_INFO_VERSION: String = "v0.28.0" + const val FALLBACK_VERSION: String = "0.0.0-test" } } diff --git a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/MetaController.kt b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/MetaController.kt index e684203..2786781 100644 --- a/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/MetaController.kt +++ b/platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/MetaController.kt @@ -1,29 +1,73 @@ package org.vibeerp.platform.bootstrap.web +import org.springframework.beans.factory.ObjectProvider +import org.springframework.boot.info.BuildProperties +import org.springframework.core.env.Environment import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.RequestMapping import org.springframework.web.bind.annotation.RestController +import java.time.Instant /** - * Minimal info endpoint that confirms the framework boots and identifies itself. + * Public info endpoint that confirms the framework is alive and + * describes what kind of vibe_erp this is. * - * Real health checks come from Spring Boot Actuator at `/actuator/health`. - * This endpoint is intentionally separate so it can serve as the "is the - * framework alive AND has it loaded its plug-ins?" signal once the plug-in - * loader is wired up — for v0.1 it just reports the static info. + * Real liveness/readiness probes come from Spring Boot Actuator at + * `/actuator/health`; this endpoint is intentionally separate so + * it can carry framework-level identification a load balancer or a + * future R1 SPA login page can display ("connected to vibe_erp + * v0.28.0, built 2026-04-09") without following a JWT-gated route. + * + * **Public by design.** The whole `/api/v1/_meta` prefix is on + * the `SecurityConfiguration` permit-all list, so this response + * must NEVER carry secrets — no JDBC URL, no JWT secret, no admin + * username, no env-specific config keys. Only framework identity + * + coarse capabilities. + * + * **Build metadata source.** [BuildProperties] is auto-wired by + * Spring Boot from `META-INF/build-info.properties`, which + * Gradle's `springBoot { buildInfo() }` produces at `:distribution` + * build time. Injected via [ObjectProvider] so the controller can + * be instantiated in test contexts that have no build-info file + * on the classpath without a bean-missing error at startup. + * + * **Active profiles** come straight from the Spring [Environment]. + * Lists the effective `spring.profiles.active` (e.g. `["dev"]` for + * `./gradlew :distribution:bootRun`, `[]` for a prod container). */ @RestController @RequestMapping("/api/v1/_meta") -class MetaController { +class MetaController( + private val buildPropertiesProvider: ObjectProvider, + private val environment: Environment, +) { @GetMapping("/info") - fun info(): Map = mapOf( - "name" to "vibe-erp", - "apiVersion" to "v1", - // TODO(v0.2): read implementationVersion from the Spring Boot - // BuildProperties bean once `springBoot { buildInfo() }` is wired - // up in the distribution module. Hard-coding a fallback here is - // fine for v0.1 but will silently drift from `gradle.properties`. - "implementationVersion" to (javaClass.`package`.implementationVersion ?: "0.1.0-SNAPSHOT"), - ) + fun info(): Map { + val build: BuildProperties? = buildPropertiesProvider.ifAvailable + val version = build?.version ?: FALLBACK_VERSION + val buildTime: Instant? = build?.time + + return mapOf( + "name" to "vibe-erp", + "apiVersion" to "v1", + "implementationVersion" to version, + // ISO-8601 string, or null if the build was produced + // without `springBoot { buildInfo() }` (e.g. a bare + // Gradle test run of platform-bootstrap itself). + "buildTime" to buildTime?.toString(), + "activeProfiles" to environment.activeProfiles.toList(), + ) + } + + companion object { + /** + * Emitted when no `build-info.properties` is on the classpath — + * happens in unit-test classloaders for platform-bootstrap + * itself, which don't go through the Spring Boot fat-jar + * packaging. A bootable distribution always has a real + * version and this fallback is unreachable. + */ + const val FALLBACK_VERSION: String = "0.0.0-test" + } }