provisioning-core/nulib/domain/version/core.nu

309 lines
11 KiB
Text
Raw Permalink Normal View History

#!/usr/bin/env nu
# Agnostic Version Management Core
# No hardcoded tools or specific implementations
# use ../utils/error.nu *
# use ../utils/format.nu *
# Generic version record schema
export def version-schema [] {
{
id: "" # Unique identifier
type: "" # Component type (tool/provider/taskserv/cluster)
version: "" # Current version
fixed: false # Version pinning
source: {} # Source configuration
detector: {} # Detection configuration
updater: {} # Update configuration
metadata: {} # Any additional data
}
}
# Generic version operations interface
export def version-operations [] {
{
detect: { |config| "" } # Detect installed version
fetch: { |config| "" } # Fetch available versions
compare: { |v1, v2| 0 } # Compare versions
update: { |config, version| {} } # Update to version
}
}
# Version comparison (works with semantic and non-semantic versions)
export def compare-versions [
v1: string
v2: string
--strategy: string = "semantic" # semantic, string, numeric, custom
] {
if $v1 == $v2 { return 0 }
if ($v1 | is-empty) { return (-1) }
if ($v2 | is-empty) { return 1 }
match $strategy {
"semantic" => {
# Try semantic versioning - safely parse version parts
let parts1 = ($v1 | split row "." | each { |p|
let trimmed = ($p | str trim)
if ($trimmed | is-empty) {
0
} else {
# Attempt to convert to int, with error handling
if ($trimmed | parse -r "^[0-9]+$" | is-empty) {
0
} else {
$trimmed | into int
}
}
})
let parts2 = ($v2 | split row "." | each { |p|
let trimmed = ($p | str trim)
if ($trimmed | is-empty) {
0
} else {
# Attempt to convert to int, with error handling
if ($trimmed | parse -r "^[0-9]+$" | is-empty) {
0
} else {
$trimmed | into int
}
}
})
let max_len = ([$parts1 $parts2] | each { |it| $it | length } | math max)
for i in 0..<$max_len {
let p1 = if ($i < ($parts1 | length)) { $parts1 | get $i } else { 0 }
let p2 = if ($i < ($parts2 | length)) { $parts2 | get $i } else { 0 }
if $p1 < $p2 { return (-1) }
if $p1 > $p2 { return 1 }
}
0
}
"string" => {
# Simple string comparison
if $v1 < $v2 { (-1) } else if $v1 > $v2 { 1 } else { 0 }
}
"numeric" => {
# Numeric comparison (for build numbers)
let n1 = ($v1 | into float | default 0)
let n2 = ($v2 | into float | default 0)
if $n1 < $n2 { (-1) } else if $n1 > $n2 { 1 } else { 0 }
}
_ => 0
}
}
# Execute command and extract version
export def detect-version [
config: record # Detection configuration
] {
if ($config | is-empty) { return "" }
let method = ($config | get method? | default "command")
match $method {
"command" => {
let cmd = ($config | get command? | default "")
if ($cmd | is-empty) { return "" }
let result = (^sh -c $cmd err> /dev/null | complete)
if $result.exit_code == 0 {
let output = $result.stdout
# Apply extraction pattern if provided
if ($config | get pattern? | default null | is-not-empty) {
let parsed = ($output | parse -r $config.pattern)
if ($parsed | length) > 0 {
let row = ($parsed | get 0)
let capture_name = ($config | get capture? | default "capture0")
if ($capture_name in ($row | columns)) { $row | get $capture_name } else { "" }
} else {
""
}
} else {
$output | str trim
}
} else {
""
}
}
"file" => {
let path = ($config | get path? | default "")
if not ($path | path exists) { return "" }
let content = (open $path)
if ($config | get field? | default null | is-not-empty) {
$content | get $config.field? | default ""
} else {
$content | str trim
}
}
"api" => {
let url = ($config | get url? | default "")
if ($url | is-empty) { return "" }
let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
if ($config | get field? | default null | is-not-empty) {
$response | get $config.field? | default ""
} else {
$response | to text | str trim
}
} else {
""
}
}
"script" => {
# Execute custom script
let script = ($config | get script? | default "")
if ($script | is-empty) { return "" }
(nu -c $script | str trim | default "")
}
_ => ""
}
}
# Fetch available versions from source
export def fetch-versions [
config: record # Source configuration
--limit: int = 10
] {
if ($config | is-empty) { return [] }
let type = ($config | get type? | default "")
match $type {
"github" => {
let repo = ($config | get repo? | default "")
if ($repo | is-empty) { return [] }
# Try releases first, then tags
let endpoints = [
$"https://api.github.com/repos/($repo)/releases"
$"https://api.github.com/repos/($repo)/tags"
]
for endpoint in $endpoints {
let result = (http get $endpoint --headers [User-Agent "provisionin-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json | default [])
if ($response | length) > 0 {
return ($response
| first $limit
| each { |item|
let version = ($item | get tag_name? | default "")
$version | str replace -r '^v' ''
})
}
}
}
[]
}
"docker" => {
let image = ($config | get image? | default "")
if ($image | is-empty) { return [] }
# Parse namespace/repo
let parts = ($image | split row "/")
let namespace = if ($parts | length) > 1 { $parts | get 0 } else { "library" }
let repo = ($parts | last)
let url = $"https://hub.docker.com/v2/namespaces/($namespace)/repositories/($repo)/tags"
let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
if ($response | get results? | default null | is-not-empty) {
$response
| get results? | default null
| first $limit
| each { |tag| $tag.name }
| where { |v| $v !~ "latest|dev|nightly|edge|alpha|beta|rc" }
} else {
[]
}
} else {
[]
}
}
"url" => {
let url = ($config | get url? | default "")
if ($url | is-empty) { return [] }
let result = (http get $url --headers [User-Agent "provisionin-version-checker"] | complete)
if $result.exit_code == 0 and ($result.stdout | length) > 0 {
let response = ($result.stdout | from json)
let field = ($config | get field? | default "")
if ($field | is-not-empty) {
if ($field in ($response | columns)) { $response | get $field } else { [] }
} else {
[$response | to text | str trim]
}
} else {
[]
}
}
"script" => {
let script = ($config | get script? | default "")
if ($script | is-empty) { return [] }
(nu -c $script | lines | default [])
}
_ => []
}
}
# Generic version check
export def check-version [
component: record
--fetch-latest = false
--respect-fixed = true
] {
# Detect installed version
let installed = if (($component | get detector? | default null) != null) {
(detect-version $component.detector)
} else { "" }
# Get configured version
let configured = ($component | get version? | default "")
# Check if fixed
let is_fixed = ($component | get fixed? | default false)
# Fetch latest if requested
let latest = if $fetch_latest and (not $is_fixed or not $respect_fixed) {
if (($component | get source? | default null) != null) {
let versions = (fetch-versions $component.source --limit=1)
if ($versions | length) > 0 { $versions | get 0 } else { $configured }
} else { $configured }
} else { $configured }
# Compare versions
let comparison_strategy = ($component | get comparison? | default "semantic")
let status = if $is_fixed and $respect_fixed {
"fixed"
} else if ($installed | is-empty) {
"not_installed"
} else if ($installed | is-not-empty) and ($latest != $installed) and ((compare-versions $installed $latest --strategy=$comparison_strategy) < 0) {
"update_available"
} else if (compare-versions $installed $configured --strategy=$comparison_strategy) < 0 {
"behind_config"
} else if (compare-versions $installed $configured --strategy=$comparison_strategy) > 0 {
"ahead_config"
} else {
"up_to_date"
}
{
id: $component.id
type: $component.type
installed: $installed
configured: $configured
latest: $latest
fixed: $is_fixed
status: $status
}
}