Skip to content

Commit 075b19b

Browse files
committed
Add the ability to add substitutions to project versions
1 parent 3a78f8a commit 075b19b

7 files changed

Lines changed: 134 additions & 11 deletions

File tree

.github/actions/update-project-versions/README.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ The following directories are always skipped: `.git`, `node_modules`, `target`,
2626
| `versions` | ✴️ || JSON object mapping project names to versions (e.g. `{"spring-boot":"3.2.3","spring-cloud-config":"4.1.1"}`). Typically produced by the `extract-bom-versions` action. Required when `release-train-version` is not set. |
2727
| `project-version` | ✴️ || The new version for this project (e.g. `4.1.2`). Used to update the root `pom.xml` `<version>`, child `<parent><version>` when the parent is the root project, the bare `version=` in `gradle.properties`, and the `version = '...'` line in `build.gradle`. Required when `release-train-version` is not set. |
2828
| `directory` || `.` | Root directory of the project to update. Defaults to the repository root. |
29+
| `project-version-substitutions` ||| JSON object mapping additional version property name prefixes to project names already in the versions map. Only used with `release-train-version`. See [Version name substitutions](#version-name-substitutions). |
2930

3031
✴️ Either `release-train-version` **or** both `versions` and `project-version` must be supplied.
3132

@@ -137,6 +138,24 @@ Property keys follow the camelCase convention used by Spring Cloud projects:
137138

138139
Only keys matching the pattern `^[a-zA-Z0-9]+Version$` (a camelCase prefix immediately followed by `Version`) are considered. Keys like `releaseVersion` or `versionCode` are intentionally ignored.
139140

141+
## Version name substitutions
142+
143+
Some projects use version property names that don't follow the standard camelCase-to-kebab-case convention. The `project-version-substitutions` input lets you bridge these non-standard names to the correct project entry in the versions map.
144+
145+
For example, `spring-cloud-contract` uses `verifierVersion` in its `gradle.properties` instead of the expected `springCloudContractVersion`. The camelCase-to-kebab conversion of `verifier` is just `verifier`, which has no entry in the versions map. To fix this, pass:
146+
147+
```yaml
148+
- name: Update project versions
149+
uses: spring-cloud/spring-cloud-github-actions/.github/actions/update-project-versions@main
150+
with:
151+
release-train-version: '2025.1.0'
152+
project-version-substitutions: '{"verifier":"spring-cloud-contract"}'
153+
```
154+
155+
This adds a synthetic `verifier` entry to the versions map with the same version as `spring-cloud-contract`, so `verifierVersion` in `gradle.properties` is updated correctly.
156+
157+
The format is `{"<property-prefix>": "<existing-project-name>"}`. The value must be a key already present in the parsed versions map; unknown values are silently ignored. This input is only used when `release-train-version` is set.
158+
140159
## Example — Maven multi-module project
141160

142161
Given a `versions` map of `{"spring-boot":"3.2.3","spring-cloud-commons":"4.1.1"}` and `project-version` of `4.1.2`:
@@ -228,3 +247,17 @@ npm run build
228247
git add dist/index.js dist/licenses.txt
229248
git commit -m "rebuild dist"
230249
```
250+
251+
### Testing Locally
252+
253+
You can run node script for this action against a project checked out locally on your machine. Here is an example:
254+
255+
```bash
256+
env \
257+
'INPUT_RELEASE-TRAIN-VERSION=2025.1.0' \
258+
INPUT_DIRECTORY=/git-repos/spring-cloud/spring-cloud-contract \
259+
GITHUB_OUTPUT=/dev/null \
260+
GITHUB_ENV=/dev/null \
261+
'INPUT_PROJECT-VERSION-SUBSTITUTIONS={"verifier":"spring-cloud-contract", "boot":"spring-boot"}' \
262+
node src/index.js
263+
```

.github/actions/update-project-versions/__tests__/fixtures/gradle-project/gradle.properties

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ version=3.1.0
22
springBootVersion=3.2.2
33
springCloudCommonsVersion=4.1.0
44
springCloudBusVersion=4.1.0
5+
springCloudContractVersion=4.1.0-SNAPSHOT
6+
verifierVersion=4.1.0-SNAPSHOT
7+
bootVersion=3.2.2
58
# not a Version key — should be ignored
69
someOtherProperty=foobar
710
# also not matched
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
releaser.fixed-versions[spring-boot]=3.2.3
2+
releaser.fixed-versions[spring-cloud-bus]=4.1.1
3+
releaser.fixed-versions[spring-cloud-commons]=4.1.1
4+
releaser.fixed-versions[spring-cloud-contract]=4.1.1

.github/actions/update-project-versions/__tests__/index.test.js

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -140,11 +140,12 @@ describe('replacePropertyValue', () => {
140140
// ── updateGradlePropertiesContent ─────────────────────────────────────────────
141141

142142
describe('updateGradlePropertiesContent', () => {
143-
const versions = {
144-
'spring-boot': '3.2.3',
145-
'spring-cloud-commons': '4.1.1',
146-
'spring-cloud-bus': '4.1.1',
147-
};
143+
const content = loadFixture('releaser-config', '2024_1_0.properties');
144+
const substitutions = {
145+
'verifier': 'spring-cloud-contract',
146+
'boot': 'spring-boot'
147+
}
148+
const versions = parseReleaserConfig(content, substitutions);
148149
const projectVersion = '3.1.1';
149150

150151
let result;
@@ -165,6 +166,10 @@ describe('updateGradlePropertiesContent', () => {
165166
expect(result.updated).toMatch(/^springBootVersion=3\.2\.3$/m);
166167
expect(result.updatedProperties).toContain('springBootVersion: 3.2.3');
167168
});
169+
it('updates bootVersion', () => {
170+
expect(result.updated).toMatch(/^bootVersion=3\.2\.3$/m);
171+
expect(result.updatedProperties).toContain('bootVersion: 3.2.3');
172+
});
168173

169174
it('updates springCloudCommonsVersion', () => {
170175
expect(result.updated).toMatch(/^springCloudCommonsVersion=4\.1\.1$/m);
@@ -174,6 +179,10 @@ describe('updateGradlePropertiesContent', () => {
174179
expect(result.updated).toMatch(/^springCloudBusVersion=4\.1\.1$/m);
175180
});
176181

182+
it('updates verifierVersion', () => {
183+
expect(result.updated).toMatch(/^verifierVersion=4\.1\.1$/m);
184+
});
185+
177186
it('leaves non-Version keys unchanged', () => {
178187
expect(result.updated).toMatch(/^someOtherProperty=foobar$/m);
179188
expect(result.updated).toMatch(/^releaseFlag=true$/m);
@@ -446,6 +455,22 @@ releaser.fixed-versions[spring-cloud-config]=4.1.1
446455
it('returns an empty map when no fixed-versions entries are present', () => {
447456
expect(parseReleaserConfig('# no versions here\n')).toEqual({});
448457
});
458+
459+
it('adds substitution entries from the versions map', () => {
460+
const content = `
461+
releaser.fixed-versions[spring-cloud-contract]=4.1.0
462+
releaser.fixed-versions[spring-boot]=3.2.3
463+
`;
464+
const versions = parseReleaserConfig(content, { verifier: 'spring-cloud-contract' });
465+
expect(versions['verifier']).toBe('4.1.0');
466+
expect(versions['spring-cloud-contract']).toBe('4.1.0');
467+
});
468+
469+
it('silently ignores substitutions whose value is not in the versions map', () => {
470+
const content = `releaser.fixed-versions[spring-boot]=3.2.3\n`;
471+
const versions = parseReleaserConfig(content, { verifier: 'spring-cloud-contract' });
472+
expect(versions['verifier']).toBeUndefined();
473+
});
449474
});
450475

451476
// ── detectProjectName ─────────────────────────────────────────────────────────
@@ -456,6 +481,19 @@ describe('detectProjectName', () => {
456481
expect(name).toBe('spring-cloud-config');
457482
});
458483

484+
it('strips -parent suffix from the artifactId', () => {
485+
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'detect-project-test-'));
486+
try {
487+
fs.writeFileSync(
488+
path.join(tmpDir, 'pom.xml'),
489+
'<project><artifactId>spring-cloud-task-parent</artifactId></project>'
490+
);
491+
expect(detectProjectName(tmpDir)).toBe('spring-cloud-task');
492+
} finally {
493+
fs.rmSync(tmpDir, { recursive: true, force: true });
494+
}
495+
});
496+
459497
it('throws when no pom.xml is present in the directory', () => {
460498
expect(() => detectProjectName(fixturePath('gradle-project'))).toThrow(
461499
'No root pom.xml found'

.github/actions/update-project-versions/action.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,15 @@ inputs:
4444
description: 'Root directory of the project to update. Defaults to the current working directory.'
4545
required: false
4646
default: '.'
47+
project-version-substitutions:
48+
description: >
49+
A JSON object that maps project names in pom.xml, gradle.properties, or build.gradle to versions when the action uses release-train-version
50+
to derive the versions map and project version. In some projects there may be properties for versions that don't conform to normal
51+
naming conventions. For example, in spring-cloud-contract uses verifierVersion in gradle.properties files. This should map
52+
to the version of spring-cloud-contract from the properties file for the release-train-version. To accomplish this, you
53+
can pass a JSON object {"verifier":"spring-cloud-contract"}. This will map the verifierVersion property in gradle.properties to
54+
the version of spring-cloud-contract in the properties file.
55+
required: false
4756

4857
runs:
4958
using: node20

.github/actions/update-project-versions/dist/index.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28247,12 +28247,23 @@ async function run() {
2824728247
let versions;
2824828248
let projectVersion;
2824928249

28250+
const substitutionsInput = core.getInput('project-version-substitutions');
28251+
let substitutions = {};
28252+
if (substitutionsInput) {
28253+
try {
28254+
substitutions = JSON.parse(substitutionsInput);
28255+
} catch {
28256+
core.setFailed('Invalid JSON supplied for the project-version-substitutions input');
28257+
return;
28258+
}
28259+
}
28260+
2825028261
if (releaseTrainVersion) {
2825128262
// ── Fetch versions from the jenkins-releaser-config properties file ──────
2825228263
const url = getReleaserConfigUrl(commercial, releaseTrainVersion);
2825328264
core.info(`Fetching releaser config from ${url}`);
2825428265
const content = await fetchReleaserConfig(url, token);
28255-
versions = parseReleaserConfig(content);
28266+
versions = parseReleaserConfig(content, substitutions);
2825628267

2825728268
// Auto-detect the project name from the root pom.xml <artifactId>
2825828269
const projectName = detectProjectName(directory);
@@ -28337,6 +28348,8 @@ async function run() {
2833728348
const { changed } = updateBuildGradleVersion(file, projectVersion);
2833828349
if (changed) {
2833928350
core.info(`Updated ${path.relative(directory, file)}: version`);
28351+
} else {
28352+
core.info(`No changes to ${path.relative(directory, file)}: version`);
2834028353
}
2834128354
}
2834228355
}
@@ -28410,7 +28423,7 @@ async function fetchReleaserConfig(url, token) {
2841028423
* @param {string} content - raw text of the properties file
2841128424
* @returns {Record<string, string>} e.g. { "spring-boot": "3.2.3", ... }
2841228425
*/
28413-
function parseReleaserConfig(content) {
28426+
function parseReleaserConfig(content, substitutions = {}) {
2841428427
const versions = {};
2841528428
for (const line of content.split('\n')) {
2841628429
const match = line.match(/^releaser\.fixed-versions\[([^\]]+)\]=(.+)$/);
@@ -28420,6 +28433,11 @@ function parseReleaserConfig(content) {
2842028433
versions[projectName] = version;
2842128434
}
2842228435
}
28436+
for (const [key, value] of Object.entries(substitutions)) {
28437+
if (versions[value] !== undefined) {
28438+
versions[key] = versions[value];
28439+
}
28440+
}
2842328441
return versions;
2842428442
}
2842528443

@@ -28450,7 +28468,7 @@ function detectProjectName(directory) {
2845028468
'Could not auto-detect project name: no <artifactId> found in root pom.xml'
2845128469
);
2845228470
}
28453-
return String(artifactId);
28471+
return artifactIdToProjectName(String(artifactId));
2845428472
}
2845528473

2845628474
// ── pom.xml ────────────────────────────────────────────────────────────────

.github/actions/update-project-versions/src/index.js

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,23 @@ async function run() {
2222
let versions;
2323
let projectVersion;
2424

25+
const substitutionsInput = core.getInput('project-version-substitutions');
26+
let substitutions = {};
27+
if (substitutionsInput) {
28+
try {
29+
substitutions = JSON.parse(substitutionsInput);
30+
} catch {
31+
core.setFailed('Invalid JSON supplied for the project-version-substitutions input');
32+
return;
33+
}
34+
}
35+
2536
if (releaseTrainVersion) {
2637
// ── Fetch versions from the jenkins-releaser-config properties file ──────
2738
const url = getReleaserConfigUrl(commercial, releaseTrainVersion);
2839
core.info(`Fetching releaser config from ${url}`);
2940
const content = await fetchReleaserConfig(url, token);
30-
versions = parseReleaserConfig(content);
41+
versions = parseReleaserConfig(content, substitutions);
3142

3243
// Auto-detect the project name from the root pom.xml <artifactId>
3344
const projectName = detectProjectName(directory);
@@ -112,6 +123,8 @@ async function run() {
112123
const { changed } = updateBuildGradleVersion(file, projectVersion);
113124
if (changed) {
114125
core.info(`Updated ${path.relative(directory, file)}: version`);
126+
} else {
127+
core.info(`No changes to ${path.relative(directory, file)}: version`);
115128
}
116129
}
117130
}
@@ -185,7 +198,7 @@ async function fetchReleaserConfig(url, token) {
185198
* @param {string} content - raw text of the properties file
186199
* @returns {Record<string, string>} e.g. { "spring-boot": "3.2.3", ... }
187200
*/
188-
function parseReleaserConfig(content) {
201+
function parseReleaserConfig(content, substitutions = {}) {
189202
const versions = {};
190203
for (const line of content.split('\n')) {
191204
const match = line.match(/^releaser\.fixed-versions\[([^\]]+)\]=(.+)$/);
@@ -195,6 +208,11 @@ function parseReleaserConfig(content) {
195208
versions[projectName] = version;
196209
}
197210
}
211+
for (const [key, value] of Object.entries(substitutions)) {
212+
if (versions[value] !== undefined) {
213+
versions[key] = versions[value];
214+
}
215+
}
198216
return versions;
199217
}
200218

@@ -225,7 +243,7 @@ function detectProjectName(directory) {
225243
'Could not auto-detect project name: no <artifactId> found in root pom.xml'
226244
);
227245
}
228-
return String(artifactId);
246+
return artifactIdToProjectName(String(artifactId));
229247
}
230248

231249
// ── pom.xml ────────────────────────────────────────────────────────────────

0 commit comments

Comments
 (0)