#!/usr/bin/env nu # User notification tool - sends update notifications and announcements # # Notifications: # - Email notifications to subscribers # - Slack/Discord announcements # - Twitter/social media posts # - RSS feed updates # - Website banner updates # - GitHub discussions/issues use std log def main [ --channels: string = "slack" # Notification channels: slack,discord,twitter,email,rss,website,all --release-version: string = "" # Release version (auto-detected if empty) --message-template: string = "" # Custom message template file --notification-config: string = "" # Notification configuration file --recipient-list: string = "" # Recipient list file (for email) --dry-run: bool = false # Show what would be sent without sending --urgent: bool = false # Mark notifications as urgent/high priority --schedule: string = "" # Schedule notifications (e.g., "+1h", "2024-01-15T10:00:00") --verbose: bool = false # Enable verbose logging ] -> record { let repo_root = ($env.PWD | path dirname | path dirname | path dirname) # Determine release version if not provided let target_version = if $release_version == "" { detect_release_version $repo_root } else { $release_version } let notification_channels = if $channels == "all" { ["slack", "discord", "twitter", "email", "rss", "website"] } else { ($channels | split row "," | each { str trim }) } let notification_config = { channels: $notification_channels release_version: $target_version message_template_file: (if $message_template == "" { "" } else { $message_template | path expand }) notification_config_file: (if $notification_config == "" { "" } else { $notification_config | path expand }) recipient_list_file: (if $recipient_list == "" { "" } else { $recipient_list | path expand }) dry_run: $dry_run urgent: $urgent schedule: $schedule verbose: $verbose repo_root: $repo_root } log info $"Starting user notifications with config: ($notification_config)" # Load notification configuration let config_data = if $notification_config.notification_config_file != "" { load_notification_config $notification_config.notification_config_file } else { get_default_notification_config } # Generate release information let release_info = generate_release_info $notification_config $repo_root # Load or generate message templates let message_templates = if $notification_config.message_template_file != "" { load_message_templates $notification_config.message_template_file } else { generate_default_templates $release_info } # Check if notifications should be scheduled if $notification_config.schedule != "" { return schedule_notifications $notification_config $message_templates $config_data $release_info } # Send notifications to each channel let notification_results = $notification_config.channels | each {|channel| send_notification $channel $notification_config $message_templates $config_data $release_info } let summary = { total_channels: ($notification_config.channels | length) successful_notifications: ($notification_results | where status == "success" | length) failed_notifications: ($notification_results | where status == "failed" | length) skipped_notifications: ($notification_results | where status == "skipped" | length) release_version: $notification_config.release_version notification_config: $notification_config results: $notification_results } if $summary.failed_notifications > 0 { log error $"Notifications completed with ($summary.failed_notifications) failures" exit 1 } else { if $notification_config.dry_run { log info $"Dry run completed - would send notifications to ($summary.total_channels) channels" } else { log info $"Notifications sent successfully to ($summary.successful_notifications) channels" } } return $summary } # Detect release version from git def detect_release_version [repo_root: string] -> string { cd $repo_root try { # Try to get exact tag for current commit let exact_tag = (git describe --tags --exact-match HEAD 2>/dev/null | str trim) if $exact_tag != "" { return ($exact_tag | str replace "^v" "") } # Fallback to latest tag let latest_tag = (git describe --tags --abbrev=0 2>/dev/null | str trim) if $latest_tag != "" { return ($latest_tag | str replace "^v" "") } return "unknown" } catch { return "unknown" } } # Load notification configuration from file def load_notification_config [config_file: string] -> record { if not ($config_file | path exists) { log warning $"Notification config file not found: ($config_file)" return (get_default_notification_config) } try { open $config_file } catch {|err| log warning $"Failed to load notification config: ($err.msg)" return (get_default_notification_config) } } # Get default notification configuration def get_default_notification_config [] -> record { { slack: { webhook_url: "" channel: "#general" username: "Provisioning Bot" icon_emoji: ":rocket:" } discord: { webhook_url: "" username: "Provisioning Bot" avatar_url: "" } twitter: { api_key: "" api_secret: "" access_token: "" access_token_secret: "" } email: { smtp_server: "smtp.gmail.com" smtp_port: 587 username: "" password: "" from_address: "noreply@example.com" from_name: "Provisioning Team" } rss: { feed_file: "releases.xml" feed_title: "Provisioning Releases" feed_description: "Latest releases of the Provisioning system" feed_url: "https://example.com/releases.xml" } website: { banner_file: "release-banner.html" api_endpoint: "" api_key: "" } } } # Generate release information def generate_release_info [notification_config: record, repo_root: string] -> record { cd $repo_root let version = $notification_config.release_version let tag_name = $"v($version)" # Get release date let release_date = try { git log -1 --format=%cd --date=short $tag_name 2>/dev/null | str trim } catch { date now | format date "%Y-%m-%d" } # Get changelog let changelog = try { get_changelog_summary $repo_root $tag_name } catch { "Bug fixes and improvements" } # Get download URLs let download_base_url = $"https://github.com/your-org/provisioning/releases/download/($tag_name)" let download_urls = { linux: $"($download_base_url)/provisioning-($version)-linux-complete.tar.gz" macos: $"($download_base_url)/provisioning-($version)-macos-complete.tar.gz" windows: $"($download_base_url)/provisioning-($version)-windows-complete.zip" } # Get release notes URL let release_url = $"https://github.com/your-org/provisioning/releases/tag/($tag_name)" { version: $version tag_name: $tag_name release_date: $release_date changelog: $changelog download_urls: $download_urls release_url: $release_url is_major: (is_major_version $version) is_security: (is_security_release $changelog) } } # Get changelog summary for a specific tag def get_changelog_summary [repo_root: string, tag_name: string] -> string { # Get previous tag let previous_tag = try { git describe --tags --abbrev=0 $"($tag_name)^" 2>/dev/null | str trim } catch { "" } # Get commits between tags let commit_range = if $previous_tag != "" { $"($previous_tag)..($tag_name)" } else { $tag_name } let commits = try { git log $commit_range --pretty=format:"%s" --no-merges | lines | where $it != "" } catch { [] } # Summarize changes let features = ($commits | where ($it =~ "^feat")) let fixes = ($commits | where ($it =~ "^fix")) let mut summary_parts = [] if ($features | length) > 0 { $summary_parts = ($summary_parts | append $"($features | length) new features") } if ($fixes | length) > 0 { $summary_parts = ($summary_parts | append $"($fixes | length) bug fixes") } if ($summary_parts | length) > 0 { return ($summary_parts | str join ", ") } else { return "Bug fixes and improvements" } } # Check if version is a major release def is_major_version [version: string] -> bool { let parts = ($version | split row ".") if ($parts | length) >= 3 { let minor = ($parts | get 1) let patch = ($parts | get 2) return ($minor == "0" and $patch == "0") } return false } # Check if release contains security fixes def is_security_release [changelog: string] -> bool { ($changelog | str downcase | str contains "security") or ($changelog | str downcase | str contains "vulnerability") or ($changelog | str downcase | str contains "cve") } # Load message templates from file def load_message_templates [template_file: string] -> record { if not ($template_file | path exists) { log warning $"Template file not found: ($template_file)" return {} } try { open $template_file } catch {|err| log warning $"Failed to load templates: ($err.msg)" return {} } } # Generate default message templates def generate_default_templates [release_info: record] -> record { let urgency_text = if $release_info.is_security { "🚨 Security Update " } else if $release_info.is_major { "🎉 Major Release " } else { "" } let emoji = if $release_info.is_security { "🔒" } else if $release_info.is_major { "🎉" } else { "🚀" } { slack: { text: $"($urgency_text)($emoji) Provisioning v($release_info.version) Released!" attachments: [ { color: (if $release_info.is_security { "danger" } else { "good" }) fields: [ { title: "Version", value: $release_info.version, short: true } { title: "Release Date", value: $release_info.release_date, short: true } { title: "Changes", value: $release_info.changelog, short: false } ] actions: [ { type: "button", text: "Download", url: $release_info.download_urls.linux } { type: "button", text: "Release Notes", url: $release_info.release_url } ] } ] } discord: { content: $"($urgency_text)($emoji) **Provisioning v($release_info.version)** has been released!" embeds: [ { title: $"Release v($release_info.version)" description: $release_info.changelog color: (if $release_info.is_security { 15158332 } else { 3066993 }) # Red or Green fields: [ { name: "Release Date", value: $release_info.release_date, inline: true } { name: "Downloads", value: $"[Linux]((\"($release_info.download_urls.linux)\")) | [macOS]((\"($release_info.download_urls.macos)\")) | [Windows]((\"($release_info.download_urls.windows)\"))", inline: false } ] url: $release_info.release_url timestamp: (date now | format date "%Y-%m-%dT%H:%M:%S.000Z") } ] } twitter: { status: $"($urgency_text)($emoji) Provisioning v($release_info.version) is now available! ($release_info.changelog) Download: ($release_info.release_url) #CloudNative #Infrastructure #DevOps" } email: { subject: $"($urgency_text)Provisioning v($release_info.version) Released" body: $"Dear Provisioning Users, We're excited to announce the release of Provisioning v($release_info.version)! **What's New:** ($release_info.changelog) **Downloads:** - Linux: ($release_info.download_urls.linux) - macOS: ($release_info.download_urls.macos) - Windows: ($release_info.download_urls.windows) **Release Notes:** For detailed information about this release, please visit: ($release_info.release_url) Thank you for using Provisioning! Best regards, The Provisioning Team" } } } # Schedule notifications for later def schedule_notifications [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info $"Scheduling notifications for: ($notification_config.schedule)" # In a real implementation, this would use a job scheduler like cron # For now, we'll just return the scheduled job information { status: "scheduled" schedule_time: $notification_config.schedule channels: $notification_config.channels release_version: $notification_config.release_version message: "Notifications scheduled successfully" } } # Send notification to specific channel def send_notification [ channel: string notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info $"Sending notification to: ($channel)" let start_time = (date now) match $channel { "slack" => { send_slack_notification $notification_config $message_templates $config_data $release_info } "discord" => { send_discord_notification $notification_config $message_templates $config_data $release_info } "twitter" => { send_twitter_notification $notification_config $message_templates $config_data $release_info } "email" => { send_email_notification $notification_config $message_templates $config_data $release_info } "rss" => { update_rss_feed $notification_config $message_templates $config_data $release_info } "website" => { update_website_banner $notification_config $message_templates $config_data $release_info } _ => { log warning $"Unknown notification channel: ($channel)" { channel: $channel status: "failed" reason: "unknown channel" duration: ((date now) - $start_time) } } } } # Send Slack notification def send_slack_notification [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Sending Slack notification..." let start_time = (date now) let slack_config = ($config_data.slack) if $slack_config.webhook_url == "" { return { channel: "slack" status: "failed" reason: "webhook URL not configured" duration: ((date now) - $start_time) } } if $notification_config.dry_run { return { channel: "slack" status: "success" message_preview: $message_templates.slack.text dry_run: true duration: ((date now) - $start_time) } } try { let payload = { channel: $slack_config.channel username: $slack_config.username icon_emoji: $slack_config.icon_emoji text: $message_templates.slack.text attachments: $message_templates.slack.attachments } let curl_result = (curl -X POST -H "Content-type: application/json" --data ($payload | to json) $slack_config.webhook_url | complete) if $curl_result.exit_code == 0 { { channel: "slack" status: "success" webhook_url: $slack_config.webhook_url duration: ((date now) - $start_time) } } else { { channel: "slack" status: "failed" reason: $curl_result.stderr duration: ((date now) - $start_time) } } } catch {|err| { channel: "slack" status: "failed" reason: $err.msg duration: ((date now) - $start_time) } } } # Send Discord notification def send_discord_notification [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Sending Discord notification..." let start_time = (date now) let discord_config = ($config_data.discord) if $discord_config.webhook_url == "" { return { channel: "discord" status: "failed" reason: "webhook URL not configured" duration: ((date now) - $start_time) } } if $notification_config.dry_run { return { channel: "discord" status: "success" message_preview: $message_templates.discord.content dry_run: true duration: ((date now) - $start_time) } } try { let payload = { username: $discord_config.username avatar_url: $discord_config.avatar_url content: $message_templates.discord.content embeds: $message_templates.discord.embeds } let curl_result = (curl -X POST -H "Content-type: application/json" --data ($payload | to json) $discord_config.webhook_url | complete) if $curl_result.exit_code == 0 { { channel: "discord" status: "success" webhook_url: $discord_config.webhook_url duration: ((date now) - $start_time) } } else { { channel: "discord" status: "failed" reason: $curl_result.stderr duration: ((date now) - $start_time) } } } catch {|err| { channel: "discord" status: "failed" reason: $err.msg duration: ((date now) - $start_time) } } } # Send Twitter notification def send_twitter_notification [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Sending Twitter notification..." let start_time = (date now) if $notification_config.dry_run { return { channel: "twitter" status: "success" tweet_preview: $message_templates.twitter.status dry_run: true duration: ((date now) - $start_time) } } # Twitter API integration would be implemented here log warning "Twitter notification not fully implemented - requires API setup" { channel: "twitter" status: "skipped" reason: "not fully implemented" duration: ((date now) - $start_time) } } # Send email notification def send_email_notification [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Sending email notification..." let start_time = (date now) if $notification_config.dry_run { return { channel: "email" status: "success" subject_preview: $message_templates.email.subject dry_run: true duration: ((date now) - $start_time) } } # Email sending would be implemented here using SMTP log warning "Email notification not fully implemented - requires SMTP configuration" { channel: "email" status: "skipped" reason: "not fully implemented" duration: ((date now) - $start_time) } } # Update RSS feed def update_rss_feed [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Updating RSS feed..." let start_time = (date now) let rss_config = ($config_data.rss) if $notification_config.dry_run { return { channel: "rss" status: "success" feed_file: $rss_config.feed_file dry_run: true duration: ((date now) - $start_time) } } try { let rss_item = generate_rss_item $release_info # RSS feed update logic would be implemented here log warning "RSS feed update not fully implemented" { channel: "rss" status: "skipped" reason: "not fully implemented" feed_file: $rss_config.feed_file duration: ((date now) - $start_time) } } catch {|err| { channel: "rss" status: "failed" reason: $err.msg duration: ((date now) - $start_time) } } } # Update website banner def update_website_banner [ notification_config: record message_templates: record config_data: record release_info: record ] -> record { log info "Updating website banner..." let start_time = (date now) if $notification_config.dry_run { return { channel: "website" status: "success" dry_run: true duration: ((date now) - $start_time) } } # Website banner update logic would be implemented here log warning "Website banner update not fully implemented" { channel: "website" status: "skipped" reason: "not fully implemented" duration: ((date now) - $start_time) } } # Generate RSS item for release def generate_rss_item [release_info: record] -> string { $" Provisioning v($release_info.version) Released ($release_info.changelog) ($release_info.release_url) ($release_info.release_url) (date now | format date "%a, %d %b %Y %H:%M:%S %z") " } # Show notification status def "main status" [] { let curl_available = (try { curl --version | complete } catch { { exit_code: 1 } }).exit_code == 0 let repo_root = ($env.PWD | path dirname | path dirname | path dirname) let current_version = (detect_release_version $repo_root) { current_version: $current_version available_tools: { curl: $curl_available } supported_channels: ["slack", "discord", "twitter", "email", "rss", "website"] implemented_channels: ["slack", "discord"] # Only these are fully implemented } } # Initialize notification configuration def "main init-config" [output_file: string = "notification-config.toml"] { let config_template = $"# Notification Configuration [slack] webhook_url = \"\" # Your Slack webhook URL channel = \"#general\" username = \"Provisioning Bot\" icon_emoji = \":rocket:\" [discord] webhook_url = \"\" # Your Discord webhook URL username = \"Provisioning Bot\" avatar_url = \"\" [twitter] api_key = \"\" api_secret = \"\" access_token = \"\" access_token_secret = \"\" [email] smtp_server = \"smtp.gmail.com\" smtp_port = 587 username = \"\" password = \"\" from_address = \"noreply@example.com\" from_name = \"Provisioning Team\" [rss] feed_file = \"releases.xml\" feed_title = \"Provisioning Releases\" feed_description = \"Latest releases of the Provisioning system\" feed_url = \"https://example.com/releases.xml\" [website] banner_file = \"release-banner.html\" api_endpoint = \"\" api_key = \"\" " $config_template | save $output_file log info $"Generated notification configuration template: ($output_file)" { config_file: $output_file channels_configured: 6 template_generated: true } } # Test notification to specific channel def "main test" [ channel: string = "slack" # Channel to test --config: string = "" # Configuration file ] { log info $"Testing notification to: ($channel)" let test_release_info = { version: "1.0.0-test" tag_name: "v1.0.0-test" release_date: (date now | format date "%Y-%m-%d") changelog: "Test notification" download_urls: { linux: "https://example.com/linux.tar.gz" macos: "https://example.com/macos.tar.gz" windows: "https://example.com/windows.zip" } release_url: "https://github.com/example/test" is_major: false is_security: false } let test_config = { channels: [$channel] release_version: "1.0.0-test" dry_run: false verbose: true } let config_data = if $config != "" { load_notification_config $config } else { get_default_notification_config } let templates = generate_default_templates $test_release_info send_notification $channel $test_config $templates $config_data $test_release_info }