From a70fdb67ccc4ef794faa73bfb643491b10bf3cc5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:55:31 +0000 Subject: [PATCH 1/9] Add Upgrade Assistant tool for appsettings.json migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Created LinkDotNet.Blog.UpgradeAssistant console tool in tools folder - Implemented migration framework with version tracking - Added migrations for 8.0β†’9.0, 9.0β†’11.0, and 11.0β†’12.0 - Colorful console output using ANSI colors - Automatic backup functionality with timestamps - Dry-run mode to preview changes - Comprehensive documentation in docs/Migrations/UpgradeAssistant.md - Updated MIGRATION.md to reference the new tool - Added ConfigVersion field to appsettings.json - Added backups/ to .gitignore Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .gitignore | 3 + MIGRATION.md | 26 ++ docs/Migrations/UpgradeAssistant.md | 388 ++++++++++++++++++ src/LinkDotNet.Blog.Web/appsettings.json | 1 + .../CommandLineOptions.cs | 21 + .../ConsoleOutput.cs | 48 +++ .../IMigration.cs | 28 ++ .../LinkDotNet.Blog.UpgradeAssistant.csproj | 12 + .../MigrationManager.cs | 181 ++++++++ .../Migrations/Migration_11_To_12.cs | 46 +++ .../Migrations/Migration_8_To_9.cs | 85 ++++ .../Migrations/Migration_9_To_11.cs | 46 +++ .../Program.cs | 119 ++++++ .../README.md | 135 ++++++ 14 files changed, 1139 insertions(+) create mode 100644 docs/Migrations/UpgradeAssistant.md create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs create mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/README.md diff --git a/.gitignore b/.gitignore index b8d6205c..7c711d17 100644 --- a/.gitignore +++ b/.gitignore @@ -30,5 +30,8 @@ bundle.min.css /**/coverage*.xml /CoverageReport/ +# Upgrade Assistant backups +backups/ + # MacOS .DS_Store diff --git a/MIGRATION.md b/MIGRATION.md index a2dccdfe..afeb510f 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,6 +1,32 @@ # Migration Guide This document describes the changes that need to be made to migrate from one version of the blog to another. +## Automated Upgrade Assistant + +Starting with version 12.0, we provide an **Upgrade Assistant** tool that automates most configuration migrations. This tool: +- Automatically detects your current configuration version +- Applies necessary transformations to `appsettings.json` files +- Creates backups before making changes +- Provides colorful console output with clear warnings and instructions + +**Usage:** +```bash +# From your blog directory +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant + +# Preview changes without applying +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run + +# See all options +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --help +``` + +For detailed documentation, see [docs/Migrations/UpgradeAssistant.md](docs/Migrations/UpgradeAssistant.md). + +**Note:** While the Upgrade Assistant handles most configuration changes automatically, some migrations still require manual steps (especially database schema changes). These are noted below. + +--- + ## 11.0 to 12.0 `ShowBuildInformation` setting was added on the root level of the `appsettings.json` file. This setting controls whether build information (like build date) is shown in the `Footer` component. diff --git a/docs/Migrations/UpgradeAssistant.md b/docs/Migrations/UpgradeAssistant.md new file mode 100644 index 00000000..e0119114 --- /dev/null +++ b/docs/Migrations/UpgradeAssistant.md @@ -0,0 +1,388 @@ +# Configuration Upgrade Assistant + +The Blog Upgrade Assistant is an automated tool that helps you migrate your `appsettings.json` configuration files to the latest version when upgrading the blog to a new major version. + +## Why Use the Upgrade Assistant? + +When upgrading the blog to a new major version, the configuration schema may change: +- New mandatory settings may be added +- Settings may be moved between sections +- Default values may change + +The Upgrade Assistant automates these changes, reducing errors and making upgrades easier. + +## Quick Start + +1. **Before upgrading**, backup your configuration files (the tool also does this automatically) +2. Navigate to your blog installation directory +3. Run the upgrade assistant: + ```bash + dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant + ``` +4. Review the changes and update any values as needed +5. Complete any additional manual steps from [MIGRATION.md](../../MIGRATION.md) + +## Installation + +The tool is included with the blog source code in the `tools/LinkDotNet.Blog.UpgradeAssistant` directory. No separate installation is needed. + +To build the tool: +```bash +cd tools/LinkDotNet.Blog.UpgradeAssistant +dotnet build +``` + +## Usage Guide + +### Basic Migration + +The simplest way to use the tool is to run it from your blog directory: + +```bash +# Navigate to your blog installation +cd /path/to/your/blog + +# Run the upgrade assistant +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant +``` + +This will: +1. Find all `appsettings*.json` files in the current directory +2. Detect the current configuration version +3. Apply all necessary migrations +4. Save backups to `./backups` directory + +### Preview Changes (Dry Run) + +To see what changes will be made without actually modifying files: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run +``` + +The tool will display: +- Which migrations will be applied +- A preview of the modified configuration +- Warnings about new settings that require attention + +### Migrate Specific Files + +To migrate a specific configuration file: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/appsettings.Production.json +``` + +To migrate all files in a specific directory: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/config +``` + +### Custom Backup Location + +By default, backups are saved to `./backups`. To use a custom location: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --backup-dir /path/to/backup/location +``` + +## Understanding the Output + +The tool uses color-coded output for clarity: + +- 🟒 **Green (Success)**: Operation completed successfully +- πŸ”΅ **Cyan (Info)**: Informational messages +- 🟑 **Yellow (Warning)**: Warnings about settings that need attention +- πŸ”΄ **Red (Error)**: Errors that need to be resolved +- 🟣 **Magenta (Headers)**: Section headers + +### Example Output + +``` +═══ Blog Upgrade Assistant ═══ + +β„Ή Target: /path/to/blog +β„Ή Backup directory: ./backups +β„Ή Found 2 file(s) to process. + +═══ Processing: appsettings.json ═══ + +β„Ή Current version: Not set (pre-12.0) + β†’ Found 3 migration(s) to apply: + β†’ β€’ 8.0 β†’ 9.0: Moves donation settings to SupportMe section + β†’ β€’ 9.0 β†’ 11.0: Adds UseMultiAuthorMode setting + β†’ β€’ 11.0 β†’ 12.0: Adds ShowBuildInformation setting +βœ“ Backup created: ./backups/appsettings_20241221_120000.json + β†’ Applying migration: 8.0 β†’ 9.0 +⚠ Added 'ShowSimilarPosts' setting. Set to true to enable similar blog post feature. +β„Ή Note: You'll need to create the SimilarBlogPosts table. See MIGRATION.md for SQL script. +βœ“ Migration 8.0 β†’ 9.0 applied successfully. +... +βœ“ File updated successfully: /path/to/blog/appsettings.json + +═══ Migration Complete ═══ + +βœ“ All files processed successfully! +``` + +## How It Works + +### Version Detection + +The tool looks for a `ConfigVersion` field in your `appsettings.json`: + +```json +{ + "ConfigVersion": "12.0", + ... +} +``` + +If this field doesn't exist, the tool assumes you're running version 8.0 or earlier and will apply all necessary migrations. + +### Migration Chain + +The tool applies migrations sequentially: +1. Detects current version (e.g., 8.0) +2. Finds all migrations from current to latest (8.0β†’9.0, 9.0β†’11.0, 11.0β†’12.0) +3. Applies each migration in order +4. Updates the `ConfigVersion` field to the latest version + +### Backup Process + +Before making any changes, the tool: +1. Creates a `backups` directory (or uses the specified backup location) +2. Copies each file with a timestamp: `appsettings_20241221_120000.json` +3. Preserves the original file structure and formatting + +### Idempotency + +The tool is **idempotent** - running it multiple times on the same file is safe: +- If no migrations are needed, no changes are made +- Already migrated files show "No migrations needed" +- Version tracking ensures migrations aren't applied twice + +## Migration Details + +### Version 8.0 β†’ 9.0 + +**Changes:** +- Moves `KofiToken`, `GithubSponsorName`, `PatreonName` from root to `SupportMe` section +- Adds new `SupportMe` configuration options +- Adds `ShowSimilarPosts` setting + +**Before:** +```json +{ + "KofiToken": "abc123", + "GithubSponsorName": "myuser", + "PatreonName": "mypatron" +} +``` + +**After:** +```json +{ + "SupportMe": { + "KofiToken": "abc123", + "GithubSponsorName": "myuser", + "PatreonName": "mypatron", + "ShowUnderBlogPost": true, + "ShowUnderIntroduction": false, + "ShowInFooter": false, + "ShowSupportMePage": false, + "SupportMePageDescription": "" + }, + "ShowSimilarPosts": false +} +``` + +**Manual Steps Required:** +- Create `SimilarBlogPosts` database table (see [MIGRATION.md](../../MIGRATION.md)) +- Set `ShowSimilarPosts` to `true` if you want to use this feature + +### Version 9.0 β†’ 11.0 + +**Changes:** +- Adds `UseMultiAuthorMode` setting (default: `false`) + +**After:** +```json +{ + "UseMultiAuthorMode": false +} +``` + +**Manual Steps Required:** +- Set to `true` if you want multi-author support +- Configure author information per blog post + +### Version 11.0 β†’ 12.0 + +**Changes:** +- Adds `ShowBuildInformation` setting (default: `true`) + +**After:** +```json +{ + "ShowBuildInformation": true +} +``` + +**Manual Steps Required:** +- None (setting is optional) + +## Command-Line Reference + +``` +Usage: upgrade-assistant [options] + +Options: + -p, --path Path to appsettings.json file or directory + Defaults to current directory + -d, --dry-run Preview changes without applying them + -b, --backup-dir Custom backup directory path + Defaults to './backups' + -h, --help Display help message + -v, --version Display tool version +``` + +## Best Practices + +### Before Running the Tool + +1. **Backup your data** - While the tool creates backups, have your own backup strategy +2. **Read the release notes** - Check [MIGRATION.md](../../MIGRATION.md) for version-specific information +3. **Test in development** - Try the migration on a development environment first +4. **Check prerequisites** - Ensure .NET 10.0 SDK is installed + +### After Running the Tool + +1. **Review the changes** - Open your migrated configuration and verify all values +2. **Update custom settings** - Adjust any default values added by migrations +3. **Apply database migrations** - Some versions require database schema changes +4. **Test your application** - Start the blog and verify everything works +5. **Keep backups** - Don't delete the backup files until you've verified the migration + +### For Production Environments + +1. **Use dry-run first** - Always preview changes with `--dry-run` +2. **Custom backup location** - Use `--backup-dir` to specify a safe backup location +3. **Version control** - Commit your changes to version control +4. **Staged rollout** - Migrate development β†’ staging β†’ production +5. **Monitor logs** - Check application logs after migration + +## Troubleshooting + +### "No appsettings.json files found" + +**Cause:** Tool can't find configuration files in the specified location. + +**Solution:** +```bash +# Specify the correct path +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /correct/path +``` + +### "Invalid JSON in appsettings.json" + +**Cause:** Your configuration file has JSON syntax errors. + +**Solution:** +1. Open the file in a JSON validator or editor +2. Fix syntax errors (missing commas, brackets, etc.) +3. Run the tool again + +### "Configuration is already up to date" + +**Cause:** Your configuration is already at the latest version. + +**Solution:** No action needed! Your configuration is current. + +### Migration Applied but Application Fails + +**Possible Causes:** +- Database migrations not applied +- Custom configuration values need adjustment +- Environment-specific settings need updating + +**Solution:** +1. Check [MIGRATION.md](../../MIGRATION.md) for additional manual steps +2. Review application logs for specific errors +3. Verify all required database tables exist +4. Check environment-specific configuration files + +### Need to Revert Changes + +**Solution:** +1. Navigate to the backup directory (default: `./backups`) +2. Find the backup file with the timestamp before migration +3. Copy it back to your configuration location +4. Restart the application + +Example: +```bash +cp backups/appsettings_20241221_120000.json appsettings.json +``` + +## Advanced Usage + +### Batch Processing + +Process multiple configuration directories: + +```bash +for dir in /path/to/blog1 /path/to/blog2 /path/to/blog3; do + dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path $dir +done +``` + +### Automated CI/CD Integration + +Include in your deployment pipeline: + +```yaml +# Example GitHub Actions workflow +- name: Upgrade Configuration + run: | + dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant \ + --path ./deployment/config \ + --backup-dir ./backups +``` + +### Scripted Migration with Verification + +```bash +#!/bin/bash +# Upgrade with verification + +# Run dry-run first +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run --path ./config + +# Ask for confirmation +read -p "Proceed with migration? (y/n) " -n 1 -r +echo +if [[ $REPLY =~ ^[Yy]$ ]]; then + # Run actual migration + dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path ./config + echo "Migration complete. Please review the changes." +fi +``` + +## Additional Resources + +- [MIGRATION.md](../../MIGRATION.md) - Complete migration guide with manual steps +- [Configuration Documentation](../Setup/Configuration.md) - All configuration options +- [Tool README](../../tools/LinkDotNet.Blog.UpgradeAssistant/README.md) - Developer documentation + +## Getting Help + +If you encounter issues: + +1. Check this documentation and [MIGRATION.md](../../MIGRATION.md) +2. Review the tool's output messages - they often contain helpful information +3. Open an issue on GitHub with: + - Tool output (with sensitive data removed) + - Your configuration version + - Error messages or unexpected behavior diff --git a/src/LinkDotNet.Blog.Web/appsettings.json b/src/LinkDotNet.Blog.Web/appsettings.json index 01c582f5..bbddcc7d 100644 --- a/src/LinkDotNet.Blog.Web/appsettings.json +++ b/src/LinkDotNet.Blog.Web/appsettings.json @@ -1,4 +1,5 @@ { + "ConfigVersion": "12.0", "Logging": { "LogLevel": { "Default": "Information", diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs new file mode 100644 index 00000000..532d83bc --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs @@ -0,0 +1,21 @@ +using CommandLine; + +namespace LinkDotNet.Blog.UpgradeAssistant; + +public sealed class CommandLineOptions +{ + [Option('p', "path", Required = false, HelpText = "Path to appsettings.json file or directory containing appsettings files. Defaults to current directory.")] + public string? Path { get; set; } + + [Option('d', "dry-run", Required = false, Default = false, HelpText = "Preview changes without applying them.")] + public bool DryRun { get; set; } + + [Option('b', "backup-dir", Required = false, HelpText = "Custom backup directory path. Defaults to './backups'.")] + public string? BackupDirectory { get; set; } + + [Option('h', "help", Required = false, Default = false, HelpText = "Display this help message.")] + public bool Help { get; set; } + + [Option('v', "version", Required = false, Default = false, HelpText = "Display the tool version.")] + public bool Version { get; set; } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs new file mode 100644 index 00000000..4800c243 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs @@ -0,0 +1,48 @@ +namespace LinkDotNet.Blog.UpgradeAssistant; + +public static class ConsoleOutput +{ + public static void WriteSuccess(string message) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"βœ“ {message}"); + Console.ResetColor(); + } + + public static void WriteError(string message) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"βœ— {message}"); + Console.ResetColor(); + } + + public static void WriteWarning(string message) + { + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine($"⚠ {message}"); + Console.ResetColor(); + } + + public static void WriteInfo(string message) + { + Console.ForegroundColor = ConsoleColor.Cyan; + Console.WriteLine($"β„Ή {message}"); + Console.ResetColor(); + } + + public static void WriteHeader(string message) + { + Console.ForegroundColor = ConsoleColor.Magenta; + Console.WriteLine(); + Console.WriteLine($"═══ {message} ═══"); + Console.WriteLine(); + Console.ResetColor(); + } + + public static void WriteStep(string message) + { + Console.ForegroundColor = ConsoleColor.White; + Console.WriteLine($" β†’ {message}"); + Console.ResetColor(); + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs new file mode 100644 index 00000000..ac9562d5 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/IMigration.cs @@ -0,0 +1,28 @@ +using System.Text.Json; + +namespace LinkDotNet.Blog.UpgradeAssistant; + +public interface IMigration +{ + /// + /// The source version that this migration upgrades from. + /// + string FromVersion { get; } + + /// + /// The target version that this migration upgrades to. + /// + string ToVersion { get; } + + /// + /// Apply the migration to the JSON document. + /// + /// The JSON document to migrate. + /// True if changes were made, false otherwise. + bool Apply(JsonDocument document, ref string jsonContent); + + /// + /// Get a description of changes this migration will make. + /// + string GetDescription(); +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj new file mode 100644 index 00000000..6afc77e1 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj @@ -0,0 +1,12 @@ +ο»Ώ + + + Exe + enable + + + + + + + diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs new file mode 100644 index 00000000..3dd8d115 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs @@ -0,0 +1,181 @@ +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using LinkDotNet.Blog.UpgradeAssistant.Migrations; + +namespace LinkDotNet.Blog.UpgradeAssistant; + +public sealed class MigrationManager +{ + private const string CurrentVersion = "12.0"; + private readonly List _migrations; + + public MigrationManager() + { + _migrations = new List + { + new Migration8To9(), + new Migration9To11(), + new Migration11To12() + }; + } + + public async Task MigrateFileAsync(string filePath, bool dryRun, string backupDirectory) + { + if (!File.Exists(filePath)) + { + ConsoleOutput.WriteError($"File not found: {filePath}"); + return false; + } + + ConsoleOutput.WriteHeader($"Processing: {Path.GetFileName(filePath)}"); + + var content = await File.ReadAllTextAsync(filePath); + JsonDocument? document; + + try + { + document = JsonDocument.Parse(content); + } + catch (JsonException ex) + { + ConsoleOutput.WriteError($"Invalid JSON in {filePath}: {ex.Message}"); + return false; + } + + var currentVersion = GetVersion(document); + ConsoleOutput.WriteInfo($"Current version: {currentVersion ?? "Not set (pre-12.0)"}"); + + var applicableMigrations = GetApplicableMigrations(currentVersion); + + if (applicableMigrations.Count == 0) + { + ConsoleOutput.WriteSuccess("No migrations needed. Configuration is up to date."); + document.Dispose(); + return true; + } + + ConsoleOutput.WriteStep($"Found {applicableMigrations.Count} migration(s) to apply:"); + foreach (var migration in applicableMigrations) + { + ConsoleOutput.WriteStep($" β€’ {migration.FromVersion} β†’ {migration.ToVersion}: {migration.GetDescription()}"); + } + + if (dryRun) + { + ConsoleOutput.WriteWarning("DRY RUN MODE - No changes will be saved."); + } + else + { + // Create backup + var backupPath = CreateBackup(filePath, backupDirectory); + ConsoleOutput.WriteSuccess($"Backup created: {backupPath}"); + } + + var modifiedContent = content; + var hasAnyChanges = false; + + foreach (var migration in applicableMigrations) + { + ConsoleOutput.WriteStep($"Applying migration: {migration.FromVersion} β†’ {migration.ToVersion}"); + + // Re-parse for each migration + document.Dispose(); + document = JsonDocument.Parse(modifiedContent); + + if (migration.Apply(document, ref modifiedContent)) + { + ConsoleOutput.WriteSuccess($"Migration {migration.FromVersion} β†’ {migration.ToVersion} applied successfully."); + hasAnyChanges = true; + } + else + { + ConsoleOutput.WriteInfo($"Migration {migration.FromVersion} β†’ {migration.ToVersion} - No changes needed."); + } + } + + if (hasAnyChanges) + { + // Update version in the content + modifiedContent = SetVersion(modifiedContent, CurrentVersion); + + if (!dryRun) + { + await File.WriteAllTextAsync(filePath, modifiedContent); + ConsoleOutput.WriteSuccess($"File updated successfully: {filePath}"); + } + else + { + ConsoleOutput.WriteInfo("Preview of changes:"); + Console.WriteLine(modifiedContent); + } + } + + document.Dispose(); + return true; + } + + private static string? GetVersion(JsonDocument document) + { + if (document.RootElement.TryGetProperty("ConfigVersion", out var versionElement)) + { + return versionElement.GetString(); + } + + return null; // Pre-12.0 version + } + + private List GetApplicableMigrations(string? currentVersion) + { + var result = new List(); + + // If no version is set, we assume it's pre-8.0 or 8.0 + var startVersion = currentVersion ?? "8.0"; + + var currentMigrationVersion = startVersion; + var foundMigration = true; + + while (foundMigration) + { + foundMigration = false; + foreach (var migration in _migrations) + { + if (migration.FromVersion == currentMigrationVersion) + { + result.Add(migration); + currentMigrationVersion = migration.ToVersion; + foundMigration = true; + break; + } + } + } + + return result; + } + + private static string CreateBackup(string filePath, string backupDirectory) + { + var fileName = Path.GetFileName(filePath); + var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss", CultureInfo.InvariantCulture); + var backupFileName = $"{Path.GetFileNameWithoutExtension(fileName)}_{timestamp}{Path.GetExtension(fileName)}"; + var backupPath = Path.Combine(backupDirectory, backupFileName); + + Directory.CreateDirectory(backupDirectory); + File.Copy(filePath, backupPath); + + return backupPath; + } + + private static string SetVersion(string jsonContent, string version) + { + var jsonNode = JsonNode.Parse(jsonContent); + if (jsonNode is JsonObject rootObject) + { + rootObject["ConfigVersion"] = version; + var options = new JsonSerializerOptions { WriteIndented = true }; + return jsonNode.ToJsonString(options); + } + + return jsonContent; + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs new file mode 100644 index 00000000..f6feb41b --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace LinkDotNet.Blog.UpgradeAssistant.Migrations; + +/// +/// Migration from version 11.0 to 12.0. +/// Adds ShowBuildInformation setting. +/// +public sealed class Migration11To12 : IMigration +{ + public string FromVersion => "11.0"; + public string ToVersion => "12.0"; + + public bool Apply(JsonDocument document, ref string jsonContent) + { + var jsonNode = JsonNode.Parse(jsonContent); + if (jsonNode is not JsonObject rootObject) + { + return false; + } + + var hasChanges = false; + + // Add ShowBuildInformation if not present + if (!rootObject.ContainsKey("ShowBuildInformation")) + { + rootObject["ShowBuildInformation"] = true; + hasChanges = true; + ConsoleOutput.WriteInfo("Added 'ShowBuildInformation' setting. Controls display of build information in the footer."); + } + + if (hasChanges) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + jsonContent = jsonNode.ToJsonString(options); + } + + return hasChanges; + } + + public string GetDescription() + { + return "Adds ShowBuildInformation setting to control build information display."; + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs new file mode 100644 index 00000000..974f93bb --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs @@ -0,0 +1,85 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace LinkDotNet.Blog.UpgradeAssistant.Migrations; + +/// +/// Migration from version 8.0 to 9.0. +/// Moves donation-related settings to SupportMe section. +/// +public sealed class Migration8To9 : IMigration +{ + public string FromVersion => "8.0"; + public string ToVersion => "9.0"; + + public bool Apply(JsonDocument document, ref string jsonContent) + { + var jsonNode = JsonNode.Parse(jsonContent); + if (jsonNode is not JsonObject rootObject) + { + return false; + } + + var hasChanges = false; + + // Check for old donation fields + if (rootObject.ContainsKey("KofiToken") || + rootObject.ContainsKey("GithubSponsorName") || + rootObject.ContainsKey("PatreonName")) + { + var supportMe = new JsonObject(); + + if (rootObject.ContainsKey("KofiToken")) + { + supportMe["KofiToken"] = rootObject["KofiToken"]?.DeepClone(); + rootObject.Remove("KofiToken"); + hasChanges = true; + } + + if (rootObject.ContainsKey("GithubSponsorName")) + { + supportMe["GithubSponsorName"] = rootObject["GithubSponsorName"]?.DeepClone(); + rootObject.Remove("GithubSponsorName"); + hasChanges = true; + } + + if (rootObject.ContainsKey("PatreonName")) + { + supportMe["PatreonName"] = rootObject["PatreonName"]?.DeepClone(); + rootObject.Remove("PatreonName"); + hasChanges = true; + } + + // Add default values for new settings + supportMe["ShowUnderBlogPost"] = true; + supportMe["ShowUnderIntroduction"] = false; + supportMe["ShowInFooter"] = false; + supportMe["ShowSupportMePage"] = false; + supportMe["SupportMePageDescription"] = ""; + + rootObject["SupportMe"] = supportMe; + } + + // Add ShowSimilarPosts if not present + if (!rootObject.ContainsKey("ShowSimilarPosts")) + { + rootObject["ShowSimilarPosts"] = false; + hasChanges = true; + ConsoleOutput.WriteWarning("Added 'ShowSimilarPosts' setting. Set to true to enable similar blog post feature."); + ConsoleOutput.WriteInfo("Note: You'll need to create the SimilarBlogPosts table. See MIGRATION.md for SQL script."); + } + + if (hasChanges) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + jsonContent = jsonNode.ToJsonString(options); + } + + return hasChanges; + } + + public string GetDescription() + { + return "Moves donation settings (KofiToken, GithubSponsorName, PatreonName) to SupportMe section. Adds ShowSimilarPosts setting."; + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs new file mode 100644 index 00000000..4e5e3d2b --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs @@ -0,0 +1,46 @@ +using System.Text.Json; +using System.Text.Json.Nodes; + +namespace LinkDotNet.Blog.UpgradeAssistant.Migrations; + +/// +/// Migration from version 9.0 to 11.0. +/// Adds UseMultiAuthorMode setting. +/// +public sealed class Migration9To11 : IMigration +{ + public string FromVersion => "9.0"; + public string ToVersion => "11.0"; + + public bool Apply(JsonDocument document, ref string jsonContent) + { + var jsonNode = JsonNode.Parse(jsonContent); + if (jsonNode is not JsonObject rootObject) + { + return false; + } + + var hasChanges = false; + + // Add UseMultiAuthorMode if not present + if (!rootObject.ContainsKey("UseMultiAuthorMode")) + { + rootObject["UseMultiAuthorMode"] = false; + hasChanges = true; + ConsoleOutput.WriteInfo("Added 'UseMultiAuthorMode' setting. Set to true to enable multi-author support."); + } + + if (hasChanges) + { + var options = new JsonSerializerOptions { WriteIndented = true }; + jsonContent = jsonNode.ToJsonString(options); + } + + return hasChanges; + } + + public string GetDescription() + { + return "Adds UseMultiAuthorMode setting for multi-author blog support."; + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs new file mode 100644 index 00000000..743dc226 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs @@ -0,0 +1,119 @@ +ο»Ώusing CommandLine; +using LinkDotNet.Blog.UpgradeAssistant; + +return await Parser.Default.ParseArguments(args) + .MapResult( + async opts => await RunWithOptionsAsync(opts), + _ => Task.FromResult(1)); + +static async Task RunWithOptionsAsync(CommandLineOptions options) +{ + if (options.Help) + { + ShowHelp(); + return 0; + } + + if (options.Version) + { + ShowVersion(); + return 0; + } + + var targetPath = options.Path ?? Directory.GetCurrentDirectory(); + var backupDirectory = options.BackupDirectory ?? Path.Combine(Directory.GetCurrentDirectory(), "backups"); + + ConsoleOutput.WriteHeader("Blog Upgrade Assistant"); + ConsoleOutput.WriteInfo($"Target: {targetPath}"); + ConsoleOutput.WriteInfo($"Backup directory: {backupDirectory}"); + + if (options.DryRun) + { + ConsoleOutput.WriteWarning("Running in DRY RUN mode - no changes will be saved."); + } + + var manager = new MigrationManager(); + var files = GetAppsettingsFiles(targetPath); + + if (files.Count == 0) + { + ConsoleOutput.WriteError("No appsettings.json files found."); + ConsoleOutput.WriteInfo("Please specify a valid path using --path option."); + return 1; + } + + ConsoleOutput.WriteInfo($"Found {files.Count} file(s) to process."); + Console.WriteLine(); + + var allSuccessful = true; + foreach (var file in files) + { + var success = await manager.MigrateFileAsync(file, options.DryRun, backupDirectory); + allSuccessful = allSuccessful && success; + Console.WriteLine(); + } + + if (allSuccessful) + { + ConsoleOutput.WriteHeader("Migration Complete"); + ConsoleOutput.WriteSuccess("All files processed successfully!"); + + if (!options.DryRun) + { + ConsoleOutput.WriteInfo($"Backups saved to: {backupDirectory}"); + } + + ConsoleOutput.WriteInfo("Please review the changes and update any configuration values as needed."); + ConsoleOutput.WriteInfo("See MIGRATION.md for additional manual steps (database migrations, etc.)."); + return 0; + } + + ConsoleOutput.WriteError("Some files could not be processed. Please review the errors above."); + return 1; +} + +static List GetAppsettingsFiles(string path) +{ + if (File.Exists(path) && path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) + { + return new List { path }; + } + + if (Directory.Exists(path)) + { + return Directory.GetFiles(path, "appsettings*.json", SearchOption.TopDirectoryOnly) + .OrderBy(f => f) + .ToList(); + } + + return new List(); +} + +static void ShowHelp() +{ + Console.WriteLine("Blog Upgrade Assistant"); + Console.WriteLine("Automatically migrates appsettings.json files to the latest configuration version."); + Console.WriteLine(); + Console.WriteLine("Usage: upgrade-assistant [options]"); + Console.WriteLine(); + Console.WriteLine("Options:"); + Console.WriteLine(" -p, --path Path to appsettings.json file or directory"); + Console.WriteLine(" Defaults to current directory"); + Console.WriteLine(" -d, --dry-run Preview changes without applying them"); + Console.WriteLine(" -b, --backup-dir Custom backup directory path"); + Console.WriteLine(" Defaults to './backups'"); + Console.WriteLine(" -h, --help Display this help message"); + Console.WriteLine(" -v, --version Display tool version"); + Console.WriteLine(); + Console.WriteLine("Examples:"); + Console.WriteLine(" upgrade-assistant"); + Console.WriteLine(" upgrade-assistant --path /path/to/appsettings.json"); + Console.WriteLine(" upgrade-assistant --path /path/to/config/dir --dry-run"); + Console.WriteLine(" upgrade-assistant --backup-dir ./my-backups"); +} + +static void ShowVersion() +{ + Console.WriteLine("Blog Upgrade Assistant v1.0.0"); + Console.WriteLine("Target Blog Version: 12.0"); +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/README.md b/tools/LinkDotNet.Blog.UpgradeAssistant/README.md new file mode 100644 index 00000000..1d3e4b79 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/README.md @@ -0,0 +1,135 @@ +# Blog Upgrade Assistant + +A command-line tool to automatically migrate `appsettings.json` files to the latest configuration version for the LinkDotNet.Blog application. + +## Features + +- **Automatic migration** - Detects current configuration version and applies necessary migrations +- **Version tracking** - Adds `ConfigVersion` field to track configuration schema version +- **Safe backups** - Creates timestamped backups before making any changes +- **Colorful output** - Uses color-coded console messages for better visibility +- **Dry-run mode** - Preview changes without applying them +- **Multi-file support** - Can process multiple appsettings files in a directory + +## Installation + +Build the tool from source: + +```bash +cd tools/LinkDotNet.Blog.UpgradeAssistant +dotnet build +``` + +## Usage + +### Basic Usage + +Navigate to your project directory containing `appsettings.json` and run: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant +``` + +### Specify Path + +Migrate a specific file or directory: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/appsettings.json +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /path/to/config/directory +``` + +### Dry Run + +Preview changes without applying them: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run +``` + +### Custom Backup Directory + +Specify a custom backup location: + +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --backup-dir ./my-backups +``` + +## Command-Line Options + +| Option | Short | Description | +|--------|-------|-------------| +| `--path ` | `-p` | Path to appsettings.json file or directory. Defaults to current directory. | +| `--dry-run` | `-d` | Preview changes without applying them. | +| `--backup-dir ` | `-b` | Custom backup directory path. Defaults to './backups'. | +| `--help` | `-h` | Display help message. | +| `--version` | `-v` | Display tool version. | + +## Supported Migrations + +The tool currently supports migrations from version 8.0 to 12.0: + +### 8.0 β†’ 9.0 +- Moves donation settings (`KofiToken`, `GithubSponsorName`, `PatreonName`) to `SupportMe` section +- Adds `ShowSimilarPosts` setting + +### 9.0 β†’ 11.0 +- Adds `UseMultiAuthorMode` setting + +### 11.0 β†’ 12.0 +- Adds `ShowBuildInformation` setting + +## Configuration Version + +After migration, your `appsettings.json` will include a `ConfigVersion` field: + +```json +{ + "ConfigVersion": "12.0", + ... +} +``` + +This field is used to track the configuration schema version and determine which migrations need to be applied. + +## Important Notes + +- **Always backup** your configuration files before running migrations (the tool does this automatically) +- **Review changes** after migration to ensure all settings are correct +- **Database migrations** may be required separately - see `MIGRATION.md` for details +- The tool is idempotent - running it multiple times on the same file is safe + +## Examples + +### Migrate all appsettings files in current directory +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant +``` + +### Preview changes before applying +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run +``` + +### Migrate specific file with custom backup location +```bash +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- \ + --path ./appsettings.Production.json \ + --backup-dir ./config-backups +``` + +## Troubleshooting + +### "No appsettings.json files found" +Make sure you're in the correct directory or specify the path using `--path` option. + +### "Invalid JSON" +Ensure your appsettings file is valid JSON before running the migration. + +### Configuration already up to date +If you see "No migrations needed", your configuration is already at the latest version. + +## See Also + +- [MIGRATION.md](../../MIGRATION.md) - Manual migration guide +- [Configuration Documentation](../../docs/Setup/Configuration.md) - Configuration options From 6c5876db9fe64748ad0efe19e72ec63a303765a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 20:59:09 +0000 Subject: [PATCH 2/9] Add unit tests for Upgrade Assistant - Created test project LinkDotNet.Blog.UpgradeAssistant.Tests - Added comprehensive tests for Migration8To9, Migration9To11, Migration11To12 - Added tests for MigrationManager including end-to-end scenarios - All 17 tests passing Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../GlobalUsings.cs | 5 + ...kDotNet.Blog.UpgradeAssistant.Tests.csproj | 16 ++ .../Migration11To12Tests.cs | 61 +++++++ .../Migration8To9Tests.cs | 113 ++++++++++++ .../Migration9To11Tests.cs | 61 +++++++ .../MigrationManagerTests.cs | 163 ++++++++++++++++++ 6 files changed, 419 insertions(+) create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs create mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs new file mode 100644 index 00000000..38ec2ec2 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/GlobalUsings.cs @@ -0,0 +1,5 @@ +global using System; +global using System.IO; +global using System.Threading.Tasks; +global using Shouldly; +global using Xunit; diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj new file mode 100644 index 00000000..0b9f399e --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj @@ -0,0 +1,16 @@ +ο»Ώ + + + Exe + false + + + + + + + + + + + \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs new file mode 100644 index 00000000..5811e01e --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration11To12Tests.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using LinkDotNet.Blog.UpgradeAssistant.Migrations; + +namespace LinkDotNet.Blog.UpgradeAssistant.Tests; + +public class Migration11To12Tests +{ + [Fact] + public void Should_Add_ShowBuildInformation_Setting() + { + // Arrange + var migration = new Migration11To12(); + var json = """ + { + "BlogName": "Test Blog" + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeTrue(); + json.ShouldContain("\"ShowBuildInformation\": true"); + document.Dispose(); + } + + [Fact] + public void Should_Not_Change_When_Setting_Already_Exists() + { + // Arrange + var migration = new Migration11To12(); + var json = """ + { + "BlogName": "Test Blog", + "ShowBuildInformation": false + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeFalse(); + document.Dispose(); + } + + [Fact] + public void Should_Have_Correct_Version_Info() + { + // Arrange + var migration = new Migration11To12(); + + // Act & Assert + migration.FromVersion.ShouldBe("11.0"); + migration.ToVersion.ShouldBe("12.0"); + migration.GetDescription().ShouldNotBeNullOrEmpty(); + } +} diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs new file mode 100644 index 00000000..f432dd78 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs @@ -0,0 +1,113 @@ +using System.Text.Json; +using LinkDotNet.Blog.UpgradeAssistant.Migrations; + +namespace LinkDotNet.Blog.UpgradeAssistant.Tests; + +public class Migration8To9Tests +{ + [Fact] + public void Should_Move_Donation_Settings_To_SupportMe_Section() + { + // Arrange + var migration = new Migration8To9(); + var json = """ + { + "KofiToken": "abc123", + "GithubSponsorName": "testuser", + "PatreonName": "testpatron", + "OtherSetting": "value" + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeTrue(); + json.ShouldContain("\"SupportMe\""); + json.ShouldContain("\"KofiToken\": \"abc123\""); + json.ShouldContain("\"GithubSponsorName\": \"testuser\""); + json.ShouldContain("\"PatreonName\": \"testpatron\""); + document.Dispose(); + } + + [Fact] + public void Should_Add_ShowSimilarPosts_Setting() + { + // Arrange + var migration = new Migration8To9(); + var json = """ + { + "BlogName": "Test Blog" + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeTrue(); + json.ShouldContain("\"ShowSimilarPosts\": false"); + document.Dispose(); + } + + [Fact] + public void Should_Not_Change_When_No_Donation_Settings() + { + // Arrange + var migration = new Migration8To9(); + var json = """ + { + "BlogName": "Test Blog", + "ShowSimilarPosts": true + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeFalse(); + document.Dispose(); + } + + [Fact] + public void Should_Add_Default_SupportMe_Settings() + { + // Arrange + var migration = new Migration8To9(); + var json = """ + { + "KofiToken": "test" + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeTrue(); + json.ShouldContain("\"ShowUnderBlogPost\": true"); + json.ShouldContain("\"ShowUnderIntroduction\": false"); + json.ShouldContain("\"ShowInFooter\": false"); + json.ShouldContain("\"ShowSupportMePage\": false"); + json.ShouldContain("\"SupportMePageDescription\": \"\""); + document.Dispose(); + } + + [Fact] + public void Should_Have_Correct_Version_Info() + { + // Arrange + var migration = new Migration8To9(); + + // Act & Assert + migration.FromVersion.ShouldBe("8.0"); + migration.ToVersion.ShouldBe("9.0"); + migration.GetDescription().ShouldNotBeNullOrEmpty(); + } +} diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs new file mode 100644 index 00000000..18500ab5 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs @@ -0,0 +1,61 @@ +using System.Text.Json; +using LinkDotNet.Blog.UpgradeAssistant.Migrations; + +namespace LinkDotNet.Blog.UpgradeAssistant.Tests; + +public class Migration9To11Tests +{ + [Fact] + public void Should_Add_UseMultiAuthorMode_Setting() + { + // Arrange + var migration = new Migration9To11(); + var json = """ + { + "BlogName": "Test Blog" + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeTrue(); + json.ShouldContain("\"UseMultiAuthorMode\": false"); + document.Dispose(); + } + + [Fact] + public void Should_Not_Change_When_Setting_Already_Exists() + { + // Arrange + var migration = new Migration9To11(); + var json = """ + { + "BlogName": "Test Blog", + "UseMultiAuthorMode": true + } + """; + var document = JsonDocument.Parse(json); + + // Act + var result = migration.Apply(document, ref json); + + // Assert + result.ShouldBeFalse(); + document.Dispose(); + } + + [Fact] + public void Should_Have_Correct_Version_Info() + { + // Arrange + var migration = new Migration9To11(); + + // Act & Assert + migration.FromVersion.ShouldBe("9.0"); + migration.ToVersion.ShouldBe("11.0"); + migration.GetDescription().ShouldNotBeNullOrEmpty(); + } +} diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs new file mode 100644 index 00000000..2ef0e404 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs @@ -0,0 +1,163 @@ +namespace LinkDotNet.Blog.UpgradeAssistant.Tests; + +public class MigrationManagerTests : IDisposable +{ + private readonly string _testDirectory; + + public MigrationManagerTests() + { + _testDirectory = Path.Combine(Path.GetTempPath(), $"blog-test-{Guid.NewGuid()}"); + Directory.CreateDirectory(_testDirectory); + } + + [Fact] + public async Task Should_Migrate_From_8_To_12() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var json = """ + { + "BlogName": "Test Blog", + "KofiToken": "test123" + } + """; + await File.WriteAllTextAsync(testFile, json); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, false, backupDir); + + // Assert + result.ShouldBeTrue(); + var content = await File.ReadAllTextAsync(testFile); + content.ShouldContain("\"ConfigVersion\": \"12.0\""); + content.ShouldContain("\"SupportMe\""); + content.ShouldContain("\"UseMultiAuthorMode\": false"); + content.ShouldContain("\"ShowBuildInformation\": true"); + + // Verify backup was created + var backupFiles = Directory.GetFiles(backupDir); + backupFiles.ShouldNotBeEmpty(); + } + + [Fact] + public async Task Should_Not_Modify_Already_Migrated_File() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var json = """ + { + "ConfigVersion": "12.0", + "BlogName": "Test Blog", + "ShowBuildInformation": true, + "UseMultiAuthorMode": false, + "ShowSimilarPosts": false + } + """; + await File.WriteAllTextAsync(testFile, json); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, false, backupDir); + + // Assert + result.ShouldBeTrue(); + var content = await File.ReadAllTextAsync(testFile); + content.ShouldBe(json); // Should not change + } + + [Fact] + public async Task Should_Handle_Invalid_Json() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "invalid.json"); + await File.WriteAllTextAsync(testFile, "{ invalid json }"); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, false, backupDir); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task Should_Handle_Missing_File() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "nonexistent.json"); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, false, backupDir); + + // Assert + result.ShouldBeFalse(); + } + + [Fact] + public async Task DryRun_Should_Not_Modify_File() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var json = """ + { + "BlogName": "Test Blog", + "KofiToken": "test123" + } + """; + await File.WriteAllTextAsync(testFile, json); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, true, backupDir); + + // Assert + result.ShouldBeTrue(); + var content = await File.ReadAllTextAsync(testFile); + content.ShouldBe(json); // Should not change in dry-run mode + + // Verify no backup was created in dry-run mode + Directory.Exists(backupDir).ShouldBeFalse(); + } + + [Fact] + public async Task Should_Apply_Partial_Migrations() + { + // Arrange + var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var json = """ + { + "ConfigVersion": "9.0", + "BlogName": "Test Blog" + } + """; + await File.WriteAllTextAsync(testFile, json); + var manager = new MigrationManager(); + var backupDir = Path.Combine(_testDirectory, "backups"); + + // Act + var result = await manager.MigrateFileAsync(testFile, false, backupDir); + + // Assert + result.ShouldBeTrue(); + var content = await File.ReadAllTextAsync(testFile); + content.ShouldContain("\"ConfigVersion\": \"12.0\""); + content.ShouldContain("\"UseMultiAuthorMode\": false"); + content.ShouldContain("\"ShowBuildInformation\": true"); + content.ShouldNotContain("\"SupportMe\""); // Should not apply 8.0->9.0 migration + } + + public void Dispose() + { + if (Directory.Exists(_testDirectory)) + { + Directory.Delete(_testDirectory, true); + } + } +} From 49c99fe79dfd0bb42aae651f248da743ce003b16 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 21 Dec 2025 21:02:22 +0000 Subject: [PATCH 3/9] Address code review feedback - Remove OutputType from test project csproj - Fix document disposal pattern in MigrationManager - Use separate variable for each migration document Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- .../LinkDotNet.Blog.UpgradeAssistant.Tests.csproj | 1 - tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs | 5 ++--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj index 0b9f399e..4ce6b0fe 100644 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj @@ -1,7 +1,6 @@ ο»Ώ - Exe false diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs index 3dd8d115..ddd77b1f 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs @@ -80,10 +80,9 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba ConsoleOutput.WriteStep($"Applying migration: {migration.FromVersion} β†’ {migration.ToVersion}"); // Re-parse for each migration - document.Dispose(); - document = JsonDocument.Parse(modifiedContent); + using var migrationDoc = JsonDocument.Parse(modifiedContent); - if (migration.Apply(document, ref modifiedContent)) + if (migration.Apply(migrationDoc, ref modifiedContent)) { ConsoleOutput.WriteSuccess($"Migration {migration.FromVersion} β†’ {migration.ToVersion} applied successfully."); hasAnyChanges = true; From 986a8753b4813ea6242edee667048badf0a6a403 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:35:12 +0000 Subject: [PATCH 4/9] Address PR feedback - use Spectre.Console, remove old migrations, skip appsettings.json - Replace CommandLineParser with Spectre.Console for CLI and colorful output - Remove Migration_8_To_9 and Migration_9_To_11 (current version is 11.0) - Make CurrentVersion dynamic based on highest ToVersion from migrations - Skip version-controlled appsettings.json file during migration - Update tests to reflect changes (9 tests passing) - Enhanced help and version output with Figlet text and tables Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- Directory.Packages.props | 1 + .../Migration8To9Tests.cs | 113 ---------- .../Migration9To11Tests.cs | 61 ------ .../MigrationManagerTests.cs | 71 +++---- .../CommandLineOptions.cs | 21 -- .../ConsoleOutput.cs | 30 +-- .../LinkDotNet.Blog.UpgradeAssistant.csproj | 2 +- .../MigrationManager.cs | 29 ++- .../Migrations/Migration_8_To_9.cs | 85 -------- .../Migrations/Migration_9_To_11.cs | 46 ---- .../Program.cs | 197 +++++++++++------- 11 files changed, 180 insertions(+), 476 deletions(-) delete mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs delete mode 100644 tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs delete mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs delete mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs delete mode 100644 tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c75ce9a..4a8e5b20 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -58,5 +58,6 @@ + diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs deleted file mode 100644 index f432dd78..00000000 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration8To9Tests.cs +++ /dev/null @@ -1,113 +0,0 @@ -using System.Text.Json; -using LinkDotNet.Blog.UpgradeAssistant.Migrations; - -namespace LinkDotNet.Blog.UpgradeAssistant.Tests; - -public class Migration8To9Tests -{ - [Fact] - public void Should_Move_Donation_Settings_To_SupportMe_Section() - { - // Arrange - var migration = new Migration8To9(); - var json = """ - { - "KofiToken": "abc123", - "GithubSponsorName": "testuser", - "PatreonName": "testpatron", - "OtherSetting": "value" - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeTrue(); - json.ShouldContain("\"SupportMe\""); - json.ShouldContain("\"KofiToken\": \"abc123\""); - json.ShouldContain("\"GithubSponsorName\": \"testuser\""); - json.ShouldContain("\"PatreonName\": \"testpatron\""); - document.Dispose(); - } - - [Fact] - public void Should_Add_ShowSimilarPosts_Setting() - { - // Arrange - var migration = new Migration8To9(); - var json = """ - { - "BlogName": "Test Blog" - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeTrue(); - json.ShouldContain("\"ShowSimilarPosts\": false"); - document.Dispose(); - } - - [Fact] - public void Should_Not_Change_When_No_Donation_Settings() - { - // Arrange - var migration = new Migration8To9(); - var json = """ - { - "BlogName": "Test Blog", - "ShowSimilarPosts": true - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeFalse(); - document.Dispose(); - } - - [Fact] - public void Should_Add_Default_SupportMe_Settings() - { - // Arrange - var migration = new Migration8To9(); - var json = """ - { - "KofiToken": "test" - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeTrue(); - json.ShouldContain("\"ShowUnderBlogPost\": true"); - json.ShouldContain("\"ShowUnderIntroduction\": false"); - json.ShouldContain("\"ShowInFooter\": false"); - json.ShouldContain("\"ShowSupportMePage\": false"); - json.ShouldContain("\"SupportMePageDescription\": \"\""); - document.Dispose(); - } - - [Fact] - public void Should_Have_Correct_Version_Info() - { - // Arrange - var migration = new Migration8To9(); - - // Act & Assert - migration.FromVersion.ShouldBe("8.0"); - migration.ToVersion.ShouldBe("9.0"); - migration.GetDescription().ShouldNotBeNullOrEmpty(); - } -} diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs deleted file mode 100644 index 18500ab5..00000000 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/Migration9To11Tests.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Text.Json; -using LinkDotNet.Blog.UpgradeAssistant.Migrations; - -namespace LinkDotNet.Blog.UpgradeAssistant.Tests; - -public class Migration9To11Tests -{ - [Fact] - public void Should_Add_UseMultiAuthorMode_Setting() - { - // Arrange - var migration = new Migration9To11(); - var json = """ - { - "BlogName": "Test Blog" - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeTrue(); - json.ShouldContain("\"UseMultiAuthorMode\": false"); - document.Dispose(); - } - - [Fact] - public void Should_Not_Change_When_Setting_Already_Exists() - { - // Arrange - var migration = new Migration9To11(); - var json = """ - { - "BlogName": "Test Blog", - "UseMultiAuthorMode": true - } - """; - var document = JsonDocument.Parse(json); - - // Act - var result = migration.Apply(document, ref json); - - // Assert - result.ShouldBeFalse(); - document.Dispose(); - } - - [Fact] - public void Should_Have_Correct_Version_Info() - { - // Arrange - var migration = new Migration9To11(); - - // Act & Assert - migration.FromVersion.ShouldBe("9.0"); - migration.ToVersion.ShouldBe("11.0"); - migration.GetDescription().ShouldNotBeNullOrEmpty(); - } -} diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs index 2ef0e404..cf3fcdfe 100644 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs @@ -11,14 +11,13 @@ public MigrationManagerTests() } [Fact] - public async Task Should_Migrate_From_8_To_12() + public async Task Should_Migrate_From_11_To_12() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var testFile = Path.Combine(_testDirectory, "appsettings.Development.json"); var json = """ { - "BlogName": "Test Blog", - "KofiToken": "test123" + "BlogName": "Test Blog" } """; await File.WriteAllTextAsync(testFile, json); @@ -32,8 +31,6 @@ public async Task Should_Migrate_From_8_To_12() result.ShouldBeTrue(); var content = await File.ReadAllTextAsync(testFile); content.ShouldContain("\"ConfigVersion\": \"12.0\""); - content.ShouldContain("\"SupportMe\""); - content.ShouldContain("\"UseMultiAuthorMode\": false"); content.ShouldContain("\"ShowBuildInformation\": true"); // Verify backup was created @@ -45,14 +42,12 @@ public async Task Should_Migrate_From_8_To_12() public async Task Should_Not_Modify_Already_Migrated_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var testFile = Path.Combine(_testDirectory, "appsettings.Production.json"); var json = """ { "ConfigVersion": "12.0", "BlogName": "Test Blog", - "ShowBuildInformation": true, - "UseMultiAuthorMode": false, - "ShowSimilarPosts": false + "ShowBuildInformation": true } """; await File.WriteAllTextAsync(testFile, json); @@ -69,11 +64,16 @@ public async Task Should_Not_Modify_Already_Migrated_File() } [Fact] - public async Task Should_Handle_Invalid_Json() + public async Task Should_Skip_Version_Controlled_Appsettings_Json() { // Arrange - var testFile = Path.Combine(_testDirectory, "invalid.json"); - await File.WriteAllTextAsync(testFile, "{ invalid json }"); + var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var json = """ + { + "BlogName": "Test Blog" + } + """; + await File.WriteAllTextAsync(testFile, json); var manager = new MigrationManager(); var backupDir = Path.Combine(_testDirectory, "backups"); @@ -81,14 +81,18 @@ public async Task Should_Handle_Invalid_Json() var result = await manager.MigrateFileAsync(testFile, false, backupDir); // Assert - result.ShouldBeFalse(); + result.ShouldBeTrue(); + var content = await File.ReadAllTextAsync(testFile); + content.ShouldBe(json); // Should not change + Directory.Exists(backupDir).ShouldBeFalse(); // No backup created } [Fact] - public async Task Should_Handle_Missing_File() + public async Task Should_Handle_Invalid_Json() { // Arrange - var testFile = Path.Combine(_testDirectory, "nonexistent.json"); + var testFile = Path.Combine(_testDirectory, "appsettings.Invalid.json"); + await File.WriteAllTextAsync(testFile, "{ invalid json }"); var manager = new MigrationManager(); var backupDir = Path.Combine(_testDirectory, "backups"); @@ -100,40 +104,27 @@ public async Task Should_Handle_Missing_File() } [Fact] - public async Task DryRun_Should_Not_Modify_File() + public async Task Should_Handle_Missing_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.json"); - var json = """ - { - "BlogName": "Test Blog", - "KofiToken": "test123" - } - """; - await File.WriteAllTextAsync(testFile, json); + var testFile = Path.Combine(_testDirectory, "nonexistent.json"); var manager = new MigrationManager(); var backupDir = Path.Combine(_testDirectory, "backups"); // Act - var result = await manager.MigrateFileAsync(testFile, true, backupDir); + var result = await manager.MigrateFileAsync(testFile, false, backupDir); // Assert - result.ShouldBeTrue(); - var content = await File.ReadAllTextAsync(testFile); - content.ShouldBe(json); // Should not change in dry-run mode - - // Verify no backup was created in dry-run mode - Directory.Exists(backupDir).ShouldBeFalse(); + result.ShouldBeFalse(); } [Fact] - public async Task Should_Apply_Partial_Migrations() + public async Task DryRun_Should_Not_Modify_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var testFile = Path.Combine(_testDirectory, "appsettings.Development.json"); var json = """ { - "ConfigVersion": "9.0", "BlogName": "Test Blog" } """; @@ -142,15 +133,15 @@ public async Task Should_Apply_Partial_Migrations() var backupDir = Path.Combine(_testDirectory, "backups"); // Act - var result = await manager.MigrateFileAsync(testFile, false, backupDir); + var result = await manager.MigrateFileAsync(testFile, true, backupDir); // Assert result.ShouldBeTrue(); var content = await File.ReadAllTextAsync(testFile); - content.ShouldContain("\"ConfigVersion\": \"12.0\""); - content.ShouldContain("\"UseMultiAuthorMode\": false"); - content.ShouldContain("\"ShowBuildInformation\": true"); - content.ShouldNotContain("\"SupportMe\""); // Should not apply 8.0->9.0 migration + content.ShouldBe(json); // Should not change in dry-run mode + + // Verify no backup was created in dry-run mode + Directory.Exists(backupDir).ShouldBeFalse(); } public void Dispose() diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs deleted file mode 100644 index 532d83bc..00000000 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/CommandLineOptions.cs +++ /dev/null @@ -1,21 +0,0 @@ -using CommandLine; - -namespace LinkDotNet.Blog.UpgradeAssistant; - -public sealed class CommandLineOptions -{ - [Option('p', "path", Required = false, HelpText = "Path to appsettings.json file or directory containing appsettings files. Defaults to current directory.")] - public string? Path { get; set; } - - [Option('d', "dry-run", Required = false, Default = false, HelpText = "Preview changes without applying them.")] - public bool DryRun { get; set; } - - [Option('b', "backup-dir", Required = false, HelpText = "Custom backup directory path. Defaults to './backups'.")] - public string? BackupDirectory { get; set; } - - [Option('h', "help", Required = false, Default = false, HelpText = "Display this help message.")] - public bool Help { get; set; } - - [Option('v', "version", Required = false, Default = false, HelpText = "Display the tool version.")] - public bool Version { get; set; } -} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs index 4800c243..9903eacd 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs @@ -1,48 +1,38 @@ +using Spectre.Console; + namespace LinkDotNet.Blog.UpgradeAssistant; public static class ConsoleOutput { public static void WriteSuccess(string message) { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"βœ“ {message}"); - Console.ResetColor(); + AnsiConsole.MarkupLine($"[green]βœ“ {message}[/]"); } public static void WriteError(string message) { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"βœ— {message}"); - Console.ResetColor(); + AnsiConsole.MarkupLine($"[red]βœ— {message}[/]"); } public static void WriteWarning(string message) { - Console.ForegroundColor = ConsoleColor.Yellow; - Console.WriteLine($"⚠ {message}"); - Console.ResetColor(); + AnsiConsole.MarkupLine($"[yellow]⚠ {message}[/]"); } public static void WriteInfo(string message) { - Console.ForegroundColor = ConsoleColor.Cyan; - Console.WriteLine($"β„Ή {message}"); - Console.ResetColor(); + AnsiConsole.MarkupLine($"[cyan]β„Ή {message}[/]"); } public static void WriteHeader(string message) { - Console.ForegroundColor = ConsoleColor.Magenta; - Console.WriteLine(); - Console.WriteLine($"═══ {message} ═══"); - Console.WriteLine(); - Console.ResetColor(); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[magenta bold]═══ {message} ═══[/]"); + AnsiConsole.WriteLine(); } public static void WriteStep(string message) { - Console.ForegroundColor = ConsoleColor.White; - Console.WriteLine($" β†’ {message}"); - Console.ResetColor(); + AnsiConsole.MarkupLine($"[white] β†’ {message}[/]"); } } diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj index 6afc77e1..dc1642ad 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/LinkDotNet.Blog.UpgradeAssistant.csproj @@ -6,7 +6,7 @@ - + diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs index ddd77b1f..8c53f5a1 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs @@ -7,17 +7,20 @@ namespace LinkDotNet.Blog.UpgradeAssistant; public sealed class MigrationManager { - private const string CurrentVersion = "12.0"; private readonly List _migrations; + private readonly string _currentVersion; public MigrationManager() { _migrations = new List { - new Migration8To9(), - new Migration9To11(), new Migration11To12() }; + + // Determine current version from the highest ToVersion in migrations + _currentVersion = _migrations.Count > 0 + ? _migrations.Max(m => m.ToVersion) ?? "11.0" + : "11.0"; } public async Task MigrateFileAsync(string filePath, bool dryRun, string backupDirectory) @@ -28,7 +31,15 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba return false; } - ConsoleOutput.WriteHeader($"Processing: {Path.GetFileName(filePath)}"); + // Skip version-controlled appsettings.json file + var fileName = Path.GetFileName(filePath); + if (fileName.Equals("appsettings.json", StringComparison.OrdinalIgnoreCase)) + { + ConsoleOutput.WriteInfo($"Skipping version-controlled file: {fileName}"); + return true; + } + + ConsoleOutput.WriteHeader($"Processing: {fileName}"); var content = await File.ReadAllTextAsync(filePath); JsonDocument? document; @@ -44,7 +55,7 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba } var currentVersion = GetVersion(document); - ConsoleOutput.WriteInfo($"Current version: {currentVersion ?? "Not set (pre-12.0)"}"); + ConsoleOutput.WriteInfo($"Current version: {currentVersion ?? $"Not set (pre-{_currentVersion})"}"); var applicableMigrations = GetApplicableMigrations(currentVersion); @@ -96,7 +107,7 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba if (hasAnyChanges) { // Update version in the content - modifiedContent = SetVersion(modifiedContent, CurrentVersion); + modifiedContent = SetVersion(modifiedContent, _currentVersion); if (!dryRun) { @@ -121,15 +132,15 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba return versionElement.GetString(); } - return null; // Pre-12.0 version + return null; } private List GetApplicableMigrations(string? currentVersion) { var result = new List(); - // If no version is set, we assume it's pre-8.0 or 8.0 - var startVersion = currentVersion ?? "8.0"; + // If no version is set, we assume it's the previous major version + var startVersion = currentVersion ?? "11.0"; var currentMigrationVersion = startVersion; var foundMigration = true; diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs deleted file mode 100644 index 974f93bb..00000000 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_8_To_9.cs +++ /dev/null @@ -1,85 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace LinkDotNet.Blog.UpgradeAssistant.Migrations; - -/// -/// Migration from version 8.0 to 9.0. -/// Moves donation-related settings to SupportMe section. -/// -public sealed class Migration8To9 : IMigration -{ - public string FromVersion => "8.0"; - public string ToVersion => "9.0"; - - public bool Apply(JsonDocument document, ref string jsonContent) - { - var jsonNode = JsonNode.Parse(jsonContent); - if (jsonNode is not JsonObject rootObject) - { - return false; - } - - var hasChanges = false; - - // Check for old donation fields - if (rootObject.ContainsKey("KofiToken") || - rootObject.ContainsKey("GithubSponsorName") || - rootObject.ContainsKey("PatreonName")) - { - var supportMe = new JsonObject(); - - if (rootObject.ContainsKey("KofiToken")) - { - supportMe["KofiToken"] = rootObject["KofiToken"]?.DeepClone(); - rootObject.Remove("KofiToken"); - hasChanges = true; - } - - if (rootObject.ContainsKey("GithubSponsorName")) - { - supportMe["GithubSponsorName"] = rootObject["GithubSponsorName"]?.DeepClone(); - rootObject.Remove("GithubSponsorName"); - hasChanges = true; - } - - if (rootObject.ContainsKey("PatreonName")) - { - supportMe["PatreonName"] = rootObject["PatreonName"]?.DeepClone(); - rootObject.Remove("PatreonName"); - hasChanges = true; - } - - // Add default values for new settings - supportMe["ShowUnderBlogPost"] = true; - supportMe["ShowUnderIntroduction"] = false; - supportMe["ShowInFooter"] = false; - supportMe["ShowSupportMePage"] = false; - supportMe["SupportMePageDescription"] = ""; - - rootObject["SupportMe"] = supportMe; - } - - // Add ShowSimilarPosts if not present - if (!rootObject.ContainsKey("ShowSimilarPosts")) - { - rootObject["ShowSimilarPosts"] = false; - hasChanges = true; - ConsoleOutput.WriteWarning("Added 'ShowSimilarPosts' setting. Set to true to enable similar blog post feature."); - ConsoleOutput.WriteInfo("Note: You'll need to create the SimilarBlogPosts table. See MIGRATION.md for SQL script."); - } - - if (hasChanges) - { - var options = new JsonSerializerOptions { WriteIndented = true }; - jsonContent = jsonNode.ToJsonString(options); - } - - return hasChanges; - } - - public string GetDescription() - { - return "Moves donation settings (KofiToken, GithubSponsorName, PatreonName) to SupportMe section. Adds ShowSimilarPosts setting."; - } -} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs deleted file mode 100644 index 4e5e3d2b..00000000 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_9_To_11.cs +++ /dev/null @@ -1,46 +0,0 @@ -using System.Text.Json; -using System.Text.Json.Nodes; - -namespace LinkDotNet.Blog.UpgradeAssistant.Migrations; - -/// -/// Migration from version 9.0 to 11.0. -/// Adds UseMultiAuthorMode setting. -/// -public sealed class Migration9To11 : IMigration -{ - public string FromVersion => "9.0"; - public string ToVersion => "11.0"; - - public bool Apply(JsonDocument document, ref string jsonContent) - { - var jsonNode = JsonNode.Parse(jsonContent); - if (jsonNode is not JsonObject rootObject) - { - return false; - } - - var hasChanges = false; - - // Add UseMultiAuthorMode if not present - if (!rootObject.ContainsKey("UseMultiAuthorMode")) - { - rootObject["UseMultiAuthorMode"] = false; - hasChanges = true; - ConsoleOutput.WriteInfo("Added 'UseMultiAuthorMode' setting. Set to true to enable multi-author support."); - } - - if (hasChanges) - { - var options = new JsonSerializerOptions { WriteIndented = true }; - jsonContent = jsonNode.ToJsonString(options); - } - - return hasChanges; - } - - public string GetDescription() - { - return "Adds UseMultiAuthorMode setting for multi-author blog support."; - } -} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs index 743dc226..62508022 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs @@ -1,77 +1,103 @@ -ο»Ώusing CommandLine; -using LinkDotNet.Blog.UpgradeAssistant; - -return await Parser.Default.ParseArguments(args) - .MapResult( - async opts => await RunWithOptionsAsync(opts), - _ => Task.FromResult(1)); - -static async Task RunWithOptionsAsync(CommandLineOptions options) +ο»Ώusing LinkDotNet.Blog.UpgradeAssistant; +using Spectre.Console; + +var targetPath = "."; +var backupDirectory = "backups"; +var dryRun = false; +var showHelp = false; +var showVersion = false; + +// Parse command-line arguments +var i = 0; +while (i < args.Length) { - if (options.Help) + switch (args[i]) { - ShowHelp(); - return 0; + case "-p" or "--path" when i + 1 < args.Length: + i++; + targetPath = args[i]; + break; + case "-b" or "--backup-dir" when i + 1 < args.Length: + i++; + backupDirectory = args[i]; + break; + case "-d" or "--dry-run": + dryRun = true; + break; + case "-h" or "--help": + showHelp = true; + break; + case "-v" or "--version": + showVersion = true; + break; } + i++; +} - if (options.Version) - { - ShowVersion(); - return 0; - } +if (showHelp) +{ + ShowHelp(); + return 0; +} - var targetPath = options.Path ?? Directory.GetCurrentDirectory(); - var backupDirectory = options.BackupDirectory ?? Path.Combine(Directory.GetCurrentDirectory(), "backups"); +if (showVersion) +{ + ShowVersion(); + return 0; +} - ConsoleOutput.WriteHeader("Blog Upgrade Assistant"); - ConsoleOutput.WriteInfo($"Target: {targetPath}"); - ConsoleOutput.WriteInfo($"Backup directory: {backupDirectory}"); - - if (options.DryRun) - { - ConsoleOutput.WriteWarning("Running in DRY RUN mode - no changes will be saved."); - } +// Resolve to full paths +targetPath = Path.GetFullPath(targetPath); +backupDirectory = Path.GetFullPath(backupDirectory); - var manager = new MigrationManager(); - var files = GetAppsettingsFiles(targetPath); +ConsoleOutput.WriteHeader("Blog Upgrade Assistant"); +ConsoleOutput.WriteInfo($"Target: {targetPath}"); +ConsoleOutput.WriteInfo($"Backup directory: {backupDirectory}"); - if (files.Count == 0) - { - ConsoleOutput.WriteError("No appsettings.json files found."); - ConsoleOutput.WriteInfo("Please specify a valid path using --path option."); - return 1; - } +if (dryRun) +{ + ConsoleOutput.WriteWarning("Running in DRY RUN mode - no changes will be saved."); +} - ConsoleOutput.WriteInfo($"Found {files.Count} file(s) to process."); - Console.WriteLine(); +var manager = new MigrationManager(); +var files = GetAppsettingsFiles(targetPath); - var allSuccessful = true; - foreach (var file in files) - { - var success = await manager.MigrateFileAsync(file, options.DryRun, backupDirectory); - allSuccessful = allSuccessful && success; - Console.WriteLine(); - } +if (files.Count == 0) +{ + ConsoleOutput.WriteError("No appsettings files found to migrate."); + ConsoleOutput.WriteInfo("Please specify a valid path using --path option."); + return 1; +} - if (allSuccessful) +ConsoleOutput.WriteInfo($"Found {files.Count} file(s) to process."); +AnsiConsole.WriteLine(); + +var allSuccessful = true; +foreach (var file in files) +{ + var success = await manager.MigrateFileAsync(file, dryRun, backupDirectory); + allSuccessful = allSuccessful && success; + AnsiConsole.WriteLine(); +} + +if (allSuccessful) +{ + ConsoleOutput.WriteHeader("Migration Complete"); + ConsoleOutput.WriteSuccess("All files processed successfully!"); + + if (!dryRun) { - ConsoleOutput.WriteHeader("Migration Complete"); - ConsoleOutput.WriteSuccess("All files processed successfully!"); - - if (!options.DryRun) - { - ConsoleOutput.WriteInfo($"Backups saved to: {backupDirectory}"); - } - - ConsoleOutput.WriteInfo("Please review the changes and update any configuration values as needed."); - ConsoleOutput.WriteInfo("See MIGRATION.md for additional manual steps (database migrations, etc.)."); - return 0; + ConsoleOutput.WriteInfo($"Backups saved to: {backupDirectory}"); } - - ConsoleOutput.WriteError("Some files could not be processed. Please review the errors above."); - return 1; + + ConsoleOutput.WriteInfo("Please review the changes and update any configuration values as needed."); + ConsoleOutput.WriteInfo("See MIGRATION.md for additional manual steps (database migrations, etc.)."); + return 0; } +ConsoleOutput.WriteError("Some files could not be processed. Please review the errors above."); +return 1; + static List GetAppsettingsFiles(string path) { if (File.Exists(path) && path.EndsWith(".json", StringComparison.OrdinalIgnoreCase)) @@ -82,6 +108,7 @@ static List GetAppsettingsFiles(string path) if (Directory.Exists(path)) { return Directory.GetFiles(path, "appsettings*.json", SearchOption.TopDirectoryOnly) + .Where(f => !Path.GetFileName(f).Equals("appsettings.json", StringComparison.OrdinalIgnoreCase)) .OrderBy(f => f) .ToList(); } @@ -91,29 +118,39 @@ static List GetAppsettingsFiles(string path) static void ShowHelp() { - Console.WriteLine("Blog Upgrade Assistant"); - Console.WriteLine("Automatically migrates appsettings.json files to the latest configuration version."); - Console.WriteLine(); - Console.WriteLine("Usage: upgrade-assistant [options]"); - Console.WriteLine(); - Console.WriteLine("Options:"); - Console.WriteLine(" -p, --path Path to appsettings.json file or directory"); - Console.WriteLine(" Defaults to current directory"); - Console.WriteLine(" -d, --dry-run Preview changes without applying them"); - Console.WriteLine(" -b, --backup-dir Custom backup directory path"); - Console.WriteLine(" Defaults to './backups'"); - Console.WriteLine(" -h, --help Display this help message"); - Console.WriteLine(" -v, --version Display tool version"); - Console.WriteLine(); - Console.WriteLine("Examples:"); - Console.WriteLine(" upgrade-assistant"); - Console.WriteLine(" upgrade-assistant --path /path/to/appsettings.json"); - Console.WriteLine(" upgrade-assistant --path /path/to/config/dir --dry-run"); - Console.WriteLine(" upgrade-assistant --backup-dir ./my-backups"); + AnsiConsole.Write(new FigletText("Blog Upgrade Assistant").Color(Color.Magenta1)); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[bold]Automatically migrates appsettings configuration files to the latest version.[/]"); + AnsiConsole.WriteLine(); + + var table = new Table() + .Border(TableBorder.Rounded) + .AddColumn(new TableColumn("[bold cyan]Option[/]")) + .AddColumn(new TableColumn("[bold cyan]Description[/]")); + + table.AddRow("[yellow]-p, --path [/]", "Path to appsettings file or directory\nDefaults to current directory"); + table.AddRow("[yellow]-d, --dry-run[/]", "Preview changes without applying them"); + table.AddRow("[yellow]-b, --backup-dir [/]", "Custom backup directory path\nDefaults to './backups'"); + table.AddRow("[yellow]-h, --help[/]", "Display this help message"); + table.AddRow("[yellow]-v, --version[/]", "Display tool version"); + + AnsiConsole.Write(table); + AnsiConsole.WriteLine(); + + AnsiConsole.MarkupLine("[bold]Examples:[/]"); + AnsiConsole.MarkupLine(" [dim]# Migrate files in current directory[/]"); + AnsiConsole.MarkupLine(" [green]dotnet run[/]"); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [dim]# Preview changes[/]"); + AnsiConsole.MarkupLine(" [green]dotnet run -- --dry-run[/]"); + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine(" [dim]# Migrate specific file[/]"); + AnsiConsole.MarkupLine(" [green]dotnet run -- --path appsettings.Production.json[/]"); } static void ShowVersion() { - Console.WriteLine("Blog Upgrade Assistant v1.0.0"); - Console.WriteLine("Target Blog Version: 12.0"); + AnsiConsole.Write(new FigletText("v1.0.0").Color(Color.Cyan1)); + AnsiConsole.MarkupLine("[bold]Blog Upgrade Assistant[/]"); + AnsiConsole.MarkupLine($"[dim]Target Blog Version: 12.0[/]"); } From 1ece14dcbbf8c607836780311d3d3a7b1c43cee3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:48:05 +0000 Subject: [PATCH 5/9] Add projects to solution and remove unnecessary comments - Added LinkDotNet.Blog.UpgradeAssistant and LinkDotNet.Blog.UpgradeAssistant.Tests to LinkDotNet.Blog.sln - Removed inline comments from code, using descriptive method names instead - Extracted ParseCommandLineArguments method for better code organization - Created DetermineCurrentVersionFromMigrations and IsVersionControlledAppsettingsFile methods - Kept only XML documentation comments (///) which are standard - All tests passing (9/9) Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- LinkDotNet.Blog.sln | 90 +++++++++++++++++++ .../MigrationManager.cs | 22 ++--- .../Migrations/Migration_11_To_12.cs | 1 - .../Program.cs | 57 ++++++------ 4 files changed, 132 insertions(+), 38 deletions(-) diff --git a/LinkDotNet.Blog.sln b/LinkDotNet.Blog.sln index a5112d62..5e87a41f 100644 --- a/LinkDotNet.Blog.sln +++ b/LinkDotNet.Blog.sln @@ -43,40 +43,128 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tools", "tools", "{A931171C EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.CriticalCSS", "tools\LinkDotNet.Blog.CriticalCSS\LinkDotNet.Blog.CriticalCSS.csproj", "{8CB83177-C078-4953-BC27-8968D2A6E0FE}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.UpgradeAssistant", "tools\LinkDotNet.Blog.UpgradeAssistant\LinkDotNet.Blog.UpgradeAssistant.csproj", "{B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LinkDotNet.Blog.UpgradeAssistant.Tests", "tests\LinkDotNet.Blog.UpgradeAssistant.Tests\LinkDotNet.Blog.UpgradeAssistant.Tests.csproj", "{8CD027B6-B188-4091-A1A0-FD678DF98C52}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x64.ActiveCfg = Debug|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x64.Build.0 = Debug|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x86.ActiveCfg = Debug|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Debug|x86.Build.0 = Debug|Any CPU {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|Any CPU.ActiveCfg = Release|Any CPU {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|Any CPU.Build.0 = Release|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x64.ActiveCfg = Release|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x64.Build.0 = Release|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x86.ActiveCfg = Release|Any CPU + {6D6519BF-9265-488D-AA3B-C879F427930F}.Release|x86.Build.0 = Release|Any CPU {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x64.ActiveCfg = Debug|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x64.Build.0 = Debug|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x86.ActiveCfg = Debug|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Debug|x86.Build.0 = Debug|Any CPU {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|Any CPU.ActiveCfg = Release|Any CPU {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|Any CPU.Build.0 = Release|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x64.ActiveCfg = Release|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x64.Build.0 = Release|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x86.ActiveCfg = Release|Any CPU + {18F8E09D-FF0B-4FF9-93A9-971A388D0E2A}.Release|x86.Build.0 = Release|Any CPU {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x64.ActiveCfg = Debug|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x64.Build.0 = Debug|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x86.ActiveCfg = Debug|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Debug|x86.Build.0 = Debug|Any CPU {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|Any CPU.ActiveCfg = Release|Any CPU {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|Any CPU.Build.0 = Release|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x64.ActiveCfg = Release|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x64.Build.0 = Release|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x86.ActiveCfg = Release|Any CPU + {E8ED38D2-FCD3-473D-BD78-43EE78E08EE6}.Release|x86.Build.0 = Release|Any CPU {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x64.ActiveCfg = Debug|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x64.Build.0 = Debug|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x86.ActiveCfg = Debug|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Debug|x86.Build.0 = Debug|Any CPU {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|Any CPU.ActiveCfg = Release|Any CPU {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|Any CPU.Build.0 = Release|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x64.ActiveCfg = Release|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x64.Build.0 = Release|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x86.ActiveCfg = Release|Any CPU + {5B868911-7C93-4190-AEE4-3A6694F2FFCE}.Release|x86.Build.0 = Release|Any CPU {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|Any CPU.Build.0 = Debug|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x64.ActiveCfg = Debug|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x64.Build.0 = Debug|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x86.ActiveCfg = Debug|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Debug|x86.Build.0 = Debug|Any CPU {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|Any CPU.ActiveCfg = Release|Any CPU {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|Any CPU.Build.0 = Release|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x64.ActiveCfg = Release|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x64.Build.0 = Release|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x86.ActiveCfg = Release|Any CPU + {DEFDA17A-9586-4E50-83FB-8F75AC29D39A}.Release|x86.Build.0 = Release|Any CPU {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x64.ActiveCfg = Debug|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x64.Build.0 = Debug|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x86.ActiveCfg = Debug|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Debug|x86.Build.0 = Debug|Any CPU {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|Any CPU.ActiveCfg = Release|Any CPU {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|Any CPU.Build.0 = Release|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x64.ActiveCfg = Release|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x64.Build.0 = Release|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x86.ActiveCfg = Release|Any CPU + {310ABEE1-C131-43E6-A759-F2DB75A483DD}.Release|x86.Build.0 = Release|Any CPU {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x64.Build.0 = Debug|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Debug|x86.Build.0 = Debug|Any CPU {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|Any CPU.ActiveCfg = Release|Any CPU {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|Any CPU.Build.0 = Release|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x64.ActiveCfg = Release|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x64.Build.0 = Release|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x86.ActiveCfg = Release|Any CPU + {8CB83177-C078-4953-BC27-8968D2A6E0FE}.Release|x86.Build.0 = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x64.ActiveCfg = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x64.Build.0 = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x86.ActiveCfg = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Debug|x86.Build.0 = Debug|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|Any CPU.Build.0 = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x64.ActiveCfg = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x64.Build.0 = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x86.ActiveCfg = Release|Any CPU + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4}.Release|x86.Build.0 = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x64.ActiveCfg = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x64.Build.0 = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x86.ActiveCfg = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Debug|x86.Build.0 = Debug|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|Any CPU.Build.0 = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x64.ActiveCfg = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x64.Build.0 = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x86.ActiveCfg = Release|Any CPU + {8CD027B6-B188-4091-A1A0-FD678DF98C52}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -89,6 +177,8 @@ Global {DEFDA17A-9586-4E50-83FB-8F75AC29D39A} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35} {310ABEE1-C131-43E6-A759-F2DB75A483DD} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35} {8CB83177-C078-4953-BC27-8968D2A6E0FE} = {A931171C-22A6-4DB5-802B-67286B536BD2} + {B0C71EE4-FD6F-4879-9F83-AEB4FD5824E4} = {A931171C-22A6-4DB5-802B-67286B536BD2} + {8CD027B6-B188-4091-A1A0-FD678DF98C52} = {CAD2F4A3-1282-49B5-B0AB-655CDBED0A35} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {FB9B0642-F1F0-4BD8-9EDD-15C95F082180} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs index 8c53f5a1..ddc075f4 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs @@ -17,8 +17,12 @@ public MigrationManager() new Migration11To12() }; - // Determine current version from the highest ToVersion in migrations - _currentVersion = _migrations.Count > 0 + _currentVersion = DetermineCurrentVersionFromMigrations(); + } + + private string DetermineCurrentVersionFromMigrations() + { + return _migrations.Count > 0 ? _migrations.Max(m => m.ToVersion) ?? "11.0" : "11.0"; } @@ -31,9 +35,8 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba return false; } - // Skip version-controlled appsettings.json file var fileName = Path.GetFileName(filePath); - if (fileName.Equals("appsettings.json", StringComparison.OrdinalIgnoreCase)) + if (IsVersionControlledAppsettingsFile(fileName)) { ConsoleOutput.WriteInfo($"Skipping version-controlled file: {fileName}"); return true; @@ -78,7 +81,6 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba } else { - // Create backup var backupPath = CreateBackup(filePath, backupDirectory); ConsoleOutput.WriteSuccess($"Backup created: {backupPath}"); } @@ -90,7 +92,6 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba { ConsoleOutput.WriteStep($"Applying migration: {migration.FromVersion} β†’ {migration.ToVersion}"); - // Re-parse for each migration using var migrationDoc = JsonDocument.Parse(modifiedContent); if (migration.Apply(migrationDoc, ref modifiedContent)) @@ -106,7 +107,6 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba if (hasAnyChanges) { - // Update version in the content modifiedContent = SetVersion(modifiedContent, _currentVersion); if (!dryRun) @@ -125,6 +125,11 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba return true; } + private static bool IsVersionControlledAppsettingsFile(string fileName) + { + return fileName.Equals("appsettings.json", StringComparison.OrdinalIgnoreCase); + } + private static string? GetVersion(JsonDocument document) { if (document.RootElement.TryGetProperty("ConfigVersion", out var versionElement)) @@ -138,10 +143,7 @@ public async Task MigrateFileAsync(string filePath, bool dryRun, string ba private List GetApplicableMigrations(string? currentVersion) { var result = new List(); - - // If no version is set, we assume it's the previous major version var startVersion = currentVersion ?? "11.0"; - var currentMigrationVersion = startVersion; var foundMigration = true; diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs index f6feb41b..be03312a 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs @@ -22,7 +22,6 @@ public bool Apply(JsonDocument document, ref string jsonContent) var hasChanges = false; - // Add ShowBuildInformation if not present if (!rootObject.ContainsKey("ShowBuildInformation")) { rootObject["ShowBuildInformation"] = true; diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs index 62508022..57cb1ecd 100644 --- a/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs @@ -7,32 +7,7 @@ var showHelp = false; var showVersion = false; -// Parse command-line arguments -var i = 0; -while (i < args.Length) -{ - switch (args[i]) - { - case "-p" or "--path" when i + 1 < args.Length: - i++; - targetPath = args[i]; - break; - case "-b" or "--backup-dir" when i + 1 < args.Length: - i++; - backupDirectory = args[i]; - break; - case "-d" or "--dry-run": - dryRun = true; - break; - case "-h" or "--help": - showHelp = true; - break; - case "-v" or "--version": - showVersion = true; - break; - } - i++; -} +ParseCommandLineArguments(args, ref targetPath, ref backupDirectory, ref dryRun, ref showHelp, ref showVersion); if (showHelp) { @@ -46,7 +21,6 @@ return 0; } -// Resolve to full paths targetPath = Path.GetFullPath(targetPath); backupDirectory = Path.GetFullPath(backupDirectory); @@ -154,3 +128,32 @@ static void ShowVersion() AnsiConsole.MarkupLine("[bold]Blog Upgrade Assistant[/]"); AnsiConsole.MarkupLine($"[dim]Target Blog Version: 12.0[/]"); } + +static void ParseCommandLineArguments(string[] args, ref string targetPath, ref string backupDirectory, ref bool dryRun, ref bool showHelp, ref bool showVersion) +{ + var i = 0; + while (i < args.Length) + { + switch (args[i]) + { + case "-p" or "--path" when i + 1 < args.Length: + i++; + targetPath = args[i]; + break; + case "-b" or "--backup-dir" when i + 1 < args.Length: + i++; + backupDirectory = args[i]; + break; + case "-d" or "--dry-run": + dryRun = true; + break; + case "-h" or "--help": + showHelp = true; + break; + case "-v" or "--version": + showVersion = true; + break; + } + i++; + } +} From 35820c854a3485791af801fb832bcef229a8bf0a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 22 Dec 2025 18:59:23 +0000 Subject: [PATCH 6/9] Add Automated Migration documentation to README files - Added chapter about Automated Configuration Migration (Version 12+) in docs/Migrations/Readme.md - Documented Upgrade Assistant features, usage examples, and benefits - Added "Automated Configuration Migration" as a feature in main Readme.md - Provided clear migration path: automated for v12+, manual for pre-v12 Co-authored-by: linkdotnet <26365461+linkdotnet@users.noreply.github.com> --- Readme.md | 1 + docs/Migrations/Readme.md | 31 ++++++++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 1 deletion(-) diff --git a/Readme.md b/Readme.md index 6d97ca09..b62b6122 100644 --- a/Readme.md +++ b/Readme.md @@ -27,6 +27,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js] - **About Me Page** - Customizable profile page that showcases skills and experience - **RSS Feed** - Allow readers to subscribe to content updates - **Visit Counter** - Get visitor counters for each blog post in the internal dashboard +- **Automated Configuration Migration** - Upgrade Assistant tool automatically migrates appsettings.json files between versions ## In Action diff --git a/docs/Migrations/Readme.md b/docs/Migrations/Readme.md index 5eb0637c..88787a7f 100644 --- a/docs/Migrations/Readme.md +++ b/docs/Migrations/Readme.md @@ -6,4 +6,33 @@ This is contrasted by Minor changes. These are things where the user does not ne Breaking changes are recorded in the [MIGRATION.md](../../MIGRATION.md). Since version 9 of the blog, β€œEntity Framework Migrations” has been introduced for all SQL providers. You can read more in the [documentation](../Storage/Readme.md). In a nutshell, this means that database migration can be carried out easily via the β€œef migration” CLI tool. More on this in the documentation linked above. -Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md. \ No newline at end of file +Changes for the appsettings.json must currently still be made manually. The exact changes that need to be made here can be found in MIGRATION.md. + +## Automated Configuration Migration (Version 12+) + +Starting with **version 12**, the blog includes an **Automated Upgrade Assistant** that handles appsettings.json migrations automatically. This tool eliminates the need for manual configuration changes in most cases. + +### What the Upgrade Assistant Does + +- **Automatic Detection** - Detects your current configuration version +- **Sequential Migration** - Applies necessary migrations step-by-step +- **Safe Backups** - Creates timestamped backups before making any changes +- **Smart Filtering** - Skips version-controlled `appsettings.json`, only migrates environment-specific files +- **Colorful Output** - Provides clear, color-coded feedback about changes and warnings + +### How to Use + +Run the Upgrade Assistant from your blog directory: + +```bash +# Preview changes without applying them +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --dry-run + +# Migrate all environment-specific appsettings files +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant + +# Migrate a specific file +dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path appsettings.Production.json +``` + +For detailed documentation on the Upgrade Assistant, see [UpgradeAssistant.md](./UpgradeAssistant.md). \ No newline at end of file From 5520da7670a05ce1c44b71b00cc469271d353d9f Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 22 Dec 2025 20:04:36 +0100 Subject: [PATCH 7/9] docs: Add clarification and cleanup --- docs/Migrations/UpgradeAssistant.md | 86 ++++------------------------- 1 file changed, 10 insertions(+), 76 deletions(-) diff --git a/docs/Migrations/UpgradeAssistant.md b/docs/Migrations/UpgradeAssistant.md index e0119114..00ee3abd 100644 --- a/docs/Migrations/UpgradeAssistant.md +++ b/docs/Migrations/UpgradeAssistant.md @@ -1,6 +1,6 @@ # Configuration Upgrade Assistant -The Blog Upgrade Assistant is an automated tool that helps you migrate your `appsettings.json` configuration files to the latest version when upgrading the blog to a new major version. +The Blog Upgrade Assistant is an automated tool that helps you migrate your `appsettings.json` configuration files to the latest version when upgrading the blog to a new major version. This was introduced in version 12 of the blog. The update software tries to upgrade from version 11 to 12 automatically but not from earlier versions. ## Why Use the Upgrade Assistant? @@ -109,15 +109,12 @@ The tool uses color-coded output for clarity: ═══ Processing: appsettings.json ═══ β„Ή Current version: Not set (pre-12.0) - β†’ Found 3 migration(s) to apply: - β†’ β€’ 8.0 β†’ 9.0: Moves donation settings to SupportMe section - β†’ β€’ 9.0 β†’ 11.0: Adds UseMultiAuthorMode setting - β†’ β€’ 11.0 β†’ 12.0: Adds ShowBuildInformation setting -βœ“ Backup created: ./backups/appsettings_20241221_120000.json - β†’ Applying migration: 8.0 β†’ 9.0 -⚠ Added 'ShowSimilarPosts' setting. Set to true to enable similar blog post feature. -β„Ή Note: You'll need to create the SimilarBlogPosts table. See MIGRATION.md for SQL script. -βœ“ Migration 8.0 β†’ 9.0 applied successfully. + β†’ Found 1 migration(s) to apply: + β†’ β€’ 11.0 β†’ 12.0: Adds ShowBuildInformation setting to control build information display. +βœ“ Backup created: ./backups/appsettings_20251221_120000.json + β†’ Applying migration: 11.0 β†’ 12.0 +β„Ή Added 'ShowBuildInformation' setting. Controls display of build information in the footer. +βœ“ Migration 11.0 β†’ 12.0 applied successfully. ... βœ“ File updated successfully: /path/to/blog/appsettings.json @@ -144,8 +141,8 @@ If this field doesn't exist, the tool assumes you're running version 8.0 or earl ### Migration Chain The tool applies migrations sequentially: -1. Detects current version (e.g., 8.0) -2. Finds all migrations from current to latest (8.0β†’9.0, 9.0β†’11.0, 11.0β†’12.0) +1. Detects current version (e.g., 12.0) +2. Finds all migrations from current to latest (11.0β†’12.0) 3. Applies each migration in order 4. Updates the `ConfigVersion` field to the latest version @@ -165,44 +162,6 @@ The tool is **idempotent** - running it multiple times on the same file is safe: ## Migration Details -### Version 8.0 β†’ 9.0 - -**Changes:** -- Moves `KofiToken`, `GithubSponsorName`, `PatreonName` from root to `SupportMe` section -- Adds new `SupportMe` configuration options -- Adds `ShowSimilarPosts` setting - -**Before:** -```json -{ - "KofiToken": "abc123", - "GithubSponsorName": "myuser", - "PatreonName": "mypatron" -} -``` - -**After:** -```json -{ - "SupportMe": { - "KofiToken": "abc123", - "GithubSponsorName": "myuser", - "PatreonName": "mypatron", - "ShowUnderBlogPost": true, - "ShowUnderIntroduction": false, - "ShowInFooter": false, - "ShowSupportMePage": false, - "SupportMePageDescription": "" - }, - "ShowSimilarPosts": false -} -``` - -**Manual Steps Required:** -- Create `SimilarBlogPosts` database table (see [MIGRATION.md](../../MIGRATION.md)) -- Set `ShowSimilarPosts` to `true` if you want to use this feature - -### Version 9.0 β†’ 11.0 **Changes:** - Adds `UseMultiAuthorMode` setting (default: `false`) @@ -248,31 +207,6 @@ Options: -v, --version Display tool version ``` -## Best Practices - -### Before Running the Tool - -1. **Backup your data** - While the tool creates backups, have your own backup strategy -2. **Read the release notes** - Check [MIGRATION.md](../../MIGRATION.md) for version-specific information -3. **Test in development** - Try the migration on a development environment first -4. **Check prerequisites** - Ensure .NET 10.0 SDK is installed - -### After Running the Tool - -1. **Review the changes** - Open your migrated configuration and verify all values -2. **Update custom settings** - Adjust any default values added by migrations -3. **Apply database migrations** - Some versions require database schema changes -4. **Test your application** - Start the blog and verify everything works -5. **Keep backups** - Don't delete the backup files until you've verified the migration - -### For Production Environments - -1. **Use dry-run first** - Always preview changes with `--dry-run` -2. **Custom backup location** - Use `--backup-dir` to specify a safe backup location -3. **Version control** - Commit your changes to version control -4. **Staged rollout** - Migrate development β†’ staging β†’ production -5. **Monitor logs** - Check application logs after migration - ## Troubleshooting ### "No appsettings.json files found" @@ -323,7 +257,7 @@ dotnet run --project tools/LinkDotNet.Blog.UpgradeAssistant -- --path /correct/p Example: ```bash -cp backups/appsettings_20241221_120000.json appsettings.json +cp backups/appsettings_20251221_120000.json appsettings.json ``` ## Advanced Usage From 5e48cf19728f9eddebd6ab49ba8508e6da252391 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 22 Dec 2025 20:05:41 +0100 Subject: [PATCH 8/9] docs: Add readme --- Readme.md | 1 + 1 file changed, 1 insertion(+) diff --git a/Readme.md b/Readme.md index b62b6122..a36c69e8 100644 --- a/Readme.md +++ b/Readme.md @@ -28,6 +28,7 @@ This also includes source code snippets. Highlighting is done via [highlight.js] - **RSS Feed** - Allow readers to subscribe to content updates - **Visit Counter** - Get visitor counters for each blog post in the internal dashboard - **Automated Configuration Migration** - Upgrade Assistant tool automatically migrates appsettings.json files between versions +- **Automated Database Migrations** - Seamless database schema updates using Entity Framework Migrations ## In Action From bb05a145a47b174b4b46e6cd14e6a72a5c25ca25 Mon Sep 17 00:00:00 2001 From: Steven Giesel Date: Mon, 22 Dec 2025 20:12:51 +0100 Subject: [PATCH 9/9] fix: Build errors --- ...kDotNet.Blog.UpgradeAssistant.Tests.csproj | 5 +- .../MigrationManagerTests.cs | 56 ++++++++++--------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj index 4ce6b0fe..a09ed594 100644 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj @@ -1,6 +1,7 @@ ο»Ώ + Exe false @@ -8,8 +9,4 @@ - - - - \ No newline at end of file diff --git a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs index cf3fcdfe..ea483f18 100644 --- a/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs @@ -1,35 +1,37 @@ +using TestContext = Xunit.TestContext; + namespace LinkDotNet.Blog.UpgradeAssistant.Tests; -public class MigrationManagerTests : IDisposable +public sealed class MigrationManagerTests : IDisposable { - private readonly string _testDirectory; + private readonly string testDirectory; public MigrationManagerTests() { - _testDirectory = Path.Combine(Path.GetTempPath(), $"blog-test-{Guid.NewGuid()}"); - Directory.CreateDirectory(_testDirectory); + testDirectory = Path.Combine(Path.GetTempPath(), $"blog-test-{Guid.NewGuid()}"); + Directory.CreateDirectory(testDirectory); } [Fact] public async Task Should_Migrate_From_11_To_12() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.Development.json"); + var testFile = Path.Combine(testDirectory, "appsettings.Development.json"); var json = """ { "BlogName": "Test Blog" } """; - await File.WriteAllTextAsync(testFile, json); + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, false, backupDir); // Assert result.ShouldBeTrue(); - var content = await File.ReadAllTextAsync(testFile); + var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken); content.ShouldContain("\"ConfigVersion\": \"12.0\""); content.ShouldContain("\"ShowBuildInformation\": true"); @@ -42,7 +44,7 @@ public async Task Should_Migrate_From_11_To_12() public async Task Should_Not_Modify_Already_Migrated_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.Production.json"); + var testFile = Path.Combine(testDirectory, "appsettings.Production.json"); var json = """ { "ConfigVersion": "12.0", @@ -50,16 +52,16 @@ public async Task Should_Not_Modify_Already_Migrated_File() "ShowBuildInformation": true } """; - await File.WriteAllTextAsync(testFile, json); + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, false, backupDir); // Assert result.ShouldBeTrue(); - var content = await File.ReadAllTextAsync(testFile); + var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken); content.ShouldBe(json); // Should not change } @@ -67,22 +69,22 @@ public async Task Should_Not_Modify_Already_Migrated_File() public async Task Should_Skip_Version_Controlled_Appsettings_Json() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.json"); + var testFile = Path.Combine(testDirectory, "appsettings.json"); var json = """ { "BlogName": "Test Blog" } """; - await File.WriteAllTextAsync(testFile, json); + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, false, backupDir); // Assert result.ShouldBeTrue(); - var content = await File.ReadAllTextAsync(testFile); + var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken); content.ShouldBe(json); // Should not change Directory.Exists(backupDir).ShouldBeFalse(); // No backup created } @@ -91,10 +93,10 @@ public async Task Should_Skip_Version_Controlled_Appsettings_Json() public async Task Should_Handle_Invalid_Json() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.Invalid.json"); - await File.WriteAllTextAsync(testFile, "{ invalid json }"); + var testFile = Path.Combine(testDirectory, "appsettings.Invalid.json"); + await File.WriteAllTextAsync(testFile, "{ invalid json }", TestContext.Current.CancellationToken); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, false, backupDir); @@ -107,9 +109,9 @@ public async Task Should_Handle_Invalid_Json() public async Task Should_Handle_Missing_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "nonexistent.json"); + var testFile = Path.Combine(testDirectory, "nonexistent.json"); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, false, backupDir); @@ -122,22 +124,22 @@ public async Task Should_Handle_Missing_File() public async Task DryRun_Should_Not_Modify_File() { // Arrange - var testFile = Path.Combine(_testDirectory, "appsettings.Development.json"); + var testFile = Path.Combine(testDirectory, "appsettings.Development.json"); var json = """ { "BlogName": "Test Blog" } """; - await File.WriteAllTextAsync(testFile, json); + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); var manager = new MigrationManager(); - var backupDir = Path.Combine(_testDirectory, "backups"); + var backupDir = Path.Combine(testDirectory, "backups"); // Act var result = await manager.MigrateFileAsync(testFile, true, backupDir); // Assert result.ShouldBeTrue(); - var content = await File.ReadAllTextAsync(testFile); + var content = await File.ReadAllTextAsync(testFile, TestContext.Current.CancellationToken); content.ShouldBe(json); // Should not change in dry-run mode // Verify no backup was created in dry-run mode @@ -146,9 +148,9 @@ public async Task DryRun_Should_Not_Modify_File() public void Dispose() { - if (Directory.Exists(_testDirectory)) + if (Directory.Exists(testDirectory)) { - Directory.Delete(_testDirectory, true); + Directory.Delete(testDirectory, true); } } }