Skip to content

Commit 9beb75b

Browse files
JPercivalclaude
andcommitted
Derive project version from git state
Replace the hardcoded version in gradle.properties with a git-derived version resolved at configuration time: - Tag vX.Y.Z at HEAD -> X.Y.Z (release, signed on publish) - On main with no tag -> latest tag's minor bumped, patch reset, -SNAPSHOT - Any other branch / detached -> <bumped>-<sanitized-branch>-<shortsha>-SNAPSHOT Ported from clinical_quality_language#1746. Also add .claude to .gitignore so Claude Code worktree state is not accidentally staged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 463a18e commit 9beb75b

5 files changed

Lines changed: 112 additions & 1 deletion

File tree

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,3 +55,4 @@ local.properties
5555
# Other
5656
.cursorignore
5757
.worktrees
58+
.claude
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import java.io.File
2+
3+
/**
4+
* Resolves the project version from the current git state.
5+
*
6+
* The resolution rules, in order:
7+
* 1. **Release** — if HEAD is tagged with `vX.Y.Z[...]`, use the tag with the `v` prefix stripped
8+
* (e.g. `v4.6.0` → `4.6.0`). Produces a non-SNAPSHOT version, which triggers artifact signing in
9+
* `cqf.publishing-conventions`.
10+
* 2. **main SNAPSHOT** — if on the `main` branch with no tag at HEAD, take the most recent `vX.Y.Z`
11+
* tag reachable from HEAD, bump the minor component, reset patch to 0, and append `-SNAPSHOT`
12+
* (e.g. last tag `v4.6.0` → `4.7.0-SNAPSHOT`).
13+
* 3. **Branch SNAPSHOT** — otherwise, append a branch identifier and the short SHA to the bumped
14+
* base (e.g. `4.7.0-feature-xyz-5ac419a5d-SNAPSHOT`). Branch name is taken from
15+
* `GITHUB_REF_NAME` if set (the GitHub Actions convention), else from `git rev-parse`; a
16+
* detached HEAD falls back to the literal `detached`.
17+
*
18+
* Fallbacks keep the build working in edge cases: no tags anywhere → base `0.0.0` (bumps to
19+
* `0.1.0`); `git` invocation failure → empty string, which the callers treat as "no data."
20+
*
21+
* The `-SNAPSHOT` suffix on non-release builds is load-bearing: the maven-publish convention checks
22+
* `version.endsWith("SNAPSHOT")` to decide whether to sign and which repository to target.
23+
*/
24+
fun gitVersion(rootDir: File): String {
25+
val exactTag =
26+
runGit(rootDir, "tag", "--points-at", "HEAD")
27+
.lineSequence()
28+
.map { it.trim() }
29+
.firstOrNull { it.matches(Regex("v\\d+\\.\\d+\\.\\d+.*")) }
30+
if (exactTag != null) {
31+
return exactTag.removePrefix("v")
32+
}
33+
34+
val lastTag =
35+
runGit(rootDir, "describe", "--tags", "--abbrev=0", "--match=v*.*.*").trim().takeIf {
36+
it.isNotEmpty()
37+
}
38+
val baseVersion = lastTag?.removePrefix("v") ?: "0.0.0"
39+
val (major, minor, _) = parseVersion(baseVersion)
40+
val bumped = "$major.${minor + 1}.0"
41+
42+
val branch = detectBranch(rootDir)
43+
if (branch == "main") {
44+
return "$bumped-SNAPSHOT"
45+
}
46+
47+
val shortSha = runGit(rootDir, "rev-parse", "--short", "HEAD").trim().ifEmpty { "nosha" }
48+
val sanitized = sanitizeBranch(branch)
49+
return "$bumped-$sanitized-$shortSha-SNAPSHOT"
50+
}
51+
52+
/** Parses `X.Y.Z` out of a version string, ignoring any `-suffix`. Missing parts default to 0. */
53+
private fun parseVersion(v: String): Triple<Int, Int, Int> {
54+
val core = v.split("-", limit = 2)[0].split(".")
55+
return Triple(
56+
core.getOrNull(0)?.toIntOrNull() ?: 0,
57+
core.getOrNull(1)?.toIntOrNull() ?: 0,
58+
core.getOrNull(2)?.toIntOrNull() ?: 0,
59+
)
60+
}
61+
62+
/**
63+
* Picks the branch name. `GITHUB_REF_NAME` is preferred because GitHub Actions often checks out in
64+
* detached-HEAD mode where `git rev-parse --abbrev-ref HEAD` only yields `HEAD`.
65+
*/
66+
private fun detectBranch(rootDir: File): String {
67+
System.getenv("GITHUB_REF_NAME")
68+
?.takeIf { it.isNotBlank() }
69+
?.let {
70+
return it
71+
}
72+
val branch = runGit(rootDir, "rev-parse", "--abbrev-ref", "HEAD").trim()
73+
return if (branch.isBlank() || branch == "HEAD") "detached" else branch
74+
}
75+
76+
/**
77+
* Makes a branch name safe to embed in a Maven version string: replaces runs of non-alphanumerics
78+
* with `-`, trims leading/trailing separators, and caps length at 40 chars.
79+
*/
80+
private fun sanitizeBranch(branch: String): String {
81+
val cleaned = branch.replace(Regex("[^A-Za-z0-9]+"), "-").trim('-')
82+
return cleaned.ifEmpty { "branch" }.take(40).trimEnd('-')
83+
}
84+
85+
/** Runs `git <args>` in [workingDir]. Returns stdout on success, empty string on any failure. */
86+
private fun runGit(workingDir: File, vararg args: String): String {
87+
return try {
88+
val proc =
89+
ProcessBuilder(listOf("git") + args.toList())
90+
.directory(workingDir)
91+
.redirectErrorStream(false)
92+
.start()
93+
val out = proc.inputStream.bufferedReader().readText()
94+
proc.errorStream.bufferedReader().readText()
95+
if (proc.waitFor() == 0) out else ""
96+
} catch (_: Exception) {
97+
""
98+
}
99+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
// Computes the project version from git state and applies it to the root project and all
2+
// subprojects. Resolved once at configuration time of the root project to avoid re-invoking git
3+
// per subproject. See GitVersion.kt for the resolution rules and the rationale behind them.
4+
5+
val resolvedVersion = gitVersion(rootDir)
6+
7+
allprojects {
8+
version = resolvedVersion
9+
}
10+
11+
logger.lifecycle("Resolved project version: $resolvedVersion")

build.gradle.kts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ plugins {
22
alias(libs.plugins.sonarqube)
33
jacoco
44
id("cqf.ci-conventions")
5+
id("cqf.git-version")
56
}
67

78
// Required for the JaCoCo ant dependency used by the aggregate report task

gradle.properties

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
group=org.opencds.cqf.fhir
2-
version=4.6.0-SNAPSHOT
32

43
# Build performance
54
org.gradle.parallel=true

0 commit comments

Comments
 (0)