Commit 944e2731a17e42ad7c953d6c4246558034f0a314
1 parent
f58e513e
feat(bootstrap): real buildInfo + framework version as single source of truth
Closes a 15-commit-old TODO on MetaController and unifies the
version story across /api/v1/_meta/info, /v3/api-docs, and the
api-v1.jar manifest.
**Build metadata wiring.** `distribution/build.gradle.kts` now
calls `buildInfo()` inside the `springBoot { }` block. This makes
Spring Boot's Gradle plug-in write `META-INF/build-info.properties`
into the bootJar at build time with group / artifact / version /
build time pulled from `project.version` + timestamps. Spring
Boot's `BuildInfoAutoConfiguration` then exposes a `BuildProperties`
bean that injection points can consume.
**MetaController enriched.** Now injects:
- `ObjectProvider<BuildProperties>` — returns the real version
(`0.28.0-SNAPSHOT`) and the build timestamp when packaged
through the distribution bootJar; falls back to `0.0.0-test`
inside a bare platform-bootstrap unit test classloader with
no build-info file on the classpath.
- `Environment` — returns `spring.profiles.active` so a
dashboard can distinguish "dev" from "staging" from a prod
container that activates no profile.
The GET /api/v1/_meta/info response now carries:
- `name`, `apiVersion` — unchanged
- `implementationVersion` — from BuildProperties (was stuck at
"0.1.0-SNAPSHOT" via an unreachable `javaClass.package` lookup)
- `buildTime` — ISO-8601 string from BuildProperties, null if
the classpath has no build-info file
- `activeProfiles` — list of effective spring profiles
**OpenApiConfiguration now reads version from BuildProperties too.**
Previously OPENAPI_INFO_VERSION was a hardcoded "v0.28.0"
constant. Now it's injected via ObjectProvider<BuildProperties>
with the same fallback pattern as MetaController. A single
version bump in gradle.properties now flows to:
gradle.properties
→ Spring Boot's buildInfo()
→ build-info.properties (on the classpath)
→ BuildProperties bean
→ MetaController (/_meta/info)
→ OpenApiConfiguration (/v3/api-docs + Swagger UI)
→ api-v1.jar manifest (already wired)
No more hand-maintained version strings in code. Bump
`vibeerp.version` in gradle.properties and every display follows.
**Version bump.** `gradle.properties` `vibeerp.version`:
`0.1.0-SNAPSHOT` → `0.28.0-SNAPSHOT`. This matches the numeric
label used on PROGRESS.md's "Latest version" row and carries a
documentation comment explaining the propagation chain so the
next person bumping it knows what to update alongside (just the
one line + PROGRESS.md).
**KDoc trap caught.** A literal `/api/v1/_meta/**` path pattern
in MetaController's KDoc tripped the Kotlin nested-comment
parser (`/**` starts a KDoc). Rephrased as "the whole `/api/v1/_meta`
prefix" to sidestep the trap — same workaround I saved in
feedback memory after the first time it bit me.
**Smoke-tested end-to-end against real Postgres:**
- GET /api/v1/_meta/info returns
`{"implementationVersion": "0.28.0-SNAPSHOT",
"buildTime": "2026-04-09T09:48:25.646Z",
"activeProfiles": ["dev"]}`
- GET /v3/api-docs `info.version` = "0.28.0-SNAPSHOT" (was
the hardcoded "v0.28.0" constant before this chunk)
- Single edit to gradle.properties propagates cleanly.
24 modules, 355 unit tests, all green.
Showing
4 changed files
with
101 additions
and
28 deletions
distribution/build.gradle.kts
| ... | ... | @@ -60,6 +60,13 @@ dependencies { |
| 60 | 60 | // the entry point because :distribution has no Kotlin sources of its own. |
| 61 | 61 | springBoot { |
| 62 | 62 | mainClass.set("org.vibeerp.platform.bootstrap.VibeErpApplicationKt") |
| 63 | + // buildInfo() writes build metadata (group, artifact, version, build | |
| 64 | + // timestamp) into META-INF/build-info.properties at build time. Spring | |
| 65 | + // Boot's BuildInfoAutoConfiguration picks it up at runtime and exposes | |
| 66 | + // it as a BuildProperties bean that MetaController injects to fill in | |
| 67 | + // a real version/build time on GET /api/v1/_meta/info. Without this | |
| 68 | + // the fallback in MetaController was stuck at "0.1.0-SNAPSHOT". | |
| 69 | + buildInfo() | |
| 63 | 70 | } |
| 64 | 71 | |
| 65 | 72 | // The fat-jar produced here is what the Dockerfile copies into the | ... | ... |
gradle.properties
| ... | ... | @@ -9,6 +9,16 @@ kotlin.code.style=official |
| 9 | 9 | kotlin.incremental=true |
| 10 | 10 | |
| 11 | 11 | # vibe_erp |
| 12 | -vibeerp.version=0.1.0-SNAPSHOT | |
| 12 | +# Single source of truth for the framework version. Flows into: | |
| 13 | +# - Spring Boot's BuildProperties (via `springBoot { buildInfo() }` | |
| 14 | +# in :distribution), which MetaController returns as | |
| 15 | +# `implementationVersion` on GET /api/v1/_meta/info and | |
| 16 | +# OpenApiConfiguration renders as the OpenAPI info.version | |
| 17 | +# in Swagger UI + /v3/api-docs. | |
| 18 | +# - api-v1.jar manifest (Implementation-Version) via | |
| 19 | +# api/api-v1/build.gradle.kts. | |
| 20 | +# Bump this line in lockstep with PROGRESS.md's "Latest version" | |
| 21 | +# row when shipping a new working surface. | |
| 22 | +vibeerp.version=0.28.0-SNAPSHOT | |
| 13 | 23 | vibeerp.api.version=1.0.0-SNAPSHOT |
| 14 | 24 | vibeerp.group=org.vibeerp | ... | ... |
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 |
| 8 | 8 | import io.swagger.v3.oas.models.security.SecurityRequirement |
| 9 | 9 | import io.swagger.v3.oas.models.security.SecurityScheme |
| 10 | 10 | import io.swagger.v3.oas.models.servers.Server |
| 11 | +import org.springframework.beans.factory.ObjectProvider | |
| 12 | +import org.springframework.boot.info.BuildProperties | |
| 11 | 13 | import org.springframework.context.annotation.Bean |
| 12 | 14 | import org.springframework.context.annotation.Configuration |
| 13 | 15 | |
| ... | ... | @@ -42,18 +44,25 @@ import org.springframework.context.annotation.Configuration |
| 42 | 44 | * behind a reverse proxy. A future multi-environment story can |
| 43 | 45 | * grow this list from configuration. |
| 44 | 46 | * |
| 45 | - * **Version source.** [OPENAPI_INFO_VERSION] is a build-time | |
| 46 | - * constant kept in sync with the framework's `PROGRESS.md` at | |
| 47 | - * memory time; it's not parsed out of any manifest because the | |
| 48 | - * distribution fat-jar doesn't carry a semver yet. When a real | |
| 49 | - * version header lands this moves to a @Value on a config | |
| 50 | - * property, but hardcoding it now keeps the chunk small. | |
| 47 | + * **Version source.** The displayed version is read from Spring | |
| 48 | + * Boot's [BuildProperties] bean, which is itself populated at | |
| 49 | + * build time from `META-INF/build-info.properties` generated by | |
| 50 | + * `springBoot { buildInfo() }` in `:distribution/build.gradle.kts`. | |
| 51 | + * Single source of truth: `gradle.properties`' `vibeerp.version` | |
| 52 | + * → `build-info.properties` → [BuildProperties] → here. When the | |
| 53 | + * framework is bumped in `gradle.properties`, every surface that | |
| 54 | + * displays a version follows automatically. Injected via | |
| 55 | + * [ObjectProvider] so unit tests that instantiate the config | |
| 56 | + * without a build-info file don't fail. | |
| 51 | 57 | */ |
| 52 | 58 | @Configuration |
| 53 | -class OpenApiConfiguration { | |
| 59 | +class OpenApiConfiguration( | |
| 60 | + private val buildPropertiesProvider: ObjectProvider<BuildProperties>, | |
| 61 | +) { | |
| 54 | 62 | |
| 55 | 63 | @Bean |
| 56 | 64 | fun vibeErpOpenApi(): OpenAPI { |
| 65 | + val version = buildPropertiesProvider.ifAvailable?.version ?: FALLBACK_VERSION | |
| 57 | 66 | val bearerScheme = SecurityScheme() |
| 58 | 67 | .type(SecurityScheme.Type.HTTP) |
| 59 | 68 | .scheme("bearer") |
| ... | ... | @@ -67,7 +76,7 @@ class OpenApiConfiguration { |
| 67 | 76 | .info( |
| 68 | 77 | Info() |
| 69 | 78 | .title("vibe_erp") |
| 70 | - .version(OPENAPI_INFO_VERSION) | |
| 79 | + .version(version) | |
| 71 | 80 | .description( |
| 72 | 81 | """ |
| 73 | 82 | Auto-generated OpenAPI 3 spec for the vibe_erp framework core and every |
| ... | ... | @@ -127,10 +136,13 @@ class OpenApiConfiguration { |
| 127 | 136 | const val BEARER_SCHEME_NAME: String = "bearerAuth" |
| 128 | 137 | |
| 129 | 138 | /** |
| 130 | - * Displayed in Swagger UI's header. Hardcoded for now; when | |
| 131 | - * a real build-time version header ships this becomes a | |
| 132 | - * configuration property. | |
| 139 | + * Emitted when `META-INF/build-info.properties` is not on | |
| 140 | + * the classpath — only happens in unit-test classloaders | |
| 141 | + * for platform-bootstrap itself, which don't package | |
| 142 | + * through the Spring Boot fat-jar pipeline. A bootable | |
| 143 | + * distribution always has a real version and this | |
| 144 | + * fallback is unreachable at runtime. | |
| 133 | 145 | */ |
| 134 | - const val OPENAPI_INFO_VERSION: String = "v0.28.0" | |
| 146 | + const val FALLBACK_VERSION: String = "0.0.0-test" | |
| 135 | 147 | } |
| 136 | 148 | } | ... | ... |
platform/platform-bootstrap/src/main/kotlin/org/vibeerp/platform/bootstrap/web/MetaController.kt
| 1 | 1 | package org.vibeerp.platform.bootstrap.web |
| 2 | 2 | |
| 3 | +import org.springframework.beans.factory.ObjectProvider | |
| 4 | +import org.springframework.boot.info.BuildProperties | |
| 5 | +import org.springframework.core.env.Environment | |
| 3 | 6 | import org.springframework.web.bind.annotation.GetMapping |
| 4 | 7 | import org.springframework.web.bind.annotation.RequestMapping |
| 5 | 8 | import org.springframework.web.bind.annotation.RestController |
| 9 | +import java.time.Instant | |
| 6 | 10 | |
| 7 | 11 | /** |
| 8 | - * Minimal info endpoint that confirms the framework boots and identifies itself. | |
| 12 | + * Public info endpoint that confirms the framework is alive and | |
| 13 | + * describes what kind of vibe_erp this is. | |
| 9 | 14 | * |
| 10 | - * Real health checks come from Spring Boot Actuator at `/actuator/health`. | |
| 11 | - * This endpoint is intentionally separate so it can serve as the "is the | |
| 12 | - * framework alive AND has it loaded its plug-ins?" signal once the plug-in | |
| 13 | - * loader is wired up — for v0.1 it just reports the static info. | |
| 15 | + * Real liveness/readiness probes come from Spring Boot Actuator at | |
| 16 | + * `/actuator/health`; this endpoint is intentionally separate so | |
| 17 | + * it can carry framework-level identification a load balancer or a | |
| 18 | + * future R1 SPA login page can display ("connected to vibe_erp | |
| 19 | + * v0.28.0, built 2026-04-09") without following a JWT-gated route. | |
| 20 | + * | |
| 21 | + * **Public by design.** The whole `/api/v1/_meta` prefix is on | |
| 22 | + * the `SecurityConfiguration` permit-all list, so this response | |
| 23 | + * must NEVER carry secrets — no JDBC URL, no JWT secret, no admin | |
| 24 | + * username, no env-specific config keys. Only framework identity | |
| 25 | + * + coarse capabilities. | |
| 26 | + * | |
| 27 | + * **Build metadata source.** [BuildProperties] is auto-wired by | |
| 28 | + * Spring Boot from `META-INF/build-info.properties`, which | |
| 29 | + * Gradle's `springBoot { buildInfo() }` produces at `:distribution` | |
| 30 | + * build time. Injected via [ObjectProvider] so the controller can | |
| 31 | + * be instantiated in test contexts that have no build-info file | |
| 32 | + * on the classpath without a bean-missing error at startup. | |
| 33 | + * | |
| 34 | + * **Active profiles** come straight from the Spring [Environment]. | |
| 35 | + * Lists the effective `spring.profiles.active` (e.g. `["dev"]` for | |
| 36 | + * `./gradlew :distribution:bootRun`, `[]` for a prod container). | |
| 14 | 37 | */ |
| 15 | 38 | @RestController |
| 16 | 39 | @RequestMapping("/api/v1/_meta") |
| 17 | -class MetaController { | |
| 40 | +class MetaController( | |
| 41 | + private val buildPropertiesProvider: ObjectProvider<BuildProperties>, | |
| 42 | + private val environment: Environment, | |
| 43 | +) { | |
| 18 | 44 | |
| 19 | 45 | @GetMapping("/info") |
| 20 | - fun info(): Map<String, Any> = mapOf( | |
| 21 | - "name" to "vibe-erp", | |
| 22 | - "apiVersion" to "v1", | |
| 23 | - // TODO(v0.2): read implementationVersion from the Spring Boot | |
| 24 | - // BuildProperties bean once `springBoot { buildInfo() }` is wired | |
| 25 | - // up in the distribution module. Hard-coding a fallback here is | |
| 26 | - // fine for v0.1 but will silently drift from `gradle.properties`. | |
| 27 | - "implementationVersion" to (javaClass.`package`.implementationVersion ?: "0.1.0-SNAPSHOT"), | |
| 28 | - ) | |
| 46 | + fun info(): Map<String, Any?> { | |
| 47 | + val build: BuildProperties? = buildPropertiesProvider.ifAvailable | |
| 48 | + val version = build?.version ?: FALLBACK_VERSION | |
| 49 | + val buildTime: Instant? = build?.time | |
| 50 | + | |
| 51 | + return mapOf( | |
| 52 | + "name" to "vibe-erp", | |
| 53 | + "apiVersion" to "v1", | |
| 54 | + "implementationVersion" to version, | |
| 55 | + // ISO-8601 string, or null if the build was produced | |
| 56 | + // without `springBoot { buildInfo() }` (e.g. a bare | |
| 57 | + // Gradle test run of platform-bootstrap itself). | |
| 58 | + "buildTime" to buildTime?.toString(), | |
| 59 | + "activeProfiles" to environment.activeProfiles.toList(), | |
| 60 | + ) | |
| 61 | + } | |
| 62 | + | |
| 63 | + companion object { | |
| 64 | + /** | |
| 65 | + * Emitted when no `build-info.properties` is on the classpath — | |
| 66 | + * happens in unit-test classloaders for platform-bootstrap | |
| 67 | + * itself, which don't go through the Spring Boot fat-jar | |
| 68 | + * packaging. A bootable distribution always has a real | |
| 69 | + * version and this fallback is unreachable. | |
| 70 | + */ | |
| 71 | + const val FALLBACK_VERSION: String = "0.0.0-test" | |
| 72 | + } | |
| 29 | 73 | } | ... | ... |