Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,43 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

---

## [0.2.0] - 2026-01-25

### Added
- **Full version string for hosting panels** - Added `FullVersion` field to artifact entries
- Format: `{version}-{hash}` (e.g., `24769-315823736cfbc085104ca0d32779311cd2f1a5a8`)
- Compatible with Pterodactyl, Pelican, and similar hosting panel egg configurations

- **Artifact statistics in API response** - Added `stats` object to metadata
- Includes counts for: `total`, `recommended`, `latest`, `active`, `deprecated`, `eol`
- Calculated from filtered results before pagination
- Enables frontend to show accurate totals regardless of current page

### Fixed
- **Pagination total count** - Fixed incorrect total count in pagination metadata
- Previously returned count of paginated results instead of total filtered results
- Created `ArtifactsResult` struct to properly track total count after filtering but before pagination
- `hasMore` now correctly indicates if more pages are available

- **Latest vs Recommended logic** - Fixed support status assignment per CFX EOL policy
- **Latest** = Single newest version (for testing/bleeding edge)
- **Recommended** = Next 3 versions after Latest (stable for production)
- Support status now dynamically assigned based on version position, not hardcoded thresholds
- See https://aka.cfx.re/eol for CFX official policy

- **EOL filter default** - Changed `includeEol` default from `true` to `false`
- EOL artifacts are now excluded by default for safety
- Users must explicitly opt-in to see end-of-life versions

### Changed
- Updated all artifact handlers to use new `ArtifactsResult` return type
- Refactored `generateFullVersion()` helper to accept hash parameter
- Added `ArtifactStats` struct and `calculateStats()` helper function
- Refactored `ProcessGitHubTags` to dynamically assign Latest/Recommended based on sorted position
- Simplified `determineSupportStatus()` to only handle Active/Deprecated/EOL thresholds

---

## [0.1.0] - 2026-01-25

### Added
Expand Down
10 changes: 9 additions & 1 deletion internal/handlers/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ func GetArtifacts(c *fiber.Ctx) error {
Status: services.SupportStatus(c.Query("status", "")),
SortBy: c.Query("sortBy", "version"),
SortOrder: c.Query("sortOrder", "desc"),
IncludeEOL: c.QueryBool("includeEol", true),
IncludeEOL: c.QueryBool("includeEol", false),
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The API documentation comment indicates the default value for includeEol is true, but the actual default has been changed to false on line 40. The documentation should be updated to reflect the new default value.

Copilot uses AI. Check for mistakes.
}

// Parse limit and offset
Expand Down Expand Up @@ -91,6 +91,14 @@ func GetArtifacts(c *fiber.Ctx) error {
"hasMore": offset+len(artifacts) < totalFiltered,
"platforms": platforms,
"supportStatuses": statuses,
"stats": fiber.Map{
"total": result.Stats.Total,
"recommended": result.Stats.Recommended,
"latest": result.Stats.Latest,
"active": result.Stats.Active,
"deprecated": result.Stats.Deprecated,
"eol": result.Stats.EOL,
},
"query": fiber.Map{
"platform": query.Platform,
"version": query.Version,
Expand Down
80 changes: 66 additions & 14 deletions internal/services/artifacts.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,21 @@ type ArtifactEntry struct {
Size int64
}

// ArtifactStats holds counts by support status
type ArtifactStats struct {
Total int `json:"total"`
Recommended int `json:"recommended"`
Latest int `json:"latest"`
Active int `json:"active"`
Deprecated int `json:"deprecated"`
EOL int `json:"eol"`
}

// ArtifactsResult holds paginated results with metadata
type ArtifactsResult struct {
Data []ArtifactEntry
Total int
Stats ArtifactStats
FilteredBy string
}

Expand Down Expand Up @@ -228,7 +239,7 @@ func (s *ArtifactsService) ProcessGitHubTags(tags []GitHubTag) ArtifactData {
}
}

// Sort by version number (descending)
// Sort by version number (descending) - highest version first
sort.Slice(artifactTags, func(i, j int) bool {
versionA := s.extractVersionNumber(artifactTags[i].Name)
versionB := s.extractVersionNumber(artifactTags[j].Name)
Expand All @@ -237,15 +248,28 @@ func (s *ArtifactsService) ProcessGitHubTags(tags []GitHubTag) ArtifactData {

now := time.Now().UTC().Format(time.RFC3339)

for _, tag := range artifactTags {
for idx, tag := range artifactTags {
versionNumber := s.extractVersionNumber(tag.Name)
version := strconv.Itoa(versionNumber)

// Determine support status based on position in sorted list
// Position 0 = Latest (newest single version)
// Position 1-3 = Recommended (stable versions)
Copy link

Copilot AI Jan 26, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment states "Position 1-3 = Recommended (stable versions)" but the code on line 262 uses "idx <= 3", which includes indices 1, 2, and 3. This is correct for "3 versions after Latest", but the comment is slightly misleading as it doesn't clarify that index 0 is handled separately. Consider revising to "Positions 1-3 = Recommended (next 3 stable versions after Latest)" for clarity.

Suggested change
// Position 1-3 = Recommended (stable versions)
// Positions 1-3 = Recommended (next 3 stable versions after Latest)

Copilot uses AI. Check for mistakes.
// Rest based on version thresholds
var status SupportStatus
if idx == 0 {
status = Latest
} else if idx <= 3 {
status = Recommended
} else {
status = s.determineSupportStatus(versionNumber)
}

baseEntry := Artifact{
Version: version,
Hash: tag.Commit.SHA,
Date: now,
SupportStatus: s.determineSupportStatus(versionNumber),
SupportStatus: status,
}

// Windows artifact
Expand Down Expand Up @@ -392,7 +416,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult,
for version, artifact := range processedData.Windows {
artifacts = append(artifacts, ArtifactEntry{
Version: version,
FullVersion: s.generateFullVersion(version),
FullVersion: s.generateFullVersion(version, artifact.Hash),
Hash: artifact.Hash,
Platform: Windows,
Date: artifact.Date,
Expand All @@ -404,7 +428,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult,
for version, artifact := range processedData.Linux {
artifacts = append(artifacts, ArtifactEntry{
Version: version,
FullVersion: s.generateFullVersion(version),
FullVersion: s.generateFullVersion(version, artifact.Hash),
Hash: artifact.Hash,
Platform: Linux,
Date: artifact.Date,
Expand All @@ -420,6 +444,9 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult,
// Store total count after filtering but before pagination
totalFiltered := len(filtered)

// Calculate stats from filtered results (before pagination)
stats := s.calculateStats(filtered)

// Apply sorting
sorted := s.SortArtifacts(filtered, query.SortBy, query.SortOrder)

Expand All @@ -432,6 +459,7 @@ func (s *ArtifactsService) GetArtifacts(query ArtifactsQuery) (*ArtifactsResult,
return &ArtifactsResult{
Data: paginated,
Total: totalFiltered,
Stats: stats,
}, nil
}

Expand All @@ -449,24 +477,48 @@ func (s *ArtifactsService) extractVersionNumber(tagName string) int {
}

func (s *ArtifactsService) determineSupportStatus(version int) SupportStatus {
// Based on CFX EOL policy: https://aka.cfx.re/eol
// This is used for versions beyond the top 4 (Latest + 3 Recommended)
// which are dynamically assigned in ProcessGitHubTags
// Active = still supported but older
// Deprecated = support ending soon
// EOL = no longer supported
switch {
case version >= 24500:
return Recommended
case version >= 24000:
return Latest
case version >= 23000:
case version >= 23000: // Still actively supported
return Active
case version >= 20000:
case version >= 20000: // Support ending
return Deprecated
default:
return EOL
}
}

// generateFullVersion creates the full version string for hosting panels like Pterodactyl
// Format: v1.0.0.{build_number} (e.g., v1.0.0.12345)
func (s *ArtifactsService) generateFullVersion(version string) string {
return fmt.Sprintf("v1.0.0.%s", version)
// Format: {version}-{hash} (e.g., 24769-315823736cfbc085104ca0d32779311cd2f1a5a8)
func (s *ArtifactsService) generateFullVersion(version string, hash string) string {
return fmt.Sprintf("%s-%s", version, hash)
}

// calculateStats calculates artifact counts by support status
func (s *ArtifactsService) calculateStats(artifacts []ArtifactEntry) ArtifactStats {
stats := ArtifactStats{
Total: len(artifacts),
}
for _, artifact := range artifacts {
switch artifact.SupportStatus {
case Recommended:
stats.Recommended++
case Latest:
stats.Latest++
case Active:
stats.Active++
case Deprecated:
stats.Deprecated++
case EOL:
stats.EOL++
}
}
return stats
}

func (s *ArtifactsService) estimateSize(version string, platform string) int64 {
Expand Down
Loading