#!/usr/bin/env nu # Artifact upload tool - uploads distribution artifacts to GitHub/registry # # Uploads to: # - GitHub releases # - Container registries (Docker Hub, GHCR, AWS ECR) # - Package repositories (npm, cargo, homebrew) # - Distribution servers # - CDN endpoints use std log def main [ --artifacts-dir: string = "packages" # Directory containing artifacts to upload --release-tag: string = "" # Release tag (auto-detected if empty) --targets: string = "github" # Upload targets: github,docker,npm,cargo,homebrew,all --registry-url: string = "" # Custom registry URL --credentials-file: string = "" # Credentials configuration file --parallel-uploads: bool = true # Upload to multiple targets in parallel --retry-count: int = 3 # Number of retry attempts for failed uploads --verify-uploads: bool = true # Verify uploads after completion --dry-run: bool = false # Show what would be uploaded without doing it --verbose: bool = false # Enable verbose logging ] -> record { let artifacts_root = ($artifacts_dir | path expand) let upload_targets = if $targets == "all" { ["github", "docker", "npm", "cargo", "homebrew"] } else { ($targets | split row "," | each { str trim }) } # Detect release tag if not provided let detected_tag = if $release_tag == "" { detect_current_release_tag } else { $release_tag } let upload_config = { artifacts_dir: $artifacts_root release_tag: $detected_tag targets: $upload_targets registry_url: $registry_url credentials_file: ($credentials_file | if $in == "" { "" } else { $in | path expand }) parallel_uploads: $parallel_uploads retry_count: $retry_count verify_uploads: $verify_uploads dry_run: $dry_run verbose: $verbose } log info $"Starting artifact uploads with config: ($upload_config)" # Validate artifacts directory if not ($artifacts_root | path exists) { log error $"Artifacts directory does not exist: ($artifacts_root)" exit 1 } # Find available artifacts let available_artifacts = find_available_artifacts $upload_config if ($available_artifacts | length) == 0 { log warning "No artifacts found to upload" return { status: "skipped" reason: "no artifacts found" uploads: [] } } log info $"Found ($available_artifacts | length) artifacts to upload" # Load credentials if provided let credentials = if $upload_config.credentials_file != "" { load_credentials $upload_config.credentials_file } else { {} } # Upload to each target let upload_results = if $upload_config.parallel_uploads { upload_parallel $upload_config $available_artifacts $credentials } else { upload_sequential $upload_config $available_artifacts $credentials } # Verify uploads if requested let verification_results = if $upload_config.verify_uploads and not $upload_config.dry_run { verify_uploads $upload_results $upload_config } else { { status: "skipped", verified: [], failed: [] } } let summary = { total_targets: ($upload_config.targets | length) successful_targets: ($upload_results | where status == "success" | length) failed_targets: ($upload_results | where status == "failed" | length) total_artifacts: ($available_artifacts | length) upload_results: $upload_results verification_results: $verification_results upload_config: $upload_config } if $summary.failed_targets > 0 { log error $"Artifact upload completed with ($summary.failed_targets) target failures" exit 1 } else { if $upload_config.dry_run { log info $"Dry run completed - would upload to ($summary.total_targets) targets" } else { log info $"Artifact upload completed successfully to ($summary.successful_targets) targets" } } return $summary } # Detect current release tag def detect_current_release_tag [] -> string { try { let latest_tag = (git describe --tags --exact-match HEAD 2>/dev/null | str trim) if $latest_tag != "" { return $latest_tag } # Fallback to latest tag git describe --tags --abbrev=0 2>/dev/null | str trim } catch { log warning "No release tag found, using 'latest'" return "latest" } } # Find available artifacts to upload def find_available_artifacts [upload_config: record] -> list { # Define artifact patterns by type let artifact_patterns = { archives: ["*.tar.gz", "*.zip"] binaries: ["*-linux-*", "*-macos-*", "*-windows-*"] containers: ["*-container-*.tar"] packages: ["*.deb", "*.rpm", "*.msi", "*.dmg"] metadata: ["checksums.txt", "manifest.json", "*.sig"] } let mut all_artifacts = [] # Find artifacts by pattern for category in ($artifact_patterns | columns) { let patterns = ($artifact_patterns | get $category) for pattern in $patterns { let found_files = (find $upload_config.artifacts_dir -name $pattern -type f) for file in $found_files { $all_artifacts = ($all_artifacts | append { path: $file name: ($file | path basename) category: $category size: (ls $file | get 0.size) modified: (ls $file | get 0.modified) }) } } } return ($all_artifacts | uniq-by path) } # Load credentials from configuration file def load_credentials [credentials_file: string] -> record { if not ($credentials_file | path exists) { log warning $"Credentials file not found: ($credentials_file)" return {} } try { open $credentials_file } catch {|err| log warning $"Failed to load credentials: ($err.msg)" return {} } } # Upload artifacts in parallel def upload_parallel [ upload_config: record artifacts: list credentials: record ] -> list { # For simplicity, using sequential for now # In a real implementation, you might use background processes upload_sequential $upload_config $artifacts $credentials } # Upload artifacts sequentially def upload_sequential [ upload_config: record artifacts: list credentials: record ] -> list { $upload_config.targets | each {|target| upload_to_target $target $artifacts $upload_config $credentials } } # Upload artifacts to a specific target def upload_to_target [ target: string artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to target: ($target)" let start_time = (date now) match $target { "github" => { upload_to_github $artifacts $upload_config $credentials } "docker" => { upload_to_docker $artifacts $upload_config $credentials } "npm" => { upload_to_npm $artifacts $upload_config $credentials } "cargo" => { upload_to_cargo $artifacts $upload_config $credentials } "homebrew" => { upload_to_homebrew $artifacts $upload_config $credentials } _ => { log warning $"Unknown upload target: ($target)" { target: $target status: "failed" reason: "unknown target" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } } } # Upload to GitHub releases def upload_to_github [ artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to GitHub releases..." let start_time = (date now) if $upload_config.dry_run { return { target: "github" status: "success" artifacts_uploaded: ($artifacts | length) dry_run: true duration: ((date now) - $start_time) } } # Check GitHub CLI availability let gh_check = try { gh --version | complete } catch { { exit_code: 1 } } if $gh_check.exit_code != 0 { return { target: "github" status: "failed" reason: "GitHub CLI (gh) not available" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } let mut upload_errors = [] let mut uploaded_count = 0 # Filter artifacts suitable for GitHub releases let github_artifacts = ($artifacts | where category in ["archives", "packages", "metadata"]) for artifact in $github_artifacts { try { if $upload_config.verbose { log info $"Uploading to GitHub: ($artifact.name)" } let upload_result = (gh release upload $upload_config.release_tag $artifact.path | complete) if $upload_result.exit_code == 0 { $uploaded_count = $uploaded_count + 1 } else { $upload_errors = ($upload_errors | append { artifact: $artifact.name error: $upload_result.stderr }) } } catch {|err| $upload_errors = ($upload_errors | append { artifact: $artifact.name error: $err.msg }) } } let status = if ($upload_errors | length) > 0 { "partial" } else { "success" } { target: "github" status: $status artifacts_uploaded: $uploaded_count total_artifacts: ($github_artifacts | length) errors: $upload_errors duration: ((date now) - $start_time) } } # Upload to Docker registry def upload_to_docker [ artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to Docker registry..." let start_time = (date now) # Check Docker availability let docker_check = try { docker --version | complete } catch { { exit_code: 1 } } if $docker_check.exit_code != 0 { return { target: "docker" status: "failed" reason: "Docker not available" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } if $upload_config.dry_run { let container_artifacts = ($artifacts | where category == "containers") return { target: "docker" status: "success" artifacts_uploaded: ($container_artifacts | length) dry_run: true duration: ((date now) - $start_time) } } let mut upload_errors = [] let mut uploaded_count = 0 # Find container artifacts let container_artifacts = ($artifacts | where category == "containers") for artifact in $container_artifacts { try { if $upload_config.verbose { log info $"Loading Docker image: ($artifact.name)" } # Load container image docker load -i $artifact.path # Tag and push (would need proper registry configuration) log warning "Docker registry push not fully implemented - container loaded locally" $uploaded_count = $uploaded_count + 1 } catch {|err| $upload_errors = ($upload_errors | append { artifact: $artifact.name error: $err.msg }) } } let status = if ($upload_errors | length) > 0 { "partial" } else { "success" } { target: "docker" status: $status artifacts_uploaded: $uploaded_count total_artifacts: ($container_artifacts | length) errors: $upload_errors duration: ((date now) - $start_time) } } # Upload to npm registry def upload_to_npm [ artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to npm registry..." let start_time = (date now) # Check for npm packages let npm_artifacts = ($artifacts | where name =~ "\.tgz$") if ($npm_artifacts | length) == 0 { return { target: "npm" status: "skipped" reason: "no npm packages found" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } if $upload_config.dry_run { return { target: "npm" status: "success" artifacts_uploaded: ($npm_artifacts | length) dry_run: true duration: ((date now) - $start_time) } } log warning "npm registry upload not implemented - would publish packages" { target: "npm" status: "skipped" reason: "not implemented" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } # Upload to Cargo registry def upload_to_cargo [ artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to Cargo registry..." let start_time = (date now) # Cargo publishes from source, not artifacts log warning "Cargo registry upload not implemented - would publish from source" { target: "cargo" status: "skipped" reason: "not implemented" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } # Upload to Homebrew def upload_to_homebrew [ artifacts: list upload_config: record credentials: record ] -> record { log info $"Uploading to Homebrew..." let start_time = (date now) if $upload_config.dry_run { return { target: "homebrew" status: "success" artifacts_uploaded: 1 dry_run: true duration: ((date now) - $start_time) } } log warning "Homebrew formula update not implemented - would update formula" { target: "homebrew" status: "skipped" reason: "not implemented" artifacts_uploaded: 0 duration: ((date now) - $start_time) } } # Verify uploads def verify_uploads [ upload_results: list upload_config: record ] -> record { log info "Verifying uploads..." let mut verified = [] let mut failed_verifications = [] for result in $upload_results { if $result.status in ["success", "partial"] { let verification = verify_target_upload $result.target $upload_config if $verification.status == "success" { $verified = ($verified | append $verification) } else { $failed_verifications = ($failed_verifications | append $verification) } } } { status: (if ($failed_verifications | length) > 0 { "partial" } else { "success" }) verified: $verified failed: $failed_verifications total_verified: ($verified | length) } } # Verify upload to specific target def verify_target_upload [target: string, upload_config: record] -> record { match $target { "github" => { verify_github_upload $upload_config } "docker" => { verify_docker_upload $upload_config } _ => { { target: $target status: "skipped" reason: "verification not implemented" } } } } # Verify GitHub upload def verify_github_upload [upload_config: record] -> record { try { let release_info = (gh release view $upload_config.release_tag --json assets | from json) let asset_count = ($release_info.assets | length) { target: "github" status: "success" verified_assets: $asset_count } } catch {|err| { target: "github" status: "failed" reason: $err.msg } } } # Verify Docker upload def verify_docker_upload [upload_config: record] -> record { # Docker verification would check registry { target: "docker" status: "skipped" reason: "verification not implemented" } } # Show upload targets and their status def "main info" [] { let github_available = (try { gh --version | complete } catch { { exit_code: 1 } }).exit_code == 0 let docker_available = (try { docker --version | complete } catch { { exit_code: 1 } }).exit_code == 0 let npm_available = (try { npm --version | complete } catch { { exit_code: 1 } }).exit_code == 0 let cargo_available = (try { cargo --version | complete } catch { { exit_code: 1 } }).exit_code == 0 { available_targets: { github: $github_available docker: $docker_available npm: $npm_available cargo: $cargo_available homebrew: false # Would need brew command } current_release_tag: (detect_current_release_tag) supported_targets: ["github", "docker", "npm", "cargo", "homebrew"] } } # List artifacts in directory def "main list" [artifacts_dir: string = "packages"] { let artifacts_root = ($artifacts_dir | path expand) if not ($artifacts_root | path exists) { return { error: "artifacts directory not found", directory: $artifacts_root } } let config = { artifacts_dir: $artifacts_root } let artifacts = find_available_artifacts $config $artifacts | group-by category | items {|category, items| { category: $category count: ($items | length) total_size: ($items | get size | math sum) artifacts: ($items | get name) } } }