provisioning/tools/release/upload-artifacts.nu
2025-10-07 11:12:02 +01:00

595 lines
18 KiB
Plaintext

#!/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)
}
}
}