#!/usr/bin/env nu # Extension Publishing Tool # Publishes extensions to OCI registry with validation and packaging use ../core/nulib/lib_provisioning/oci/client.nu * use ../core/nulib/lib_provisioning/utils/logger.nu * # Publish extension to OCI registry export def main [ extension_path: string # Path to extension directory --registry: string = "" # OCI registry (default from config) --namespace: string = "" # Registry namespace (default from config) --version: string # Extension version --auth-token: string = "" # Authentication token path --dry-run # Validate without publishing --force (-f) # Overwrite existing version ] { print $"📦 Publishing extension to OCI registry..." # Load config if registry/namespace not provided let config = if ($registry | is-empty) or ($namespace | is-empty) { get-oci-config } else { { registry: $registry namespace: $namespace auth_token_path: $auth_token } } let final_registry = if ($registry | is-empty) { $config.registry } else { $registry } let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace } let token_path = if ($auth_token | is-empty) { $config.auth_token_path } else { $auth_token } # 1. Validate extension structure print "1️⃣ Validating extension structure..." let validation = (validate-extension-structure $extension_path) if not $validation.valid { print $"❌ Extension validation failed:" for error in $validation.errors { print $" • ($error)" } exit 1 } print " ✅ Extension structure valid" # 2. Read extension manifest print "2️⃣ Reading extension manifest..." let manifest_file = ($extension_path | path join "extension.yaml") if not ($manifest_file | path exists) { print "❌ extension.yaml not found" exit 1 } let manifest = (open $manifest_file | from yaml) let ext_name = ($manifest.extension.name? | default (error make {msg: "Extension name not found in manifest"})) let ext_version = if ($version | is-not-empty) { $version } else { $manifest.extension.version? | default (error make {msg: "Extension version not specified"}) } let ext_type = ($manifest.extension.type? | default "unknown") print $" 📋 Name: ($ext_name)" print $" 🏷️ Version: ($ext_version)" print $" 📦 Type: ($ext_type)" # 3. Check if version already exists if not $force { print "3️⃣ Checking if version already exists..." let token = (load-oci-token $token_path) let exists = (oci-artifact-exists $final_registry $final_namespace $ext_name $ext_version) if $exists { print $"❌ Version ($ext_version) already exists in registry" print " Use --force to overwrite" exit 1 } print " ✅ Version available" } # 4. Create OCI artifact print "4️⃣ Creating OCI artifact..." let artifact_path = (package-extension-as-oci $extension_path $manifest $ext_version) print $" 📁 Artifact created at: ($artifact_path)" if $dry_run { print "🏁 Dry run complete (artifact not published)" print $" Artifact path: ($artifact_path)" exit 0 } # 5. Push to registry print "5️⃣ Pushing to OCI registry..." let token = (load-oci-token $token_path) let success = (oci-push-artifact $artifact_path $final_registry $final_namespace $ext_name $ext_version --auth-token $token ) # Clean up artifact rm -rf $artifact_path if $success { print "" print $"✅ Successfully published ($ext_name):($ext_version)" print $" Registry: ($final_registry)" print $" Namespace: ($final_namespace)" print $" Pull with: oci://($final_registry)/($final_namespace)/($ext_name):($ext_version)" } else { print "" print "❌ Failed to push artifact to OCI registry" exit 1 } } # Validate extension directory structure def validate-extension-structure [path: string]: nothing -> record { let mut errors = [] # Check if path exists if not ($path | path exists) { $errors = ($errors | append $"Path does not exist: ($path)") return {valid: false, errors: $errors} } # Check required file: extension.yaml let manifest_file = ($path | path join "extension.yaml") if not ($manifest_file | path exists) { $errors = ($errors | append "Missing required file: extension.yaml") } # Validate manifest structure if ($manifest_file | path exists) { let manifest = (open $manifest_file | from yaml) # Check required fields if ($manifest.extension?.name? | is-empty) { $errors = ($errors | append "extension.name is required in manifest") } if ($manifest.extension?.type? | is-empty) { $errors = ($errors | append "extension.type is required in manifest") } if ($manifest.extension?.version? | is-empty) { $errors = ($errors | append "extension.version is required in manifest") } } # Check for at least one content directory let has_kcl = (($path | path join "kcl") | path exists) let has_scripts = (($path | path join "scripts") | path exists) let has_templates = (($path | path join "templates") | path exists) if not ($has_kcl or $has_scripts or $has_templates) { $errors = ($errors | append "Extension must have at least one of: kcl/, scripts/, templates/") } { valid: ($errors | is-empty) errors: $errors } } # Package extension as OCI artifact def package-extension-as-oci [ extension_path: string manifest: record version: string ]: nothing -> string { let temp_dir = (mktemp -d) print $" 📦 Packaging to: ($temp_dir)" # Copy extension files let dirs_to_copy = [ {src: "kcl", desc: "KCL schemas"} {src: "scripts", desc: "Scripts"} {src: "templates", desc: "Templates"} {src: "docs", desc: "Documentation"} ] for dir_info in $dirs_to_copy { let src_path = ($extension_path | path join $dir_info.src) if ($src_path | path exists) { cp -r $src_path $temp_dir print $" ✅ Copied ($dir_info.desc)" } } # Copy manifest let manifest_src = ($extension_path | path join "extension.yaml") cp $manifest_src $temp_dir print " ✅ Copied manifest" # Create OCI config with metadata let oci_config = { created: (date now | format date "%Y-%m-%dT%H:%M:%SZ") architecture: "any" os: "any" config: { Labels: { "org.opencontainers.image.title": ($manifest.extension.name? | default "unknown") "org.opencontainers.image.version": $version "org.opencontainers.image.description": ($manifest.extension.description? | default "") "org.opencontainers.image.authors": ($manifest.extension.author? | default "") "provisioning.extension.type": ($manifest.extension.type? | default "unknown") "provisioning.extension.requires": ($manifest.extension.requires? | default [] | to json) } } annotations: { "provisioning.extension.type": ($manifest.extension.type? | default "unknown") "provisioning.extension.name": ($manifest.extension.name? | default "unknown") "provisioning.extension.version": $version } } # Save OCI config $oci_config | to json | save $"($temp_dir)/oci-config.json" print " ✅ Created OCI config" $temp_dir } # List published extensions export def "list" [ --registry: string = "" --namespace: string = "" ] { let config = if ($registry | is-empty) or ($namespace | is-empty) { get-oci-config } else { {registry: $registry, namespace: $namespace} } let final_registry = if ($registry | is-empty) { $config.registry } else { $registry } let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace } print $"📦 Extensions in ($final_registry)/($final_namespace):" let artifacts = (oci-list-artifacts $final_registry $final_namespace) if ($artifacts | is-empty) { print " (no extensions found)" return } for artifact in $artifacts { print $" • ($artifact)" } } # Delete published extension export def "delete" [ extension_name: string version: string --registry: string = "" --namespace: string = "" --auth-token: string = "" --force (-f) ] { let config = if ($registry | is-empty) or ($namespace | is-empty) { get-oci-config } else { {registry: $registry, namespace: $namespace, auth_token_path: $auth_token} } let final_registry = if ($registry | is-empty) { $config.registry } else { $registry } let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace } let token_path = if ($auth_token | is-empty) { $config.auth_token_path } else { $auth_token } if not $force { print $"⚠️ Delete ($extension_name):($version) from ($final_registry)/($final_namespace)?" let confirm = (input "Type 'yes' to confirm: ") if $confirm != "yes" { print "❌ Cancelled" return } } let token = (load-oci-token $token_path) let success = (oci-delete-artifact $final_registry $final_namespace $extension_name $version --auth-token $token ) if $success { print $"✅ Deleted ($extension_name):($version)" } else { print "❌ Failed to delete extension" exit 1 } } # Show extension info export def "info" [ extension_name: string version: string --registry: string = "" --namespace: string = "" ] { let config = if ($registry | is-empty) or ($namespace | is-empty) { get-oci-config } else { {registry: $registry, namespace: $namespace} } let final_registry = if ($registry | is-empty) { $config.registry } else { $registry } let final_namespace = if ($namespace | is-empty) { $config.namespace } else { $namespace } let metadata = (get-oci-extension-metadata $extension_name $version { registry: $final_registry namespace: $final_namespace }) if ($metadata | is-empty) { print $"❌ Extension ($extension_name):($version) not found" exit 1 } print $"📦 Extension: ($extension_name):($version)" print $" Registry: ($final_registry)" print $" Namespace: ($final_namespace)" print $" Type: ($metadata.annotations?.\"provisioning.extension.type\"? | default 'unknown')" print $" Digest: ($metadata.oci_digest)" print $" Created: ($metadata.created)" print $" Size: ($metadata.size) bytes" } # Helper to get OCI extension metadata def get-oci-extension-metadata [ extension_name: string version: string config: record ]: nothing -> record { use ../core/nulib/lib_provisioning/extensions/discovery.nu get-oci-extension-metadata get-oci-extension-metadata $extension_name $version $config }