595 lines
18 KiB
Plaintext
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)
|
|
}
|
|
}
|
|
} |