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/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/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/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/Readme.md b/Readme.md index 6d97ca09..a36c69e8 100644 --- a/Readme.md +++ b/Readme.md @@ -27,6 +27,8 @@ 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 +- **Automated Database Migrations** - Seamless database schema updates using Entity Framework Migrations ## 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 diff --git a/docs/Migrations/UpgradeAssistant.md b/docs/Migrations/UpgradeAssistant.md new file mode 100644 index 00000000..00ee3abd --- /dev/null +++ b/docs/Migrations/UpgradeAssistant.md @@ -0,0 +1,322 @@ +# 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. 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? + +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 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 + +═══ 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., 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 + +### 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 + + +**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 +``` + +## 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_20251221_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/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..a09ed594 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/LinkDotNet.Blog.UpgradeAssistant.Tests.csproj @@ -0,0 +1,12 @@ +ο»Ώ + + + 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/MigrationManagerTests.cs b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs new file mode 100644 index 00000000..ea483f18 --- /dev/null +++ b/tests/LinkDotNet.Blog.UpgradeAssistant.Tests/MigrationManagerTests.cs @@ -0,0 +1,156 @@ +using TestContext = Xunit.TestContext; + +namespace LinkDotNet.Blog.UpgradeAssistant.Tests; + +public sealed 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_11_To_12() + { + // Arrange + var testFile = Path.Combine(testDirectory, "appsettings.Development.json"); + var json = """ + { + "BlogName": "Test Blog" + } + """; + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); + 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, TestContext.Current.CancellationToken); + content.ShouldContain("\"ConfigVersion\": \"12.0\""); + 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.Production.json"); + var json = """ + { + "ConfigVersion": "12.0", + "BlogName": "Test Blog", + "ShowBuildInformation": true + } + """; + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); + 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, TestContext.Current.CancellationToken); + content.ShouldBe(json); // Should not change + } + + [Fact] + public async Task Should_Skip_Version_Controlled_Appsettings_Json() + { + // Arrange + var testFile = Path.Combine(testDirectory, "appsettings.json"); + var json = """ + { + "BlogName": "Test Blog" + } + """; + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); + 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, TestContext.Current.CancellationToken); + content.ShouldBe(json); // Should not change + Directory.Exists(backupDir).ShouldBeFalse(); // No backup created + } + + [Fact] + public async Task Should_Handle_Invalid_Json() + { + // Arrange + 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"); + + // 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.Development.json"); + var json = """ + { + "BlogName": "Test Blog" + } + """; + await File.WriteAllTextAsync(testFile, json, TestContext.Current.CancellationToken); + 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, TestContext.Current.CancellationToken); + 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() + { + if (Directory.Exists(testDirectory)) + { + Directory.Delete(testDirectory, true); + } + } +} diff --git a/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs new file mode 100644 index 00000000..9903eacd --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/ConsoleOutput.cs @@ -0,0 +1,38 @@ +using Spectre.Console; + +namespace LinkDotNet.Blog.UpgradeAssistant; + +public static class ConsoleOutput +{ + public static void WriteSuccess(string message) + { + AnsiConsole.MarkupLine($"[green]βœ“ {message}[/]"); + } + + public static void WriteError(string message) + { + AnsiConsole.MarkupLine($"[red]βœ— {message}[/]"); + } + + public static void WriteWarning(string message) + { + AnsiConsole.MarkupLine($"[yellow]⚠ {message}[/]"); + } + + public static void WriteInfo(string message) + { + AnsiConsole.MarkupLine($"[cyan]β„Ή {message}[/]"); + } + + public static void WriteHeader(string message) + { + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine($"[magenta bold]═══ {message} ═══[/]"); + AnsiConsole.WriteLine(); + } + + public static void WriteStep(string message) + { + AnsiConsole.MarkupLine($"[white] β†’ {message}[/]"); + } +} 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..dc1642ad --- /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..ddc075f4 --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/MigrationManager.cs @@ -0,0 +1,193 @@ +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 readonly List _migrations; + private readonly string _currentVersion; + + public MigrationManager() + { + _migrations = new List + { + new Migration11To12() + }; + + _currentVersion = DetermineCurrentVersionFromMigrations(); + } + + private string DetermineCurrentVersionFromMigrations() + { + return _migrations.Count > 0 + ? _migrations.Max(m => m.ToVersion) ?? "11.0" + : "11.0"; + } + + public async Task MigrateFileAsync(string filePath, bool dryRun, string backupDirectory) + { + if (!File.Exists(filePath)) + { + ConsoleOutput.WriteError($"File not found: {filePath}"); + return false; + } + + var fileName = Path.GetFileName(filePath); + if (IsVersionControlledAppsettingsFile(fileName)) + { + ConsoleOutput.WriteInfo($"Skipping version-controlled file: {fileName}"); + return true; + } + + ConsoleOutput.WriteHeader($"Processing: {fileName}"); + + 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-{_currentVersion})"}"); + + 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 + { + 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}"); + + using var migrationDoc = JsonDocument.Parse(modifiedContent); + + if (migration.Apply(migrationDoc, 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) + { + 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 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)) + { + return versionElement.GetString(); + } + + return null; + } + + private List GetApplicableMigrations(string? currentVersion) + { + var result = new List(); + var startVersion = currentVersion ?? "11.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..be03312a --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Migrations/Migration_11_To_12.cs @@ -0,0 +1,45 @@ +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; + + 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/Program.cs b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs new file mode 100644 index 00000000..57cb1ecd --- /dev/null +++ b/tools/LinkDotNet.Blog.UpgradeAssistant/Program.cs @@ -0,0 +1,159 @@ +ο»Ώusing LinkDotNet.Blog.UpgradeAssistant; +using Spectre.Console; + +var targetPath = "."; +var backupDirectory = "backups"; +var dryRun = false; +var showHelp = false; +var showVersion = false; + +ParseCommandLineArguments(args, ref targetPath, ref backupDirectory, ref dryRun, ref showHelp, ref showVersion); + +if (showHelp) +{ + ShowHelp(); + return 0; +} + +if (showVersion) +{ + ShowVersion(); + return 0; +} + +targetPath = Path.GetFullPath(targetPath); +backupDirectory = Path.GetFullPath(backupDirectory); + +ConsoleOutput.WriteHeader("Blog Upgrade Assistant"); +ConsoleOutput.WriteInfo($"Target: {targetPath}"); +ConsoleOutput.WriteInfo($"Backup directory: {backupDirectory}"); + +if (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 files found to migrate."); + ConsoleOutput.WriteInfo("Please specify a valid path using --path option."); + return 1; +} + +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.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) + .Where(f => !Path.GetFileName(f).Equals("appsettings.json", StringComparison.OrdinalIgnoreCase)) + .OrderBy(f => f) + .ToList(); + } + + return new List(); +} + +static void ShowHelp() +{ + 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() +{ + AnsiConsole.Write(new FigletText("v1.0.0").Color(Color.Cyan1)); + 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++; + } +} 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