diff --git a/apps/download/changelog.cfc b/apps/download/changelog.cfc new file mode 100644 index 0000000..ce7bc22 --- /dev/null +++ b/apps/download/changelog.cfc @@ -0,0 +1,229 @@ +component { + + function init( required download ) { + variables.download = arguments.download; + return this; + } + + /** + * Process raw versions from the update provider and filter them for the changelog + * @versions The raw versions struct from download.getVersions() + * @returns Struct with { major: {}, latestSnapshots: {} } + */ + function processVersions( required struct versions ) localmode=true { + var major = {}; + var snapshots = {}; + + // add types to each version + loop struct=arguments.versions index="local.vs" item="local.data" { + if ( findNoCase( "-snapshot", data.version ) ) { + data['type'] = "snapshots"; + } else if ( findNoCase( "-rc", data.version ) ) { + data['type'] = "rc"; + } else if ( findNoCase( "-beta", data.version ) ) { + data['type'] = "beta"; + } else if ( findNoCase( "-alpha", data.version ) ) { + data['type'] = "alpha"; + } else { + data['type'] = "releases"; + } + data['versionNoAppendix'] = data.version; + + if ( data.type != "snapshots" ) { + major[ vs ] = data; + } else { + // Track all snapshots - key (vs) is already the sorted version + // Extract major version from key: "07" from "07.000.001.0044.000" + var majorVersion = listFirst( vs, "." ); + + // Keep only the latest snapshot per major version (highest key) + if ( !structKeyExists( snapshots, majorVersion ) || vs > snapshots[ majorVersion ].key ) { + snapshots[ majorVersion ] = { + key: vs, + data: data + }; + } + } + } + + // Promote the latest snapshot per major version to major + // BUT only if there isn't already an RC/Beta/Release with the same version + structEach( snapshots, function( majorVer, snapshot ) { + var snapshotKey = snapshot.key; + var snapshotData = snapshot.data; + + // Extract version prefix (first 4 parts) to check for duplicates + // e.g., "07.000.001.0044" from "07.000.001.0044.000" + var versionPrefix = listDeleteAt( snapshotKey, listLen( snapshotKey, "." ), "." ); + + // Check if there's already an RC/Beta/Release with same version + var hasDuplicate = false; + loop list=".050,.075,.100" index="local.qualifier" { + if ( structKeyExists( major, versionPrefix & qualifier ) ) { + hasDuplicate = true; + break; + } + } + + // If no duplicate, promote this snapshot to major + if ( !hasDuplicate ) { + major[ snapshotKey ] = snapshotData; + } + }); + + return { + major: major, + latestSnapshots: snapshots + }; + } + + /** + * Get sorted array of version keys for display + * @major The major versions struct + * @returns Array of version keys, sorted newest first + */ + function getSortedVersions( required struct major ) localmode=true { + return structKeyArray( arguments.major ).reverse().sort( "text", "desc" ); + } + + /** + * Sort changelog struct keys by version, filtering to only include versions matching the major version filter + * @changelog The changelog struct from download.getChangelog() - keys are fix versions, values are ticket structs + * @majorVersionFilter The major version to filter by (e.g., "7.0") + * @returns Array of version keys, sorted newest first, filtered to only include matching major versions + */ + function getSortedChangelogVersions( required struct changelog, required string majorVersionFilter ) localmode=true { + var versions = structKeyArray( arguments.changelog ); + + // Filter to only include versions that start with the major version filter + var filtered = []; + loop array=versions index="local.i" item="local.ver" { + if ( left( ver, len( arguments.majorVersionFilter ) ) eq arguments.majorVersionFilter ) { + arrayAppend( filtered, ver ); + } + } + + // Sort using proper version sorting (convert to sortable format first) + // Use callback to compare using toVersionSortable format + arraySort( filtered, function( v1, v2 ) { + try { + var sorted1 = toVersionSortable( arguments.v1 ); + var sorted2 = toVersionSortable( arguments.v2 ); + // Descending order (newest first) + return compare( sorted2, sorted1 ); + } catch ( any e ) { + // Fallback to text comparison if version parsing fails + return compareNoCase( arguments.v2, arguments.v1 ); + } + }); + + return filtered; + } + + /** + * Build the changelog data array for a specific major version + * @versions The processed versions struct (from processVersions) + * @arrVersions Array of sorted version keys (from getSortedVersions) + * @majorVersionFilter Major version to filter by (e.g., "7.0") + * @returns Array of structs with version metadata and changelog data + */ + function buildChangelogData( required struct versions, required array arrVersions, required string majorVersionFilter ) localmode=true { + var arrChangeLogs = []; + + loop array=arguments.arrVersions index="local.idx" item="local._version" { + var version = arguments.versions[ _version ].version; + var prevVersion = ""; + + // Determine previous version for changelog range + if ( idx lt arrayLen( arguments.arrVersions ) ) { + prevVersion = arguments.versions[ arguments.arrVersions[ idx + 1 ] ].version; + } else { + // Last version - use the oldest version from the sorted array (last item) + var lastKey = arguments.arrVersions[ arrayLen( arguments.arrVersions ) ]; + prevVersion = arguments.versions[ lastKey ].version; + } + + // Determine header type and title + var versionTitle = version; + var header = "h4"; + switch( arguments.versions[ _version ].type ) { + case "releases": + header = "h2"; + versionTitle &= " Stable"; + break; + } + + // Fetch changelog only if version matches the major version filter + var changelog = {}; + var versionReleaseDate = ""; + if ( left( version, len( arguments.majorVersionFilter ) ) eq arguments.majorVersionFilter ) { + changelog = variables.download.getChangelog( prevVersion, version, false, true ); + versionReleaseDate = variables.download.getReleaseDate( version ); + } + + if ( !isStruct( changelog ) ) { + changelog = {}; + } + + arrayAppend( arrChangeLogs, { + version: version, + _version: _version, + type: arguments.versions[ _version ].type, + prevVersion: prevVersion, + versionReleaseDate: versionReleaseDate, + changelog: changelog, + header: header, + versionTitle: versionTitle + }); + } + + return arrChangeLogs; + } + + /** + * Convert a version string to sortable format (from VersionUtils.cfc logic) + * @version Version string like "7.0.1.44-SNAPSHOT" + * @returns Sortable version string like "07.000.001.0044.000" + */ + private function toVersionSortable( required string version ) localmode=true { + var arr = listToArray( arguments.version, '.' ); + + if ( arr.len() != 4 || !isNumeric( arr[ 1 ] ) || !isNumeric( arr[ 2 ] ) || !isNumeric( arr[ 3 ] ) ) { + throw "version number [" & arguments.version & "] is invalid"; + } + + var sct = { + major: arr[ 1 ] + 0, + minor: arr[ 2 ] + 0, + micro: arr[ 3 ] + 0, + qualifier_appendix: "", + qualifier_appendix_nbr: 100 + }; + + // qualifier has an appendix? (BETA,SNAPSHOT) + var qArr = listToArray( arr[ 4 ], '-' ); + if ( qArr.len() == 1 && isNumeric( qArr[ 1 ] ) ) { + sct.qualifier = qArr[ 1 ] + 0; + } else if ( qArr.len() == 2 && isNumeric( qArr[ 1 ] ) ) { + sct.qualifier = qArr[ 1 ] + 0; + sct.qualifier_appendix = qArr[ 2 ]; + if ( sct.qualifier_appendix == "SNAPSHOT" ) { + sct.qualifier_appendix_nbr = 0; + } else if ( sct.qualifier_appendix == "BETA" ) { + sct.qualifier_appendix_nbr = 50; + } else { + sct.qualifier_appendix_nbr = 75; // every other appendix is better than SNAPSHOT + } + } else { + sct.qualifier = qArr[ 1 ] + 0; + sct.qualifier_appendix_nbr = 75; + } + + return repeatString( "0", 2 - len( sct.major ) ) & sct.major + & "." & repeatString( "0", 3 - len( sct.minor ) ) & sct.minor + & "." & repeatString( "0", 3 - len( sct.micro ) ) & sct.micro + & "." & repeatString( "0", 4 - len( sct.qualifier ) ) & sct.qualifier + & "." & repeatString( "0", 3 - len( sct.qualifier_appendix_nbr ) ) & sct.qualifier_appendix_nbr; + } + +} diff --git a/apps/download/changelog/index.cfm b/apps/download/changelog/index.cfm index 0402fe5..ac6b64b 100644 --- a/apps/download/changelog/index.cfm +++ b/apps/download/changelog/index.cfm @@ -62,48 +62,15 @@ } versions=tmp; - major = {}; - preRelease = {}; - // add types - //releases,snapshots,rc,beta - loop struct=versions index="vs" item="data" { - if(findNoCase("-snapshot",data.version)) data['type']="snapshots"; - else if(findNoCase("-rc",data.version)) data['type']="rc"; - else if(findNoCase("-beta",data.version)) data['type']="beta"; - else if(findNoCase("-alpha",data.version)) data['type']="alpha"; - else data['type']="releases"; - data['versionNoAppendix']=data.version; - if ( data.type != "snapshots" ) { - major[ vs ] = data; - } else { - // need to find latest pre release builds - if ( structKeyExists( data, "versionSorted" ) ){ - v = ArrayToList( ArraySlice( listToArray( data.versionSorted,"." ), 1 , 2 ), "." ); - preRelease[ v ] = { - versionSorted: data.versionSorted, - versionNoAppendix: data.versionNoAppendix - }; - } - } - } - // avoid showingh a snapshot for a release etc - structEach( preRelease, function( k, v ) { - var releaseVersionSorted = left( v.versionSorted, len( v.versionSorted ) -4 ); - // check for RC / BETA / SNAPSHOT with the same version - arrayEach( [ ".050",".100",".075" ], function( i ){ - if ( structKeyExists( major, releaseVersionSorted & arguments.i ) ) - structDelete( preRelease, k ); - }); - }); - structEach( preRelease, function( k, v ) { - major[ v.versionSorted ] = { - version: v.versionNoAppendix, - type: "snapshot" - }; - }); + // Use changelog.cfc to process versions + changelogService = CreateObject( "component", "changelog" ).init( download ); + processedVersions = changelogService.processVersions( versions ); + major = processedVersions.major; - arrVersions = structKeyArray(major).reverse().sort("text","desc"); - arrChangeLogs = []; + arrVersions = changelogService.getSortedVersions( major ); + + // Build changelog data array using the new method + arrChangeLogs = changelogService.buildChangelogData( major, arrVersions, url.version ); function getBadgeForType( type ) { switch(arguments.type){ @@ -132,66 +99,6 @@

Lucee Server Changelogs - #url.version#

- - - - - version = versions[ _version ].version; - if (idx lt ArrayLen(arrVersions)){ - prevVersion = versions[arrVersions[ idx + 1 ]].version; - } else { - prevVersion = structKeyArray(versions); - prevVersion = versions[prevVersion[arrayLen(prevVersion)]].version; - } - versionTitle = version; - switch(versions[_version].type){ - case "releases": - header="h2"; - versionTitle &= " Stable"; - break; - default: - header="h4"; - } - changelog = {}; - versionReleaseDate = ""; - if ( left( version, 3 ) eq url.version ){ - changeLog = download.getChangelog( prevVersion, version, false, true ); - versionReleaseDate = download.getReleaseDate(version); - } - if (!isStruct(changelog)) - changelog = {}; - - arrayAppend(arrChangeLogs, { - version: version, - _version: _version, - type: versions[_version].type, - prevVersion: prevVersion, - versionReleaseDate: versionReleaseDate, - changelog: changelog, - header: header, - versionTitle: versionTitle - }); - /* - structEach(changeLog, function(cl){ - structEach(changeLog[cl], function( ticket ){ - var _type= changeLog[ cl ][ ticket ].type; - if ( !structKeyExists( ticketTypes, _type ) ) - ticketTypes[ _type ]=0; - ticketTypes[ _type ]++; - - var _labels = changeLog [cl ][ ticket ].labels; - arrayEach(_labels, function(_label) { - if (!structKeyExists( ticketLabels, _label ) ) - ticketLabels[_label]=0; - ticketLabels[_label]++; - }); - }); - }); - */ - - - -
@@ -288,7 +195,9 @@ - + + + to ) continue; + if ( fvs <= from || fvs > to ) continue; if( !structKeyExists( sct, fv ) ) sct[ fv ] = structNew( "linked" ); if (arguments.detailed) diff --git a/tests/testChangelog.cfc b/tests/testChangelog.cfc new file mode 100644 index 0000000..b520568 --- /dev/null +++ b/tests/testChangelog.cfc @@ -0,0 +1,205 @@ +component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider" { + + function beforeAll() { + variables.dir = getDirectoryFromPath( getCurrentTemplatePath() ); + application action="update" mappings={ + "/download" : expandPath( dir & "../apps/download" ) + }; + variables.changelog = new download.changelog( new download.download() ); + } + + function run( testResults, testBox ) { + describe( "changelog version filtering", function() { + + it( "should include RC versions in major", function() { + var versions = { + "07.000.000.0392.075": { + "version": "7.0.0.392-RC", + "war": "lucee-7.0.0.392-RC.war" + } + }; + + var result = changelog.processVersions( versions ); + + expect( result.major ).toHaveKey( "07.000.000.0392.075" ); + expect( result.major[ "07.000.000.0392.075" ].type ).toBe( "rc" ); + }); + + it( "should include latest snapshot per major version in major", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "war": "lucee-7.0.1.44-SNAPSHOT.war" + }, + "07.000.001.0043.000": { + "version": "7.0.1.43-SNAPSHOT", + "war": "lucee-7.0.1.43-SNAPSHOT.war" + } + }; + + var result = changelog.processVersions( versions ); + + // Only the latest snapshot (7.0.1.44) should be in major + expect( result.major ).toHaveKey( "07.000.001.0044.000" ); + expect( result.major ).notToHaveKey( "07.000.001.0043.000" ); + expect( result.major[ "07.000.001.0044.000" ].type ).toBe( "snapshots" ); + }); + + it( "should show 7.0.1.44-SNAPSHOT as lead version with 7.0.0.392-RC also showing", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "war": "lucee-7.0.1.44-SNAPSHOT.war" + }, + "07.000.000.0392.075": { + "version": "7.0.0.392-RC", + "war": "lucee-7.0.0.392-RC.war" + } + }; + + var result = changelog.processVersions( versions ); + + // Both should be in major - snapshot is newer so it's the lead version + expect( result.major ).toHaveKey( "07.000.001.0044.000" ); + expect( result.major ).toHaveKey( "07.000.000.0392.075" ); + expect( result.major[ "07.000.001.0044.000" ].type ).toBe( "snapshots" ); + expect( result.major[ "07.000.000.0392.075" ].type ).toBe( "rc" ); + }); + + it( "should filter out snapshots when a release exists for same version", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "versionSorted": "07.000.001.0044.000", + "war": "lucee-7.0.1.44-SNAPSHOT.war" + }, + "07.000.001.0044.100": { + "version": "7.0.1.44", + "war": "lucee-7.0.1.44.war" + } + }; + + var result = changelog.processVersions( versions ); + + // snapshot should be removed from major because release exists + expect( result.major ).toHaveKey( "07.000.001.0044.100" ); + expect( result.major ).notToHaveKey( "07.000.001.0044.000" ); + }); + + it( "should sort versions newest first", function() { + var major = { + "07.000.000.0392.075": { "version": "7.0.0.392-RC" }, + "07.000.001.0044.000": { "version": "7.0.1.44-SNAPSHOT" }, + "07.000.000.0388.075": { "version": "7.0.0.388-RC" } + }; + + var sorted = changelog.getSortedVersions( major ); + + expect( sorted[ 1 ] ).toBe( "07.000.001.0044.000" ); + expect( sorted[ 2 ] ).toBe( "07.000.000.0392.075" ); + expect( sorted[ 3 ] ).toBe( "07.000.000.0388.075" ); + }); + + }); + + describe( "buildChangelogData method", function() { + + it( "should build changelog data array with correct structure", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "type": "snapshots" + }, + "07.000.000.0392.075": { + "version": "7.0.0.392-RC", + "type": "rc" + } + }; + + var arrVersions = [ "07.000.001.0044.000", "07.000.000.0392.075" ]; + + var result = changelog.buildChangelogData( versions, arrVersions, "7.0" ); + + expect( result ).toBeArray(); + expect( arrayLen( result ) ).toBe( 2 ); + + // Check first version (latest snapshot) + expect( result[ 1 ].version ).toBe( "7.0.1.44-SNAPSHOT" ); + expect( result[ 1 ].type ).toBe( "snapshots" ); + expect( result[ 1 ]._version ).toBe( "07.000.001.0044.000" ); + expect( result[ 1 ].prevVersion ).toBe( "7.0.0.392-RC" ); + expect( result[ 1 ].header ).toBe( "h4" ); + + // Check second version (RC) + expect( result[ 2 ].version ).toBe( "7.0.0.392-RC" ); + expect( result[ 2 ].type ).toBe( "rc" ); + }); + + it( "should set h2 header for releases", function() { + var versions = { + "07.000.001.0044.100": { + "version": "7.0.1.44", + "type": "releases" + } + }; + + var arrVersions = [ "07.000.001.0044.100" ]; + + var result = changelog.buildChangelogData( versions, arrVersions, "7.0" ); + + expect( result[ 1 ].header ).toBe( "h2" ); + expect( result[ 1 ].versionTitle ).toBe( "7.0.1.44 Stable" ); + }); + + it( "should only fetch changelog for matching major version", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "type": "snapshots" + }, + "06.002.000.0030.100": { + "version": "6.2.0.30", + "type": "releases" + } + }; + + var arrVersions = [ "07.000.001.0044.000", "06.002.000.0030.100" ]; + + var result = changelog.buildChangelogData( versions, arrVersions, "7.0" ); + + // 7.0 version should have changelog fetched (non-empty) + // 6.2 version should have empty changelog struct + expect( result[ 1 ].version ).toBe( "7.0.1.44-SNAPSHOT" ); + expect( result[ 2 ].version ).toBe( "6.2.0.30" ); + expect( structCount( result[ 2 ].changelog ) ).toBe( 0 ); + }); + + it( "should determine prevVersion correctly for last item", function() { + var versions = { + "07.000.001.0044.000": { + "version": "7.0.1.44-SNAPSHOT", + "type": "snapshots" + }, + "07.000.000.0392.075": { + "version": "7.0.0.392-RC", + "type": "rc" + }, + "07.000.000.0388.075": { + "version": "7.0.0.388-RC", + "type": "rc" + } + }; + + var arrVersions = [ "07.000.001.0044.000", "07.000.000.0392.075", "07.000.000.0388.075" ]; + + var result = changelog.buildChangelogData( versions, arrVersions, "7.0" ); + + // Last item should use the oldest version as prevVersion + expect( result[ 3 ].version ).toBe( "7.0.0.388-RC" ); + expect( result[ 3 ].prevVersion ).toBe( "7.0.0.388-RC" ); + }); + + }); + } + +} diff --git a/tests/testJiraChangelog.cfc b/tests/testJiraChangelog.cfc index d4b25c6..13f25d3 100644 --- a/tests/testJiraChangelog.cfc +++ b/tests/testJiraChangelog.cfc @@ -1,4 +1,4 @@ -component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider-integration" { +component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider" { function beforeAll(){ variables.dir = getDirectoryFromPath( getCurrentTemplatePath() ); @@ -76,6 +76,64 @@ component extends="org.lucee.cfml.test.LuceeTestCase" labels="data-provider-inte } } ); + it( title="test getChangelog includes upper boundary version", body=function(){ + var service = new services.JiraChangeLogService(); + service.setS3Root( expandPath( dir & "../apps/updateserver/services/legacy/build/servers/" ) ); + + try { + service.loadIssues( force=false ); + var issues = service.getIssues(); + + // Skip test if no issues loaded + if ( issues.recordCount eq 0 ) { + systemOutput( "No issues loaded, skipping boundary test", true ); + return; + } + + // Find a ticket with a specific fix version to test with + var testVersion = ""; + loop query=issues { + if ( isArray( issues.fixVersions ) && arrayLen( issues.fixVersions ) > 0 ) { + testVersion = issues.fixVersions[ 1 ]; + break; + } + } + + if ( len( testVersion ) eq 0 ) { + systemOutput( "No issues with fix versions found, skipping boundary test", true ); + return; + } + + if ( isArray( testVersion ) ) { + systemOutput( "testVersion is still an array, using first element", true ); + testVersion = testVersion[ 1 ]; + } + + systemOutput( "Testing boundary with version: #testVersion#", true ); + + // Get changelog with range that should include this exact version as upper boundary + var changelog = service.getChangelog( + versionFrom = "6.0.0.0", + versionTo = testVersion, + detailed = false + ); + + expect( changelog ).toBeStruct(); + + // The changelog should include the testVersion as a key + if ( structKeyExists( changelog, testVersion ) ) { + systemOutput( "PASS: Upper boundary version #testVersion# is included in changelog", true ); + expect( structCount( changelog[ testVersion ] ) ).toBeGT( 0 ); + } else { + systemOutput( "WARNING: Version #testVersion# not found in changelog keys: #structKeyList( changelog )#", true ); + // This could be legitimate if the version is outside the range we're testing + } + } catch ( any e ) { + systemOutput( "Error testing boundary condition: #e.message#", true ); + rethrow; + } + } ); + } ); }