Skip to content

Commit 1a0144e

Browse files
committed
Fix bug in child pom versions not being updated
1 parent 688761c commit 1a0144e

3 files changed

Lines changed: 144 additions & 24 deletions

File tree

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

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -446,6 +446,56 @@ describe('updatePomFile', () => {
446446
expect(parentBlock).toContain('<version>4.1.1</version>');
447447
});
448448

449+
it('updates parent version in a deep sub-module whose parent is an intermediate aggregate pom', () => {
450+
// Regression test for spring-cloud-function-adapters:
451+
// spring-cloud-function-adapter-aws has parent spring-cloud-function-adapter-parent,
452+
// which is itself a child of the root spring-cloud-function-parent.
453+
// Without internalArtifactIds the intermediate parent name does not match the root,
454+
// so the deep sub-module's parent version was left unchanged.
455+
const srcDir = fixturePath('maven-multi-level');
456+
const destRoot = path.join(tmpDir, 'root');
457+
const destAdapters = path.join(destRoot, 'spring-cloud-function-adapters');
458+
const destAws = path.join(destAdapters, 'spring-cloud-function-adapter-aws');
459+
fs.mkdirSync(destRoot, { recursive: true });
460+
fs.mkdirSync(destAdapters, { recursive: true });
461+
fs.mkdirSync(destAws, { recursive: true });
462+
463+
fs.copyFileSync(path.join(srcDir, 'pom.xml'), path.join(destRoot, 'pom.xml'));
464+
fs.copyFileSync(path.join(srcDir, 'spring-cloud-function-adapters', 'pom.xml'), path.join(destAdapters, 'pom.xml'));
465+
fs.copyFileSync(path.join(srcDir, 'spring-cloud-function-adapters', 'spring-cloud-function-adapter-aws', 'pom.xml'), path.join(destAws, 'pom.xml'));
466+
467+
const rootPom = path.join(destRoot, 'pom.xml');
468+
const adaptersPom = path.join(destAdapters, 'pom.xml');
469+
const awsPom = path.join(destAws, 'pom.xml');
470+
471+
const internalIds = new Set(['spring-cloud-function-parent', 'spring-cloud-function-adapter-parent', 'spring-cloud-function-adapter-aws']);
472+
const versionMap = { 'spring-cloud-build': '4.2.1', 'spring-boot': '3.4.2' };
473+
const currentRoot = '5.0.3';
474+
const newVersion = '5.0.3.1-SNAPSHOT';
475+
476+
updatePomFile(rootPom, true, newVersion, versionMap, currentRoot, 'spring-cloud-function-parent', internalIds);
477+
updatePomFile(adaptersPom, false, newVersion, versionMap, currentRoot, 'spring-cloud-function-parent', internalIds);
478+
updatePomFile(awsPom, false, newVersion, versionMap, currentRoot, 'spring-cloud-function-parent', internalIds);
479+
480+
// Root: own version updated
481+
const rootContent = fs.readFileSync(rootPom, 'utf-8');
482+
expect(rootContent).toMatch(/<artifactId>spring-cloud-function-parent<\/artifactId>\s*<version>5\.0\.3\.1-SNAPSHOT<\/version>/);
483+
// Root: spring-cloud-build parent version updated from versions map
484+
expect(rootContent).toMatch(/<parent>[\s\S]*?<version>4\.2\.1<\/version>[\s\S]*?<\/parent>/);
485+
486+
// Intermediate aggregate: no own <version>, but parent version updated
487+
const adaptersContent = fs.readFileSync(adaptersPom, 'utf-8');
488+
const adaptersParent = adaptersContent.match(/<parent>[\s\S]*?<\/parent>/)[0];
489+
expect(adaptersParent).toContain('<version>5.0.3.1-SNAPSHOT</version>');
490+
491+
// Deep sub-module: parent (spring-cloud-function-adapter-parent) version updated
492+
const awsContent = fs.readFileSync(awsPom, 'utf-8');
493+
const awsParent = awsContent.match(/<parent>[\s\S]*?<\/parent>/)[0];
494+
expect(awsParent).toContain('<version>5.0.3.1-SNAPSHOT</version>');
495+
// spring-boot.version property also updated
496+
expect(awsContent).toContain('<spring-boot.version>3.4.2</spring-boot.version>');
497+
});
498+
449499
});
450500

451501
// ── isChildOfRoot ─────────────────────────────────────────────────────────────
@@ -488,6 +538,24 @@ describe('isChildOfRoot', () => {
488538
it('returns false with no parent element', () => {
489539
expect(isChildOfRoot({}, {}, 'spring-cloud-config')).toBe(false);
490540
});
541+
542+
it('returns true when parent is an intermediate project pom in internalArtifactIds', () => {
543+
// Mirrors spring-cloud-function-adapters case: the leaf adapter has
544+
// spring-cloud-function-adapter-parent (not the root) as its parent.
545+
const internal = new Set(['spring-cloud-function-parent', 'spring-cloud-function-adapter-parent']);
546+
expect(isChildOfRoot(makeProject('spring-cloud-function-adapter-parent'), {}, null, internal)).toBe(true);
547+
});
548+
549+
it('returns false when parent is external even though versions map is empty (internalArtifactIds provided)', () => {
550+
const internal = new Set(['spring-cloud-function-parent', 'spring-cloud-function-adapter-parent']);
551+
expect(isChildOfRoot(makeProject('spring-boot-dependencies'), {}, null, internal)).toBe(false);
552+
});
553+
554+
it('internalArtifactIds takes precedence over rootArtifactId', () => {
555+
// rootArtifactId alone would return false for adapter-parent, but internalArtifactIds knows it's internal
556+
const internal = new Set(['spring-cloud-function-parent', 'spring-cloud-function-adapter-parent']);
557+
expect(isChildOfRoot(makeProject('spring-cloud-function-adapter-parent'), {}, 'spring-cloud-function-parent', internal)).toBe(true);
558+
});
491559
});
492560

493561
// ── findFiles ──────────────────────────────────────────────────────────────────

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

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -28320,6 +28320,19 @@ async function run() {
2832028320
: null;
2832128321
}
2832228322

28323+
// Pre-scan all pom files to collect every artifactId that belongs to this
28324+
// project. This lets isChildOfRoot recognise intermediate aggregate parent poms
28325+
// (e.g. spring-cloud-function-adapter-parent inside spring-cloud-function-adapters/)
28326+
// so that their child poms have their parent version updated correctly.
28327+
const internalArtifactIds = new Set();
28328+
if (rootArtifactId) internalArtifactIds.add(rootArtifactId);
28329+
const pomParser = new XMLParser({ ignoreAttributes: false });
28330+
for (const file of pomFiles) {
28331+
const parsed = pomParser.parse(fs.readFileSync(file, 'utf-8'));
28332+
const aid = parsed?.project?.artifactId;
28333+
if (aid) internalArtifactIds.add(String(aid));
28334+
}
28335+
2832328336
for (const file of pomFiles) {
2832428337
const isRoot = path.resolve(file) === path.resolve(rootPom);
2832528338
const { changed, updatedProperties } = updatePomFile(
@@ -28328,7 +28341,8 @@ async function run() {
2832828341
projectVersion,
2832928342
versions,
2833028343
currentRootVersion,
28331-
rootArtifactId
28344+
rootArtifactId,
28345+
internalArtifactIds
2833228346
);
2833328347
if (changed) {
2833428348
core.info(`Updated ${path.relative(directory, file)}: ${updatedProperties.join(', ')}`);
@@ -28516,8 +28530,10 @@ function detectProjectName(directory) {
2851628530
* @param {Record<string, string>} versions
2851728531
* @param {string|null} currentRootVersion - the root pom's version before any edits;
2851828532
* non-root poms whose own <version> equals this value will also have it updated
28533+
* @param {Set<string>|null} internalArtifactIds - set of all artifactIds found in
28534+
* this project's pom files; used to recognise intermediate parent poms as internal
2851928535
*/
28520-
function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVersion = null, rootArtifactId = null) {
28536+
function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVersion = null, rootArtifactId = null, internalArtifactIds = null) {
2852128537
const content = fs.readFileSync(filePath, 'utf-8');
2852228538
let updated = content;
2852328539
const updatedProperties = [];
@@ -28565,7 +28581,7 @@ function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVe
2856528581
? versions[parentArtifactId]
2856628582
: versions[parentName] !== undefined
2856728583
? versions[parentName]
28568-
: isChildOfRoot(project, versions, rootArtifactId)
28584+
: isChildOfRoot(project, versions, rootArtifactId, internalArtifactIds)
2856928585
? projectVersion
2857028586
: null;
2857128587

@@ -28815,20 +28831,26 @@ function artifactIdToProjectName(artifactId) {
2881528831
}
2881628832

2881728833
/**
28818-
* Returns true when a child pom's parent is the root project of this repo
28834+
* Returns true when a child pom's parent is part of this project
2881928835
* (i.e. not an external parent like spring-boot-starter-parent or
2882028836
* spring-cloud-dependencies-parent).
2882128837
*
28822-
* When rootArtifactId is supplied (read from the root pom at runtime) the parent
28823-
* must both (a) be absent from the external versions map AND (b) match the root
28824-
* artifactId or its stripped form. This prevents external parents that happen to
28825-
* be absent from the versions map (e.g. spring-boot-starter-parent when versions
28826-
* is empty) from being incorrectly stamped with the project version.
28838+
* When internalArtifactIds is supplied (a Set of every artifactId found in the
28839+
* project's pom files) the parent must be a member of that set. This correctly
28840+
* handles multi-level module hierarchies where an intermediate aggregate pom
28841+
* (e.g. spring-cloud-function-adapter-parent) is itself a parent of deeper
28842+
* sub-modules — those sub-modules would be missed when only comparing against
28843+
* the root artifactId.
2882728844
*
28828-
* When rootArtifactId is null (e.g. in unit tests that don't supply it) the
28829-
* function falls back to the looser heuristic of "not in versions map".
28845+
* When only rootArtifactId is supplied the parent must match the root artifactId
28846+
* or its stripped form. This prevents external parents absent from the versions
28847+
* map (e.g. spring-boot-starter-parent when versions is empty) from being
28848+
* incorrectly stamped with the project version.
28849+
*
28850+
* When neither is supplied the function falls back to the looser heuristic of
28851+
* "not in versions map" (used only in unit tests that don't supply either).
2883028852
*/
28831-
function isChildOfRoot(project, versions, rootArtifactId = null) {
28853+
function isChildOfRoot(project, versions, rootArtifactId = null, internalArtifactIds = null) {
2883228854
const parentArtifactId = project?.parent?.artifactId;
2883328855
if (!parentArtifactId) return false;
2883428856
const parentName = artifactIdToProjectName(parentArtifactId);
@@ -28837,6 +28859,10 @@ function isChildOfRoot(project, versions, rootArtifactId = null) {
2883728859
return false;
2883828860
}
2883928861

28862+
if (internalArtifactIds !== null) {
28863+
return internalArtifactIds.has(parentArtifactId) || internalArtifactIds.has(parentName);
28864+
}
28865+
2884028866
if (rootArtifactId !== null) {
2884128867
const rootName = artifactIdToProjectName(rootArtifactId);
2884228868
return (

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

Lines changed: 38 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,19 @@ async function run() {
9595
: null;
9696
}
9797

98+
// Pre-scan all pom files to collect every artifactId that belongs to this
99+
// project. This lets isChildOfRoot recognise intermediate aggregate parent poms
100+
// (e.g. spring-cloud-function-adapter-parent inside spring-cloud-function-adapters/)
101+
// so that their child poms have their parent version updated correctly.
102+
const internalArtifactIds = new Set();
103+
if (rootArtifactId) internalArtifactIds.add(rootArtifactId);
104+
const pomParser = new XMLParser({ ignoreAttributes: false });
105+
for (const file of pomFiles) {
106+
const parsed = pomParser.parse(fs.readFileSync(file, 'utf-8'));
107+
const aid = parsed?.project?.artifactId;
108+
if (aid) internalArtifactIds.add(String(aid));
109+
}
110+
98111
for (const file of pomFiles) {
99112
const isRoot = path.resolve(file) === path.resolve(rootPom);
100113
const { changed, updatedProperties } = updatePomFile(
@@ -103,7 +116,8 @@ async function run() {
103116
projectVersion,
104117
versions,
105118
currentRootVersion,
106-
rootArtifactId
119+
rootArtifactId,
120+
internalArtifactIds
107121
);
108122
if (changed) {
109123
core.info(`Updated ${path.relative(directory, file)}: ${updatedProperties.join(', ')}`);
@@ -291,8 +305,10 @@ function detectProjectName(directory) {
291305
* @param {Record<string, string>} versions
292306
* @param {string|null} currentRootVersion - the root pom's version before any edits;
293307
* non-root poms whose own <version> equals this value will also have it updated
308+
* @param {Set<string>|null} internalArtifactIds - set of all artifactIds found in
309+
* this project's pom files; used to recognise intermediate parent poms as internal
294310
*/
295-
function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVersion = null, rootArtifactId = null) {
311+
function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVersion = null, rootArtifactId = null, internalArtifactIds = null) {
296312
const content = fs.readFileSync(filePath, 'utf-8');
297313
let updated = content;
298314
const updatedProperties = [];
@@ -340,7 +356,7 @@ function updatePomFile(filePath, isRoot, projectVersion, versions, currentRootVe
340356
? versions[parentArtifactId]
341357
: versions[parentName] !== undefined
342358
? versions[parentName]
343-
: isChildOfRoot(project, versions, rootArtifactId)
359+
: isChildOfRoot(project, versions, rootArtifactId, internalArtifactIds)
344360
? projectVersion
345361
: null;
346362

@@ -590,20 +606,26 @@ function artifactIdToProjectName(artifactId) {
590606
}
591607

592608
/**
593-
* Returns true when a child pom's parent is the root project of this repo
609+
* Returns true when a child pom's parent is part of this project
594610
* (i.e. not an external parent like spring-boot-starter-parent or
595611
* spring-cloud-dependencies-parent).
596612
*
597-
* When rootArtifactId is supplied (read from the root pom at runtime) the parent
598-
* must both (a) be absent from the external versions map AND (b) match the root
599-
* artifactId or its stripped form. This prevents external parents that happen to
600-
* be absent from the versions map (e.g. spring-boot-starter-parent when versions
601-
* is empty) from being incorrectly stamped with the project version.
613+
* When internalArtifactIds is supplied (a Set of every artifactId found in the
614+
* project's pom files) the parent must be a member of that set. This correctly
615+
* handles multi-level module hierarchies where an intermediate aggregate pom
616+
* (e.g. spring-cloud-function-adapter-parent) is itself a parent of deeper
617+
* sub-modules — those sub-modules would be missed when only comparing against
618+
* the root artifactId.
602619
*
603-
* When rootArtifactId is null (e.g. in unit tests that don't supply it) the
604-
* function falls back to the looser heuristic of "not in versions map".
620+
* When only rootArtifactId is supplied the parent must match the root artifactId
621+
* or its stripped form. This prevents external parents absent from the versions
622+
* map (e.g. spring-boot-starter-parent when versions is empty) from being
623+
* incorrectly stamped with the project version.
624+
*
625+
* When neither is supplied the function falls back to the looser heuristic of
626+
* "not in versions map" (used only in unit tests that don't supply either).
605627
*/
606-
function isChildOfRoot(project, versions, rootArtifactId = null) {
628+
function isChildOfRoot(project, versions, rootArtifactId = null, internalArtifactIds = null) {
607629
const parentArtifactId = project?.parent?.artifactId;
608630
if (!parentArtifactId) return false;
609631
const parentName = artifactIdToProjectName(parentArtifactId);
@@ -612,6 +634,10 @@ function isChildOfRoot(project, versions, rootArtifactId = null) {
612634
return false;
613635
}
614636

637+
if (internalArtifactIds !== null) {
638+
return internalArtifactIds.has(parentArtifactId) || internalArtifactIds.has(parentName);
639+
}
640+
615641
if (rootArtifactId !== null) {
616642
const rootName = artifactIdToProjectName(rootArtifactId);
617643
return (

0 commit comments

Comments
 (0)