615 lines
19 KiB
Plaintext
615 lines
19 KiB
Plaintext
#!/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
|
|
} |