#!/usr/bin/env nu # Release rollback tool - rollback problematic releases # # Rollbacks: # - Git tags and releases # - Package manager entries # - Container registry tags # - Distribution artifacts # - Notification corrections # - Version reversions use std log def main [ --release-version: string # Release version to rollback (required) --rollback-scope: string = "complete" # Rollback scope: git, github, packages, containers, notifications, complete --reason: string = "" # Reason for rollback (for documentation) --replacement-version: string = "" # Replacement version to recommend (optional) --notify-users: bool = true # Send rollback notifications to users --cleanup-artifacts: bool = true # Clean up distribution artifacts --dry-run: bool = false # Show what would be rolled back without doing it --force: bool = false # Force rollback even with warnings --verbose: bool = false # Enable verbose logging ] -> record { if $release_version == "" { log error "Release version is required for rollback" exit 1 } let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let rollback_scopes = if $rollback_scope == "complete" { ["git", "github", "packages", "containers", "notifications"] } else { ($rollback_scope | split row "," | each { str trim }) } let rollback_config = { release_version: $release_version scopes: $rollback_scopes reason: (if $reason == "" { "Release rollback requested" } else { $reason }) replacement_version: $replacement_version notify_users: $notify_users cleanup_artifacts: $cleanup_artifacts dry_run: $dry_run force: $force verbose: $verbose repo_root: $repo_root } log info $"Starting release rollback with config: ($rollback_config)" # Validate rollback request let validation_result = validate_rollback_request $rollback_config if $validation_result.status != "valid" and not $rollback_config.force { log error $"Rollback validation failed: ($validation_result.reason)" if ($validation_result.warnings | length) > 0 { log warning "Warnings:" for warning in $validation_result.warnings { log warning $" - ($warning)" } } exit 1 } # Confirm rollback if not forced if not $rollback_config.force and not $rollback_config.dry_run { let confirmation = (input $"Are you sure you want to rollback release v($rollback_config.release_version)? [y/N] ") if not ($confirmation | str downcase | str starts-with "y") { log info "Rollback cancelled by user" exit 0 } } # Execute rollback for each scope let rollback_results = $rollback_config.scopes | each {|scope| execute_rollback_scope $scope $rollback_config } # Send rollback notifications if requested let notification_result = if $rollback_config.notify_users and not $rollback_config.dry_run { send_rollback_notifications $rollback_config $rollback_results } else { { status: "skipped", reason: "notifications disabled or dry run" } } # Generate rollback report let rollback_report = generate_rollback_report $rollback_config $rollback_results $notification_result let summary = { rollback_version: $rollback_config.release_version total_scopes: ($rollback_config.scopes | length) successful_rollbacks: ($rollback_results | where status == "success" | length) failed_rollbacks: ($rollback_results | where status == "failed" | length) skipped_rollbacks: ($rollback_results | where status == "skipped" | length) notification_result: $notification_result rollback_config: $rollback_config results: $rollback_results report: $rollback_report } if $summary.failed_rollbacks > 0 { log error $"Release rollback completed with ($summary.failed_rollbacks) failures" exit 1 } else { if $rollback_config.dry_run { log info $"Dry run completed - would rollback ($summary.total_scopes) scopes" } else { log info $"Release rollback completed successfully - ($summary.successful_rollbacks) scopes rolled back" } } return $summary } # Validate rollback request def validate_rollback_request [rollback_config: record] -> record { let mut warnings = [] let mut errors = [] cd $rollback_config.repo_root # Check if release version exists let tag_name = $"v($rollback_config.release_version)" let tag_exists = try { git tag -l $tag_name | str trim } catch { "" } if $tag_exists == "" { $errors = ($errors | append $"Release tag ($tag_name) does not exist") } # Check if this is the latest release let latest_tag = try { git describe --tags --abbrev=0 2>/dev/null | str trim } catch { "" } if $latest_tag != $tag_name { $warnings = ($warnings | append $"Rolling back ($tag_name) which is not the latest release (latest: ($latest_tag))") } # Check repository state let status_result = (git status --porcelain | complete) if $status_result.exit_code == 0 and ($status_result.stdout | str trim) != "" { $warnings = ($warnings | append "Repository has uncommitted changes") } # Check GitHub CLI availability for GitHub scope if "github" in $rollback_config.scopes { let gh_check = try { gh --version | complete } catch { { exit_code: 1 } } if $gh_check.exit_code != 0 { $warnings = ($warnings | append "GitHub CLI not available - GitHub rollback will be skipped") } } let status = if ($errors | length) > 0 { "invalid" } else if ($warnings | length) > 0 { "warning" } else { "valid" } { status: $status errors: $errors warnings: $warnings reason: (if ($errors | length) > 0 { ($errors | str join "; ") } else { "" }) } } # Execute rollback for specific scope def execute_rollback_scope [ scope: string rollback_config: record ] -> record { log info $"Rolling back scope: ($scope)" let start_time = (date now) match $scope { "git" => { rollback_git_release $rollback_config } "github" => { rollback_github_release $rollback_config } "packages" => { rollback_package_releases $rollback_config } "containers" => { rollback_container_releases $rollback_config } "notifications" => { rollback_notifications $rollback_config } _ => { log warning $"Unknown rollback scope: ($scope)" { scope: $scope status: "failed" reason: "unknown scope" duration: ((date now) - $start_time) } } } } # Rollback Git release (tag and commits) def rollback_git_release [rollback_config: record] -> record { log info "Rolling back Git release..." let start_time = (date now) cd $rollback_config.repo_root if $rollback_config.dry_run { return { scope: "git" status: "success" tag_name: $"v($rollback_config.release_version)" dry_run: true duration: ((date now) - $start_time) } } let tag_name = $"v($rollback_config.release_version)" let mut actions_taken = [] try { # Delete local tag let delete_local = (git tag -d $tag_name | complete) if $delete_local.exit_code == 0 { $actions_taken = ($actions_taken | append "deleted local tag") } # Delete remote tag let delete_remote = (git push --delete origin $tag_name | complete) if $delete_remote.exit_code == 0 { $actions_taken = ($actions_taken | append "deleted remote tag") } else { log warning $"Failed to delete remote tag: ($delete_remote.stderr)" } # Note: We don't automatically reset commits as that could be destructive # Users should manually handle commit rollbacks if needed { scope: "git" status: "success" tag_name: $tag_name actions_taken: $actions_taken duration: ((date now) - $start_time) } } catch {|err| { scope: "git" status: "failed" reason: $err.msg tag_name: $tag_name duration: ((date now) - $start_time) } } } # Rollback GitHub release def rollback_github_release [rollback_config: record] -> record { log info "Rolling back GitHub release..." let start_time = (date now) # Check GitHub CLI availability let gh_check = try { gh --version | complete } catch { { exit_code: 1 } } if $gh_check.exit_code != 0 { return { scope: "github" status: "skipped" reason: "GitHub CLI not available" duration: ((date now) - $start_time) } } if $rollback_config.dry_run { return { scope: "github" status: "success" release_tag: $"v($rollback_config.release_version)" dry_run: true duration: ((date now) - $start_time) } } let tag_name = $"v($rollback_config.release_version)" try { cd $rollback_config.repo_root # Check if GitHub release exists let release_check = (gh release view $tag_name | complete) if $release_check.exit_code != 0 { return { scope: "github" status: "skipped" reason: $"GitHub release ($tag_name) does not exist" duration: ((date now) - $start_time) } } # Delete GitHub release let delete_result = (gh release delete $tag_name --yes | complete) if $delete_result.exit_code == 0 { log info $"Successfully deleted GitHub release: ($tag_name)" { scope: "github" status: "success" release_tag: $tag_name duration: ((date now) - $start_time) } } else { { scope: "github" status: "failed" reason: $delete_result.stderr release_tag: $tag_name duration: ((date now) - $start_time) } } } catch {|err| { scope: "github" status: "failed" reason: $err.msg release_tag: $tag_name duration: ((date now) - $start_time) } } } # Rollback package releases (Homebrew, APT, etc.) def rollback_package_releases [rollback_config: record] -> record { log info "Rolling back package releases..." let start_time = (date now) if $rollback_config.dry_run { return { scope: "packages" status: "success" packages_affected: ["homebrew", "apt", "yum"] dry_run: true duration: ((date now) - $start_time) } } # Package rollback would involve: # 1. Reverting Homebrew formula to previous version # 2. Removing packages from APT/YUM repositories # 3. Updating package indexes log warning "Package rollback not fully implemented - would revert package manager entries" { scope: "packages" status: "skipped" reason: "not fully implemented" packages_affected: [] duration: ((date now) - $start_time) } } # Rollback container releases def rollback_container_releases [rollback_config: record] -> record { log info "Rolling back container releases..." let start_time = (date now) if $rollback_config.dry_run { return { scope: "containers" status: "success" images_affected: [$"provisioning:($rollback_config.release_version)"] dry_run: true duration: ((date now) - $start_time) } } # Container rollback would involve: # 1. Removing specific version tags # 2. Updating 'latest' tag to previous version # 3. Cleaning up registry entries log warning "Container rollback not fully implemented - would remove container tags" { scope: "containers" status: "skipped" reason: "not fully implemented" images_affected: [] duration: ((date now) - $start_time) } } # Rollback notifications (send corrections) def rollback_notifications [rollback_config: record] -> record { log info "Rolling back notifications..." let start_time = (date now) if $rollback_config.dry_run { return { scope: "notifications" status: "success" correction_message: $"Release v($rollback_config.release_version) has been rolled back" dry_run: true duration: ((date now) - $start_time) } } # Notification rollback would involve: # 1. Sending correction messages to all notification channels # 2. Updating RSS feeds with rollback notice # 3. Posting rollback announcements log warning "Notification rollback not fully implemented - would send correction messages" { scope: "notifications" status: "skipped" reason: "not fully implemented" duration: ((date now) - $start_time) } } # Send rollback notifications to users def send_rollback_notifications [ rollback_config: record rollback_results: list ] -> record { log info "Sending rollback notifications..." let rollback_message = generate_rollback_message $rollback_config $rollback_results # This would use the notification system to send rollback alerts log warning "Rollback notifications not fully implemented - would send user alerts" { status: "skipped" reason: "not fully implemented" message: $rollback_message } } # Generate rollback message for notifications def generate_rollback_message [ rollback_config: record rollback_results: list ] -> string { let successful_scopes = ($rollback_results | where status == "success" | get scope) let replacement_text = if $rollback_config.replacement_version != "" { $" Please use version ($rollback_config.replacement_version) instead." } else { "" } $"🚨 Release Rollback Notice Release v($rollback_config.release_version) has been rolled back due to: ($rollback_config.reason) Affected areas: ($successful_scopes | str join ', ') ($replacement_text) We apologize for any inconvenience. Please ensure you're using a stable version. The Provisioning Team" } # Generate rollback report def generate_rollback_report [ rollback_config: record rollback_results: list notification_result: record ] -> record { let report = { timestamp: (date now) rollback_version: $rollback_config.release_version reason: $rollback_config.reason replacement_version: $rollback_config.replacement_version summary: { total_scopes: ($rollback_results | length) successful: ($rollback_results | where status == "success" | length) failed: ($rollback_results | where status == "failed" | length) skipped: ($rollback_results | where status == "skipped" | length) } scopes: $rollback_results notifications: $notification_result configuration: $rollback_config } # Save report to file let report_file = ($rollback_config.repo_root | path join $"rollback-report-($rollback_config.release_version).json") $report | to json | save $report_file log info $"Rollback report saved to: ($report_file)" return $report } # Show rollback status and options def "main status" [release_version: string = ""] { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) cd $repo_root if $release_version == "" { # Show general rollback status let latest_tag = try { git describe --tags --abbrev=0 2>/dev/null | str trim } catch { "none" } let recent_tags = try { git tag -l --sort=-version:refname | head -5 | lines } catch { [] } return { repository: $repo_root latest_release: $latest_tag recent_releases: $recent_tags rollback_scopes: ["git", "github", "packages", "containers", "notifications"] tools_available: { git: true github_cli: (try { gh --version | complete } catch { { exit_code: 1 } }).exit_code == 0 } } } else { # Show status for specific release let tag_name = $"v($release_version)" let tag_exists = try { git tag -l $tag_name | str trim } catch { "" } let github_release_exists = if (try { gh --version | complete } catch { { exit_code: 1 } }).exit_code == 0 { (try { gh release view $tag_name | complete } catch { { exit_code: 1 } }).exit_code == 0 } else { false } return { release_version: $release_version tag_name: $tag_name git_tag_exists: ($tag_exists != "") github_release_exists: $github_release_exists rollback_possible: ($tag_exists != "" or $github_release_exists) } } } # List recent releases that can be rolled back def "main list" [] { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) cd $repo_root let tags = try { git tag -l --sort=-version:refname | head -10 | lines } catch { [] } let gh_available = (try { gh --version | complete } catch { { exit_code: 1 } }).exit_code == 0 $tags | each {|tag| let release_exists = if $gh_available { (try { gh release view $tag | complete } catch { { exit_code: 1 } }).exit_code == 0 } else { false } let tag_date = try { git log -1 --format=%cd --date=short $tag | str trim } catch { "unknown" } { tag: $tag version: ($tag | str replace "^v" "") date: $tag_date github_release: $release_exists can_rollback: true } } } # Emergency rollback (interactive) def "main emergency" [release_version: string] { log warning $"🚨 EMERGENCY ROLLBACK for v($release_version)" print "This will immediately rollback the release with minimal checks." print "Use this only in critical situations (security issues, broken releases, etc.)" print "" let confirmation = (input $"Type 'EMERGENCY ROLLBACK' to confirm: ") if $confirmation != "EMERGENCY ROLLBACK" { log info "Emergency rollback cancelled" exit 0 } # Execute emergency rollback with force main $release_version --rollback-scope complete --force --notify-users --reason "Emergency rollback" --verbose }