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

819 lines
25 KiB
Plaintext

#!/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 {
$"<item>
<title>Provisioning v($release_info.version) Released</title>
<description>($release_info.changelog)</description>
<link>($release_info.release_url)</link>
<guid>($release_info.release_url)</guid>
<pubDate>(date now | format date "%a, %d %b %Y %H:%M:%S %z")</pubDate>
</item>"
}
# 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
}