diff --git a/AssemblyAnalyzer/Reader/LocalReader.cs b/AssemblyAnalyzer/Reader/LocalReader.cs index 4c05a2f..b250a59 100644 --- a/AssemblyAnalyzer/Reader/LocalReader.cs +++ b/AssemblyAnalyzer/Reader/LocalReader.cs @@ -38,7 +38,7 @@ public async Task ReadAssemblyAsync(string assemblyDllPath, string } logger.LogDebug("Reading assembly from {AssemblyDllPath}", assemblyDllPath); - var assemblyInfo = await ReadAssemblyInternalAsync(assemblyDllPath, publisherPrefix, sharedOptions.Value.ConfigName, cancellationToken); + var assemblyInfo = await ReadAssemblyInternalAsync(assemblyDllPath, publisherPrefix, sharedOptions.Value.ProfileName, cancellationToken); // Cache the assembly info assemblyCache[assemblyDllPath] = assemblyInfo; @@ -78,7 +78,7 @@ public List ReadWebResourceFolder(string folderPath, stri ]; } - private async Task ReadAssemblyInternalAsync(string assemblyDllPath, string publisherPrefix, string configName, CancellationToken cancellationToken) + private async Task ReadAssemblyInternalAsync(string assemblyDllPath, string publisherPrefix, string? configName, CancellationToken cancellationToken) { var (filename, args) = await GetExecutionInfoAsync(assemblyDllPath, publisherPrefix, configName, cancellationToken); @@ -97,9 +97,10 @@ private async Task ReadAssemblyInternalAsync(string assemblyDllPat return assemblyInfo ?? throw new AnalysisException("Failed to read plugin type information from assembly"); } - private async Task<(string filename, string args)> GetExecutionInfoAsync(string assemblyDllPath, string publisherPrefix, string configName, CancellationToken cancellationToken) + private async Task<(string filename, string args)> GetExecutionInfoAsync(string assemblyDllPath, string publisherPrefix, string? configName, CancellationToken cancellationToken) { - var baseArgs = $"analyze --assembly \"{assemblyDllPath}\" --prefix \"{publisherPrefix}\" --config \"{configName}\""; + var configArg = !string.IsNullOrWhiteSpace(configName) ? $" --profile \"{configName}\"" : ""; + var baseArgs = $"analyze --assembly \"{assemblyDllPath}\" --prefix \"{publisherPrefix}\"{configArg}"; #if DEBUG // In debug, try to invoke the currently executing assembly first diff --git a/CHANGELOG.md b/CHANGELOG.md index e1f4f75..b98392b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +### Unreleased +* Refactor: The configuration format has been updated +* Remove: `--save-config` and `--save-config-to` options - configuration files should be created manually + ### v1.0.0-preview.14 - 06 November 2025 * Add: Validation rule to prevent duplicate webresource creation diff --git a/CLAUDE.md b/CLAUDE.md index ffd710a..ef8bebf 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -76,11 +76,45 @@ The solution is organized into distinct layers with clear separation of concerns 5. Execute operations via `IWebresourceWriter` **Configuration System**: -- Hierarchical configuration under `XrmSync` section in `appsettings.json` -- Named configurations support (e.g., "default", "dev", "prod") -- CLI options override configuration file values -- `--save-config` flag generates/updates configuration files from CLI arguments -- Root command can execute multiple sub-commands from a single configuration +- Profile-based configuration under `XrmSync` section in `appsettings.json` +- Global settings (DryRun, LogLevel, CiMode) apply to all profiles +- Each profile contains a solution name and list of sync items (Plugin, PluginAnalysis, Webresource) +- Profile support (e.g., "default", "dev", "prod") via `--profile` flag +- CLI options override configuration file values for standalone execution +- Root command can execute all sync items in a profile sequentially + +**Configuration Format**: +```json +{ + "XrmSync": { + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "dev", + "SolutionName": "MySolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "../path/to/plugin.dll" + }, + { + "Type": "Webresource", + "FolderPath": "../path/to/webresources" + }, + { + "Type": "PluginAnalysis", + "AssemblyPath": "../path/to/plugin.dll", + "PublisherPrefix": "new", + "PrettyPrint": true + } + ] + } + ] + } +} +``` **Command Architecture**: - All commands implement `IXrmSyncCommand` and extend `XrmSyncCommandBase` diff --git a/Dataverse/CustomApiWriter.cs b/Dataverse/CustomApiWriter.cs index 854fd6f..b413f27 100644 --- a/Dataverse/CustomApiWriter.cs +++ b/Dataverse/CustomApiWriter.cs @@ -8,7 +8,7 @@ namespace XrmSync.Dataverse; -internal class CustomApiWriter(IDataverseWriter writer, IOptions configuration) : ICustomApiWriter +internal class CustomApiWriter(IDataverseWriter writer, IOptions configuration) : ICustomApiWriter { private Dictionary Parameters { get; } = new() { { "SolutionUniqueName", configuration.Value.SolutionName } diff --git a/Dataverse/DataverseWriter.cs b/Dataverse/DataverseWriter.cs index b45d931..0bd0605 100644 --- a/Dataverse/DataverseWriter.cs +++ b/Dataverse/DataverseWriter.cs @@ -15,7 +15,7 @@ internal sealed class DataverseWriter : IDataverseWriter private readonly ServiceClient serviceClient; private readonly ILogger logger; - public DataverseWriter(ServiceClient serviceClient, ILogger logger, IOptions configuration) + public DataverseWriter(ServiceClient serviceClient, ILogger logger, IOptions configuration) { if (configuration.Value.DryRun) { diff --git a/Dataverse/DryRunDataverseWriter.cs b/Dataverse/DryRunDataverseWriter.cs index b71a99f..0935fc4 100644 --- a/Dataverse/DryRunDataverseWriter.cs +++ b/Dataverse/DryRunDataverseWriter.cs @@ -14,7 +14,7 @@ internal class DryRunDataverseWriter : IDataverseWriter { private readonly ILogger logger; - public DryRunDataverseWriter(IOptions configuration, ILogger logger) + public DryRunDataverseWriter(IOptions configuration, ILogger logger) { if (!configuration.Value.DryRun) { diff --git a/Dataverse/Extensions/ServiceCollectionExtensions.cs b/Dataverse/Extensions/ServiceCollectionExtensions.cs index 0f4220f..8f4eef7 100644 --- a/Dataverse/Extensions/ServiceCollectionExtensions.cs +++ b/Dataverse/Extensions/ServiceCollectionExtensions.cs @@ -14,7 +14,7 @@ public static IServiceCollection AddDataverseConnection(this IServiceCollection services.AddSingleton(); services.AddSingleton((sp) => { - var options = sp.GetRequiredService>(); + var options = sp.GetRequiredService>(); return options.Value.DryRun ? ActivatorUtilities.CreateInstance(sp) diff --git a/Dataverse/PluginAssemblyWriter.cs b/Dataverse/PluginAssemblyWriter.cs index c2b768d..25e6ac4 100644 --- a/Dataverse/PluginAssemblyWriter.cs +++ b/Dataverse/PluginAssemblyWriter.cs @@ -6,7 +6,7 @@ namespace XrmSync.Dataverse; -internal class PluginAssemblyWriter(IDataverseWriter writer, IOptions configuration) : IPluginAssemblyWriter +internal class PluginAssemblyWriter(IDataverseWriter writer, IOptions configuration) : IPluginAssemblyWriter { private Dictionary Parameters { get; } = new() { { "SolutionUniqueName", configuration.Value.SolutionName } diff --git a/Dataverse/PluginWriter.cs b/Dataverse/PluginWriter.cs index e0f6e6d..6beb5b3 100644 --- a/Dataverse/PluginWriter.cs +++ b/Dataverse/PluginWriter.cs @@ -10,7 +10,7 @@ namespace XrmSync.Dataverse; -internal class PluginWriter(IMessageReader messageReader, IDataverseWriter writer, IOptions configuration) : IPluginWriter +internal class PluginWriter(IMessageReader messageReader, IDataverseWriter writer, IOptions configuration) : IPluginWriter { private Dictionary Parameters { get; } = new() { { "SolutionUniqueName", configuration.Value.SolutionName } diff --git a/Dataverse/WebresourceWriter.cs b/Dataverse/WebresourceWriter.cs index bd74873..fd29b08 100644 --- a/Dataverse/WebresourceWriter.cs +++ b/Dataverse/WebresourceWriter.cs @@ -8,7 +8,7 @@ namespace XrmSync.Dataverse; -internal class WebresourceWriter(IDataverseWriter writer, IOptions configuration) : IWebresourceWriter +internal class WebresourceWriter(IDataverseWriter writer, IOptions configuration) : IWebresourceWriter { private Dictionary Parameters { get; } = new() { { "SolutionUniqueName", configuration.Value.SolutionName } diff --git a/Model/XrmSyncOptions.cs b/Model/XrmSyncOptions.cs index 038e770..d81c5e1 100644 --- a/Model/XrmSyncOptions.cs +++ b/Model/XrmSyncOptions.cs @@ -1,50 +1,77 @@ using Microsoft.Extensions.Logging; using System.Runtime.CompilerServices; +using System.Text.Json.Serialization; [assembly: InternalsVisibleTo("Tests")] [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")] namespace XrmSync.Model; -public record XrmSyncConfiguration(PluginOptions Plugin, WebresourceOptions Webresource, LoggerOptions Logger, ExecutionOptions Execution) +public record XrmSyncConfiguration(bool DryRun, LogLevel LogLevel, bool CiMode, List Profiles) { - public static XrmSyncConfiguration Empty => new (PluginOptions.Empty, WebresourceOptions.Empty, LoggerOptions.Empty, ExecutionOptions.Empty); + public static XrmSyncConfiguration Empty => new (false, LogLevel.Information, false, new List()); } -public record PluginOptions(PluginSyncOptions Sync, PluginAnalysisOptions Analysis) + +public record ProfileConfiguration(string Name, string SolutionName, List Sync) { - public static PluginOptions Empty => new(PluginSyncOptions.Empty, PluginAnalysisOptions.Empty); + public static ProfileConfiguration Empty => new (string.Empty, string.Empty, new List()); } -public record WebresourceOptions(WebresourceSyncOptions Sync) +[JsonPolymorphic(TypeDiscriminatorPropertyName = "Type")] +[JsonDerivedType(typeof(PluginSyncItem), typeDiscriminator: "Plugin")] +[JsonDerivedType(typeof(PluginAnalysisSyncItem), typeDiscriminator: "PluginAnalysis")] +[JsonDerivedType(typeof(WebresourceSyncItem), typeDiscriminator: "Webresource")] +public abstract record SyncItem { - public static WebresourceOptions Empty => new (WebresourceSyncOptions.Empty); + [JsonIgnore] + public abstract string SyncType { get; } } -public record PluginSyncOptions(string AssemblyPath, string SolutionName) +public record PluginSyncItem(string AssemblyPath) : SyncItem { - public static PluginSyncOptions Empty => new (string.Empty, string.Empty); + public static PluginSyncItem Empty => new (string.Empty); + + [JsonIgnore] + public override string SyncType => "Plugin"; } -public record PluginAnalysisOptions(string AssemblyPath, string PublisherPrefix, bool PrettyPrint) +public record PluginAnalysisSyncItem(string AssemblyPath, string PublisherPrefix, bool PrettyPrint) : SyncItem { - public static PluginAnalysisOptions Empty => new (string.Empty, "new", false); + public static PluginAnalysisSyncItem Empty => new (string.Empty, "new", false); + + [JsonIgnore] + public override string SyncType => "PluginAnalysis"; } -public record WebresourceSyncOptions(string FolderPath, string SolutionName) +public record WebresourceSyncItem(string FolderPath) : SyncItem { - public static WebresourceSyncOptions Empty => new (string.Empty, string.Empty); + public static WebresourceSyncItem Empty => new (string.Empty); + + [JsonIgnore] + public override string SyncType => "Webresource"; } -public record LoggerOptions(LogLevel LogLevel, bool CiMode) +public record SharedOptions(string? ProfileName) { - public static LoggerOptions Empty => new (LogLevel.Information, false); + public static SharedOptions Empty => new((string?)null); } -public record ExecutionOptions(bool DryRun) +// Command-specific options that can be populated from CLI or profile +public record PluginSyncCommandOptions(string AssemblyPath, string SolutionName) { - public static ExecutionOptions Empty => new (false); + public static PluginSyncCommandOptions Empty => new(string.Empty, string.Empty); } -public record SharedOptions(bool SaveConfig, string? SaveConfigTo, string ConfigName) +public record PluginAnalysisCommandOptions(string AssemblyPath, string PublisherPrefix, bool PrettyPrint) { - public static SharedOptions Empty => new(false, null, string.Empty); -} \ No newline at end of file + public static PluginAnalysisCommandOptions Empty => new(string.Empty, "new", false); +} + +public record WebresourceSyncCommandOptions(string FolderPath, string SolutionName) +{ + public static WebresourceSyncCommandOptions Empty => new(string.Empty, string.Empty); +} + +public record ExecutionModeOptions(bool DryRun) +{ + public static ExecutionModeOptions Empty => new(false); +} diff --git a/README.md b/README.md index 76bcef8..462ef9a 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,11 @@ xrmsync webresources --folder "path/to/webresources" --solution-name "YourSoluti For repeated operations or complex configurations, you can read the configuration from the appsettings.json file: ```bash # Run all configured commands (plugins, webresources, analysis) -xrmsync --config default +# If only one profile exists, it's used automatically without --profile +xrmsync --profile myprofile # Run a specific command with configuration -xrmsync plugins --config default +xrmsync plugins --profile myprofile ``` You can also override specific options when using a configuration file: @@ -98,9 +99,7 @@ xrmsync plugins --dry-run --log-level Debug | `--dry-run` | | Perform a dry run without making changes | No | | `--log-level` | `-l` | Set the minimum log level (Trace, Debug, Information, Warning, Error, Critical) | No | | `--ci-mode` | `--ci` | Enable CI mode which prefixes all warnings and errors | No | -| `--config` | `-c` | Name of the configuration to load from appsettings.json | No | -| `--save-config` | `--sc` | Save current CLI options to appsettings.json | No | -| `--save-config-to` | | If `--save-config` is specified, override the filename to save to | No | +| `--profile` | `-p`, `--profile-name` | Name of the profile to load from appsettings.json | No | *Required when not present in appsettings.json @@ -113,9 +112,7 @@ xrmsync plugins --dry-run --log-level Debug | `--dry-run` | | Perform a dry run without making changes | No | | `--log-level` | `-l` | Set the minimum log level (Trace, Debug, Information, Warning, Error, Critical) | No | | `--ci-mode` | `--ci` | Enable CI mode which prefixes all warnings and errors | No | -| `--config` | `-c` | Name of the configuration to load from appsettings.json | No | -| `--save-config` | `--sc` | Save current CLI options to appsettings.json | No | -| `--save-config-to` | | If `--save-config` is specified, override the filename to save to | No | +| `--profile` | `-p`, `--profile-name` | Name of the profile to load from appsettings.json | No | *Required when not present in appsettings.json @@ -139,8 +136,6 @@ The webresource name in Dataverse is determined by the file path relative to the | `--assembly` | `-a` | Path to the plugin assembly (*.dll) | Yes* | | `--prefix` | `-p` | Publisher prefix for unique names | No (Default: "new") | | `--pretty-print` | `--pp` | Pretty print the JSON output | No | -| `--save-config` | `--sc` | Save current CLI options to appsettings.json | No | -| `--save-config-to` | | If `--save-config` is specified, override the filename to save to | No | *Required when not present in appsettings.json @@ -150,13 +145,13 @@ The webresource name in Dataverse is determined by the file path relative to the | Option | Short | Description | Required | |--------|-------|-------------|----------| -| `--config` | `-c` | Name of the configuration to validate | No (Default: "default") | +| `--profile` | `-p`, `--profile-name` | Name of the profile to validate | No (auto-selects if only one profile exists) | **Config List Command** | Option | Short | Description | Required | |--------|-------|-------------|----------| -| No options | | Lists all configurations from appsettings.json | N/A | +| No options | | Lists all profiles from appsettings.json | N/A | ### Assembly Analysis @@ -165,10 +160,6 @@ You can analyze an assembly without connecting to Dataverse: xrmsync analyze --assembly "path/to/your/plugin.dll" --pretty-print ``` -You can also save analysis configurations: -```bash -xrmsync analyze --assembly "path/to/your/plugin.dll" --prefix "contoso" --pretty-print --save-config -``` This outputs JSON information about the plugin types, steps, and images found in the assembly. ### Configuration Validation @@ -180,7 +171,7 @@ You can validate your configuration files to ensure they are correctly set up: xrmsync config validate # Validate a specific named configuration -xrmsync config validate --config dev +xrmsync config validate --profile dev ``` The `config validate` command shows: @@ -238,64 +229,38 @@ Available configurations (from appsettings.json): XrmSync supports JSON configuration files that contain all the necessary settings for synchronization and analysis. This is particularly useful for CI/CD pipelines or when you have consistent settings across multiple runs. -The configuration uses a hierarchical structure under the XrmSync section, separating sync and analysis options under Plugin.Sync and Plugin.Analysis respectively. - -#### Generating Configuration Files - -You can automatically generate configuration files using the `--save-config` option with any command: - -##### Save sync options to appsettings.json (default) -```bash -xrmsync plugins --assembly "MyPlugin.dll" --solution-name "MyCustomSolution" --save-config -``` - -##### Save analysis options to appsettings.json -```bash -xrmsync analyze --assembly "MyPlugin.dll" --prefix "contoso" --pretty-print --save-config -``` - -##### Save to a custom file -```bash -xrmsync plugins --assembly "MyPlugin.dll" --solution-name "MyCustomSolution" --save-config --save-config-to "my-project.json" -``` - -When using `--save-config`, XrmSync will: -1. Take all the provided CLI options -2. Create or update the target configuration file -3. Merge with existing content if the file already exists -4. Save the configuration in the proper JSON format +The configuration uses a profile-based structure under the XrmSync section, with global settings (DryRun, LogLevel, CiMode) and an array of named profiles. Each profile contains a solution name and a list of sync items (Plugin, Webresource, PluginAnalysis). #### JSON Schema ```json { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "path/to/your/plugin.dll", - "SolutionName": "YourSolutionName" - }, - "Analysis": { - "AssemblyPath": "path/to/your/plugin.dll", - "PublisherPrefix": "contoso", - "PrettyPrint": true - } - }, - "Webresource": { - "Sync": { - "FolderPath": "path/to/webresources", - "SolutionName": "YourSolutionName" - } - }, - "Logger": { - "LogLevel": "Information", - "CiMode": false - }, - "Execution": { - "DryRun": false + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "default", + "SolutionName": "YourSolutionName", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "path/to/your/plugin.dll" + }, + { + "Type": "Webresource", + "FolderPath": "path/to/webresources" + }, + { + "Type": "PluginAnalysis", + "AssemblyPath": "path/to/your/plugin.dll", + "PublisherPrefix": "contoso", + "PrettyPrint": true + } + ] } - } + ] } } ``` @@ -306,42 +271,42 @@ XrmSync supports multiple named configurations within a single appsettings.json **Using named configurations:** ```bash -# Use the 'default' configuration (or the only configuration if only one exists) -xrmsync --config default - -# Use a specific named configuration -xrmsync --config dev - -# If --config is not specified, 'default' is used, or the single config if only one exists +# If only one profile exists, it's used automatically xrmsync + +# If multiple profiles exist, you must specify which one to use +xrmsync --profile ``` **Example with multiple named configurations:** ```json { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Debug/net462/MyPlugin.dll", - "SolutionName": "DevSolution" - } - }, - "Execution": { - "DryRun": true - } - }, - "prod": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "SolutionName": "ProdSolution" - } + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "default", + "SolutionName": "DevSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "bin/Debug/net462/MyPlugin.dll" + } + ] }, - "Execution": { - "DryRun": false + { + "Name": "prod", + "SolutionName": "ProdSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "bin/Release/net462/MyPlugin.dll" + } + ] } - } + ] } } ``` @@ -353,7 +318,7 @@ When you call the root command with a configuration name, XrmSync will automatic ```bash # This will run plugin sync, plugin analysis, and webresource sync # for all that are configured in the 'default' configuration -xrmsync --config default +xrmsync --profile default ``` XrmSync will only execute sub-commands that have their required properties configured. For example: @@ -361,40 +326,48 @@ XrmSync will only execute sub-commands that have their required properties confi - Plugin analysis runs only if `AssemblyPath` is provided - Webresource sync runs only if `FolderPath` and `SolutionName` are provided -#### Plugin Sync Properties +#### Global Properties | Property | Type | Description | Default | |----------|------|-------------|---------| -| `AssemblyPath` | string | Path to the plugin assembly (*.dll) | Required | -| `SolutionName` | string | Name of the target Dataverse solution | Required | +| `DryRun` | boolean | Perform a dry run without making changes | false | +| `LogLevel` | string | Log level (Trace, Debug, Information, Warning, Error, Critical) | "Information" | +| `CiMode` | boolean | Enable CI mode for easier parsing in CI systems | false | -#### Plugin Analysis Properties +#### Profile Properties | Property | Type | Description | Default | |----------|------|-------------|---------| -| `AssemblyPath` | string | Path to the plugin assembly (*.dll) | Required | -| `PublisherPrefix` | string | Publisher prefix for unique names | "new" | -| `PrettyPrint` | boolean | Pretty print the JSON output | false | +| `Name` | string | Name of the profile | Required | +| `SolutionName` | string | Name of the target Dataverse solution | Required | +| `Sync` | array | List of sync items (see below) | Required | + +#### Sync Item Properties + +Each sync item must have a `Type` property indicating the sync type: -#### Webresource Sync Properties +**Plugin Sync Item (Type: "Plugin")** | Property | Type | Description | Default | |----------|------|-------------|---------| -| `FolderPath` | string | Path to the root folder containing webresources | Required | -| `SolutionName` | string | Name of the target Dataverse solution | Required | +| `Type` | string | Must be "Plugin" | Required | +| `AssemblyPath` | string | Path to the plugin assembly (*.dll) | Required | -#### Logger Properties +**Webresource Sync Item (Type: "Webresource")** | Property | Type | Description | Default | |----------|------|-------------|---------| -| `LogLevel` | string | Log level (Trace, Debug, Information, Warning, Error, Critical) | "Information" | -| `CiMode` | boolean | Enable CI mode for easier parsing in CI systems | false | +| `Type` | string | Must be "Webresource" | Required | +| `FolderPath` | string | Path to the root folder containing webresources | Required | -#### Execution Properties +**Plugin Analysis Item (Type: "PluginAnalysis")** | Property | Type | Description | Default | |----------|------|-------------|---------| -| `DryRun` | boolean | Perform a dry run without making changes | false | +| `Type` | string | Must be "PluginAnalysis" | Required | +| `AssemblyPath` | string | Path to the plugin assembly (*.dll) | Required | +| `PublisherPrefix` | string | Publisher prefix for unique names | "new" | +| `PrettyPrint` | boolean | Pretty print the JSON output | false | #### Example Configuration Files @@ -402,14 +375,18 @@ XrmSync will only execute sub-commands that have their required properties confi ```json { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "MyPlugin.dll", - "SolutionName": "MyCustomSolution" - } + "Profiles": [ + { + "Name": "default", + "SolutionName": "MyCustomSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "MyPlugin.dll" + } + ] } - } + ] } } ``` @@ -419,31 +396,31 @@ XrmSync will only execute sub-commands that have their required properties confi ```json { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "SolutionName": "MyCustomSolution" - }, - "Analysis": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "PublisherPrefix": "contoso", - "PrettyPrint": true - } - }, - "Webresource": { - "Sync": { - "FolderPath": "wwwroot", - "SolutionName": "MyCustomSolution" - } - }, - "Logger": { - "LogLevel": "Debug" - }, - "Execution": { - "DryRun": true + "DryRun": true, + "LogLevel": "Debug", + "CiMode": false, + "Profiles": [ + { + "Name": "default", + "SolutionName": "MyCustomSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "bin/Release/net462/MyPlugin.dll" + }, + { + "Type": "Webresource", + "FolderPath": "wwwroot" + }, + { + "Type": "PluginAnalysis", + "AssemblyPath": "bin/Release/net462/MyPlugin.dll", + "PublisherPrefix": "contoso", + "PrettyPrint": true + } + ] } - } + ] } } ``` @@ -452,17 +429,19 @@ XrmSync will only execute sub-commands that have their required properties confi ```json { "XrmSync": { - "default": { - "Webresource": { - "Sync": { - "FolderPath": "src/webresources", - "SolutionName": "MyCustomSolution" - } - }, - "Execution": { - "DryRun": false + "DryRun": false, + "Profiles": [ + { + "Name": "default", + "SolutionName": "MyCustomSolution", + "Sync": [ + { + "Type": "Webresource", + "FolderPath": "src/webresources" + } + ] } - } + ] } } ``` @@ -471,15 +450,20 @@ XrmSync will only execute sub-commands that have their required properties confi ```json { "XrmSync": { - "default": { - "Plugin": { - "Analysis": { - "AssemblyPath": "../../../bin/Debug/net462/ILMerged.SamplePlugins.dll", - "PublisherPrefix": "contoso", - "PrettyPrint": false - } + "Profiles": [ + { + "Name": "default", + "SolutionName": "MySolution", + "Sync": [ + { + "Type": "PluginAnalysis", + "AssemblyPath": "../../../bin/Debug/net462/ILMerged.SamplePlugins.dll", + "PublisherPrefix": "contoso", + "PrettyPrint": false + } + ] } - } + ] } } ``` @@ -504,22 +488,22 @@ xrmsync webresources --folder "wwwroot" --solution-name "MyCustomSolution" --dry #### Using a configuration file to run all configured commands: ```bash # Runs all configured sub-commands (plugin sync, analysis, webresource sync) -# from the 'default' configuration -xrmsync --config default +# from the specified profile +xrmsync --profile myprofile -# Or simply (uses 'default' if it exists, or the only config if there's just one) +# If only one profile exists, it's used automatically xrmsync ``` #### Using a specific named configuration: ```bash -xrmsync --config prod +xrmsync --profile prod ``` #### Running a specific sub-command with configuration: ```bash # Uses the configuration but only runs the plugins command -xrmsync plugins --config dev +xrmsync plugins --profile dev ``` #### Configuration file with CLI overrides: @@ -671,7 +655,7 @@ The solution consists of several projects: ### Prerequisites -- .NET 8 SDK +- .NET 10 SDK - Visual Studio 2022 or VS Code - Access to a Dataverse environment for testing diff --git a/SyncService/Difference/PrintService.cs b/SyncService/Difference/PrintService.cs index f4c2efa..a68410d 100644 --- a/SyncService/Difference/PrintService.cs +++ b/SyncService/Difference/PrintService.cs @@ -9,7 +9,7 @@ namespace XrmSync.SyncService.Difference; internal class PrintService( ILogger log, - IOptions configuration, + IOptions configuration, IDescription description, IDataverseReader dataverseReader ) : IPrintService diff --git a/SyncService/PluginSyncService.cs b/SyncService/PluginSyncService.cs index 09e50e5..3842cc1 100644 --- a/SyncService/PluginSyncService.cs +++ b/SyncService/PluginSyncService.cs @@ -30,11 +30,11 @@ internal class PluginSyncService( IDifferenceCalculator differenceUtility, IDescription description, IPrintService printService, - IOptions configuration, ILogger log) : ISyncService + IOptions configuration, ILogger log) : ISyncService { private record SyncData(AssemblyInfo LocalAssembly, AssemblyInfo? CrmAssembly); - private readonly PluginSyncOptions options = configuration.Value; + private readonly PluginSyncCommandOptions options = configuration.Value; public async Task Sync(CancellationToken cancellationToken) { diff --git a/SyncService/WebresourceSyncService.cs b/SyncService/WebresourceSyncService.cs index a634489..807d3af 100644 --- a/SyncService/WebresourceSyncService.cs +++ b/SyncService/WebresourceSyncService.cs @@ -12,7 +12,7 @@ namespace XrmSync.SyncService; internal class WebresourceSyncService( - IOptions config, + IOptions config, ILogger log, ILocalReader localReader, ISolutionReader solutionReader, @@ -22,7 +22,7 @@ internal class WebresourceSyncService( IPrintService printService ) : ISyncService { - private readonly WebresourceSyncOptions options = config.Value; + private readonly WebresourceSyncCommandOptions options = config.Value; public Task Sync(CancellationToken cancellation) { diff --git a/Tests/Config/ConfigWriterTests.cs b/Tests/Config/ConfigWriterTests.cs deleted file mode 100644 index dc261ff..0000000 --- a/Tests/Config/ConfigWriterTests.cs +++ /dev/null @@ -1,310 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using NSubstitute; -using System.Text.Json; -using XrmSync.Model; -using XrmSync.Options; - -namespace Tests.Config; - -public class ConfigWriterTests -{ - private readonly ILogger _logger; - - public ConfigWriterTests() - { - _logger = Substitute.For>(); - } - - [Fact] - public async Task SaveConfig_CreatesNewFile_UsesDefaultConfigName() - { - // Arrange - var tempFile = Path.GetTempFileName(); - File.Delete(tempFile); // Ensure file doesn't exist - - XrmSyncConfiguration config = new ( - new ( - new ("test.dll", "TestSolution"), - new ("analysis.dll", "tst", true) - ), - new ( - new ("wwwroot", "WebSolution") - ), - new (LogLevel.Debug, false), - new (true) - ); - - var options = Options.Create(config); - var configWriter = new ConfigWriter(options, _logger); - - try - { - // Act - await configWriter.SaveConfig(tempFile); - - // Assert - Assert.True(File.Exists(tempFile)); - - var content = await File.ReadAllTextAsync(tempFile); - var json = JsonSerializer.Deserialize(content); - - var xrmSyncSection = json.GetProperty("XrmSync"); - var defaultConfigSection = xrmSyncSection.GetProperty("default"); - - var pluginSection = defaultConfigSection.GetProperty("Plugin"); - var syncSection = pluginSection.GetProperty("Sync"); - Assert.Equal("test.dll", syncSection.GetProperty("AssemblyPath").GetString()); - Assert.Equal("TestSolution", syncSection.GetProperty("SolutionName").GetString()); - - var analysisSection = pluginSection.GetProperty("Analysis"); - Assert.Equal("analysis.dll", analysisSection.GetProperty("AssemblyPath").GetString()); - Assert.Equal("tst", analysisSection.GetProperty("PublisherPrefix").GetString()); - Assert.True(analysisSection.GetProperty("PrettyPrint").GetBoolean()); - - var webSection = defaultConfigSection.GetProperty("Webresource"); - var webSyncSection = webSection.GetProperty("Sync"); - Assert.Equal("wwwroot", webSyncSection.GetProperty("FolderPath").GetString()); - Assert.Equal("WebSolution", webSyncSection.GetProperty("SolutionName").GetString()); - - var loggerSection = defaultConfigSection.GetProperty("Logger"); - Assert.Equal("Debug", loggerSection.GetProperty("LogLevel").GetString()); - Assert.False(loggerSection.GetProperty("CiMode").GetBoolean()); - - var executionSection = defaultConfigSection.GetProperty("Execution"); - Assert.True(executionSection.GetProperty("DryRun").GetBoolean()); - } - finally - { - if (File.Exists(tempFile)) - File.Delete(tempFile); - } - } - - [Fact] - public async Task SaveConfig_CreatesNewFile_WithNamedStructure() - { - // Arrange - var tempFile = Path.GetTempFileName(); - File.Delete(tempFile); // Ensure file doesn't exist - - XrmSyncConfiguration config = new ( - new PluginOptions( - new ("dev.dll", "DevSolution"), - new ("dev-analysis.dll", "dev", false) - ), - new WebresourceOptions( - new ("dev-wwwroot", "DevWebSolution") - ), - new (LogLevel.Debug, false), - new (true) - ); - - var options = Options.Create(config); - var configWriter = new ConfigWriter(options, _logger); - - try - { - // Act - await configWriter.SaveConfig(tempFile, configName: "dev"); - - // Assert - Assert.True(File.Exists(tempFile)); - - var content = await File.ReadAllTextAsync(tempFile); - var json = JsonSerializer.Deserialize(content); - - var xrmSyncSection = json.GetProperty("XrmSync"); - var devSection = xrmSyncSection.GetProperty("dev"); - var pluginSection = devSection.GetProperty("Plugin"); - var syncSection = pluginSection.GetProperty("Sync"); - Assert.Equal("dev.dll", syncSection.GetProperty("AssemblyPath").GetString()); - Assert.Equal("DevSolution", syncSection.GetProperty("SolutionName").GetString()); - } - finally - { - if (File.Exists(tempFile)) - File.Delete(tempFile); - } - } - - [Fact] - public async Task SaveConfig_MergesWithExistingFile_PreservingOtherSections() - { - // Arrange - var tempFile = Path.GetTempFileName(); - var existingContent = """ - { - "SomeOtherSection": { - "SomeProperty": "SomeValue" - } - } - """; - await File.WriteAllTextAsync(tempFile, existingContent); - - XrmSyncConfiguration config = new ( - new ( - new ("test.dll", "TestSolution"), - new ("analysis.dll", "new", false) - ), - new ( - new ("wwwroot", "WebSolution") - ), - new (LogLevel.Debug, false), - new (true) - ); - - var options = Options.Create(config); - var configWriter = new ConfigWriter(options, _logger); - - try - { - // Act - await configWriter.SaveConfig(tempFile); - - // Assert - var content = await File.ReadAllTextAsync(tempFile); - var json = JsonSerializer.Deserialize(content); - - // Check original section is preserved - var otherSection = json.GetProperty("SomeOtherSection"); - Assert.Equal("SomeValue", otherSection.GetProperty("SomeProperty").GetString()); - - // Check XrmSync section is added - var xrmSyncSection = json.GetProperty("XrmSync"); - var defaultSection = xrmSyncSection.GetProperty("default"); - var pluginSection = defaultSection.GetProperty("Plugin"); - var syncSection = pluginSection.GetProperty("Sync"); - Assert.Equal("test.dll", syncSection.GetProperty("AssemblyPath").GetString()); - } - finally - { - if (File.Exists(tempFile)) - File.Delete(tempFile); - } - } - - [Fact] - public async Task SaveConfig_UpdatesExistingNamedConfig_WithoutAffectingOtherConfigs() - { - // Arrange - var tempFile = Path.GetTempFileName(); - var existingContent = """ - { - "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "default.dll", - "SolutionName": "DefaultSolution", - "LogLevel": "Information" - } - }, - "Execution": { - "DryRun": false - } - } - } - } - """; - await File.WriteAllTextAsync(tempFile, existingContent); - - XrmSyncConfiguration config = new ( - new ( - new ("dev.dll", "DevSolution"), - new ("dev-analysis.dll", "dev", true) - ), - new ( - new ("dev-wwwroot", "DevWebSolution") - ), - new (LogLevel.Debug, false), - new (true) - ); - - var options = Options.Create(config); - var configWriter = new ConfigWriter(options, _logger); - - try - { - // Act - await configWriter.SaveConfig(tempFile, configName: "dev"); - - // Assert - var content = await File.ReadAllTextAsync(tempFile); - var json = JsonSerializer.Deserialize(content); - - var xrmSyncSection = json.GetProperty("XrmSync"); - - // Check default config is preserved - var defaultSection = xrmSyncSection.GetProperty("default"); - var defaultSync = defaultSection.GetProperty("Plugin").GetProperty("Sync"); - Assert.Equal("default.dll", defaultSync.GetProperty("AssemblyPath").GetString()); - var defaultExecution = defaultSection.GetProperty("Execution"); - Assert.False(defaultExecution.GetProperty("DryRun").GetBoolean()); - - // Check dev config is added - var devSection = xrmSyncSection.GetProperty("dev"); - var devSync = devSection.GetProperty("Plugin").GetProperty("Sync"); - Assert.Equal("dev.dll", devSync.GetProperty("AssemblyPath").GetString()); - Assert.Equal("DevSolution", devSync.GetProperty("SolutionName").GetString()); - var devExecution = devSection.GetProperty("Execution"); - Assert.True(devExecution.GetProperty("DryRun").GetBoolean()); - } - finally - { - if (File.Exists(tempFile)) - File.Delete(tempFile); - } - } - - [Fact] - public async Task SaveConfig_UsesDefaultFileName_WhenFilePathIsNull() - { - // Arrange - var currentDir = Directory.GetCurrentDirectory(); - var defaultFile = Path.Combine(currentDir, "appsettings.json"); - - // Clean up if file exists - if (File.Exists(defaultFile)) - File.Delete(defaultFile); - - XrmSyncConfiguration config = new ( - new ( - new ("test.dll", "TestSolution"), - new ("analysis.dll", "new", false) - ), - new ( - new ("wwwroot", "WebSolution") - ), - new (LogLevel.Debug, false), - new ExecutionOptions(false) - ); - - var options = Options.Create(config); - var configWriter = new ConfigWriter(options, _logger); - - try - { - // Act - await configWriter.SaveConfig(); - - // Assert - Assert.True(File.Exists(defaultFile)); - - var content = await File.ReadAllTextAsync(defaultFile); - var json = JsonSerializer.Deserialize(content); - - var xrmSyncSection = json.GetProperty("XrmSync"); - var defaultSection = xrmSyncSection.GetProperty("default"); - var pluginSection = defaultSection.GetProperty("Plugin"); - var syncSection = pluginSection.GetProperty("Sync"); - Assert.Equal("test.dll", syncSection.GetProperty("AssemblyPath").GetString()); - Assert.Equal("TestSolution", syncSection.GetProperty("SolutionName").GetString()); - } - finally - { - if (File.Exists(defaultFile)) - File.Delete(defaultFile); - } - } -} \ No newline at end of file diff --git a/Tests/Config/NamedConfigurationTests.cs b/Tests/Config/NamedConfigurationTests.cs index 6cd10f9..edd54e7 100644 --- a/Tests/Config/NamedConfigurationTests.cs +++ b/Tests/Config/NamedConfigurationTests.cs @@ -16,37 +16,52 @@ public void ResolveConfigurationName_WithSpecificName_ReturnsRequestedName() const string configJson = """ { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "default.dll" - } + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "default", + "SolutionName": "DefaultSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "default.dll" + } + ] + }, + { + "Name": "dev", + "SolutionName": "DevSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "dev.dll" + } + ] } - }, - "dev": { - "Plugin": { - "Sync": { - "AssemblyPath": "dev.dll" - } - } - } + ] } } """; - + var tempFile = Path.GetTempFileName(); File.WriteAllText(tempFile, configJson); - + try { var configReader = new TestConfigReader(tempFile); - var builder = new XrmSyncConfigurationBuilder(configReader.GetConfiguration(), Options.Create(SharedOptions.Empty with { ConfigName = "dev" })); + var builder = new XrmSyncConfigurationBuilder(configReader.GetConfiguration(), Options.Create(SharedOptions.Empty with { ProfileName = "dev" })); // Act - var configuration = builder.Build(); - + var profile = builder.GetProfile("dev"); + // Assert - Assert.Equal("dev.dll", configuration.Plugin.Sync.AssemblyPath); + Assert.NotNull(profile); + Assert.Equal("dev", profile.Name); + Assert.Single(profile.Sync); + var pluginSync = Assert.IsType(profile.Sync[0]); + Assert.Equal("dev.dll", pluginSync.AssemblyPath); } finally { @@ -55,43 +70,53 @@ public void ResolveConfigurationName_WithSpecificName_ReturnsRequestedName() } [Fact] - public void ResolveConfigurationName_WithNoSpecificName_ReturnsDefault() + public void ResolveConfigurationName_WithMultipleProfilesAndNoSpecificName_ThrowsException() { // Arrange const string configJson = """ { "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "default.dll" - } - } - }, - "dev": { - "Plugin": { - "Sync": { - "AssemblyPath": "dev.dll" - } + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "profile1", + "SolutionName": "Solution1", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "profile1.dll" + } + ] + }, + { + "Name": "profile2", + "SolutionName": "Solution2", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "profile2.dll" + } + ] } - } + ] } } """; - + var tempFile = Path.GetTempFileName(); File.WriteAllText(tempFile, configJson); - + try { var configReader = new TestConfigReader(tempFile); var builder = new XrmSyncConfigurationBuilder(configReader.GetConfiguration(), Options.Create(SharedOptions.Empty)); - // Act - var result = builder.Build(); - - // Assert - Assert.Equal("default.dll", result.Plugin.Sync.AssemblyPath); + // Act & Assert + var exception = Assert.Throws(() => builder.GetProfile(null)); + Assert.Contains("Multiple profiles found", exception.Message); + Assert.Contains("--profile", exception.Message); } finally { @@ -106,30 +131,42 @@ public void ResolveConfigurationName_WithSingleConfig_ReturnsThatConfig() const string configJson = """ { "XrmSync": { - "myconfig": { - "Plugin": { - "Sync": { - "AssemblyPath": "myconfig.dll" - } + "DryRun": false, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "myconfig", + "SolutionName": "MySolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "myconfig.dll" + } + ] } - } + ] } } """; - + var tempFile = Path.GetTempFileName(); File.WriteAllText(tempFile, configJson); - + try { var configReader = new TestConfigReader(tempFile); var builder = new XrmSyncConfigurationBuilder(configReader.GetConfiguration(), Options.Create(SharedOptions.Empty)); // Act - var result = builder.Build(); + var profile = builder.GetProfile(null); // Assert - Assert.Equal("myconfig.dll", result.Plugin.Sync.AssemblyPath); + Assert.NotNull(profile); + Assert.Equal("myconfig", profile.Name); + Assert.Single(profile.Sync); + var pluginSync = Assert.IsType(profile.Sync[0]); + Assert.Equal("myconfig.dll", pluginSync.AssemblyPath); } finally { diff --git a/Tests/Config/OptionsValidationTests.cs b/Tests/Config/OptionsValidationTests.cs index 86d3674..508dc77 100644 --- a/Tests/Config/OptionsValidationTests.cs +++ b/Tests/Config/OptionsValidationTests.cs @@ -7,589 +7,466 @@ namespace Tests.Config; public class OptionsValidationTests { + private static SharedOptions CreateSharedOptions(string profileName = "default") => + new SharedOptions(profileName); + [Fact] - public void SyncOptionsValidator_ValidOptions_PassesValidation() + public void PluginSyncValidator_ValidOptions_PassesValidation() { - // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "test.dll", - SolutionName: "TestSolution" - ); - - // Create a test DLL file + // Arrange - Create a test DLL file var tempFile = Path.GetTempFileName(); File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); var dllPath = Path.ChangeExtension(tempFile, ".dll"); File.WriteAllText(dllPath, "test content"); - options = options with { AssemblyPath = dllPath }; - try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginSyncItem(dllPath) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); validator.Validate(ConfigurationScope.PluginSync); // Should not throw } finally { - // Cleanup if (File.Exists(dllPath)) File.Delete(dllPath); } } [Fact] - public void SyncOptionsValidator_EmptyAssemblyPath_ThrowsValidationException() + public void PluginSyncValidator_EmptyAssemblyPath_ThrowsValidationException() { // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "", - SolutionName: "TestSolution" + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginSyncItem("") + }) + } ); // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginSync)); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginSync)); Assert.Contains("Assembly path is required", exception.Message); } [Fact] - public void SyncOptionsValidator_EmptySolutionName_ThrowsValidationException() - { - // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "test.dll", - SolutionName: "" - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginSync)); - Assert.Contains("Solution name is required", exception.Message); - } - - [Fact] - public void SyncOptionsValidator_NonExistentAssemblyFile_ThrowsValidationException() + public void PluginSyncValidator_NonExistentAssemblyFile_ThrowsValidationException() { // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "nonexistent.dll", - SolutionName: "TestSolution" + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginSyncItem("nonexistent.dll") + }) + } ); // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginSync)); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginSync)); Assert.Contains("Assembly file does not exist", exception.Message); } [Fact] - public void SyncOptionsValidator_WrongFileExtension_ThrowsValidationException() + public void PluginSyncValidator_WrongFileExtension_ThrowsValidationException() { // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "testhost.exe", - SolutionName: "TestSolution" + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginSyncItem("testhost.exe") + }) + } ); // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginSync)); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginSync)); Assert.Contains("Assembly file must have a .dll extension", exception.Message); } [Fact] - public void SyncOptionsValidator_SolutionNameTooLong_ThrowsValidationException() + public void ProfileValidator_SolutionNameTooLong_ThrowsValidationException() { // Arrange - var options = new PluginSyncOptions( - AssemblyPath: "test.dll", - SolutionName: new string('a', 66) // 66 characters - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginSync)); - Assert.Contains("Solution name cannot exceed 65 characters", exception.Message); - } - - [Fact] - public void AnalysisOptionsValidator_ValidOptions_PassesValidation() - { - // Arrange - - // Create a test DLL file var tempFile = Path.GetTempFileName(); File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); var dllPath = Path.ChangeExtension(tempFile, ".dll"); File.WriteAllText(dllPath, "test content"); - var options = new PluginAnalysisOptions( - AssemblyPath: dllPath, - PublisherPrefix: "contoso", - PrettyPrint: true - ); - try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", new string('a', 66), new List + { + new PluginSyncItem(dllPath) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - validator.Validate(ConfigurationScope.PluginAnalysis); // Should not throw + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginSync)); + Assert.Contains("Solution name cannot exceed 65 characters", exception.Message); } finally { - // Cleanup if (File.Exists(dllPath)) File.Delete(dllPath); } } [Fact] - public void AnalysisOptionsValidator_EmptyAssemblyPath_ThrowsValidationException() - { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "", - PublisherPrefix: "contoso", - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Assembly path is required", exception.Message); - } - - [Fact] - public void AnalysisOptionsValidator_EmptyPublisherPrefix_ThrowsValidationException() - { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "test.dll", - PublisherPrefix: "", - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Publisher prefix is required", exception.Message); - } - - [Fact] - public void AnalysisOptionsValidator_InvalidPublisherPrefixTooShort_ThrowsValidationException() + public void PluginAnalysisValidator_ValidOptions_PassesValidation() { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "test.dll", - PublisherPrefix: "a", // Only 1 character - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Publisher prefix must be between 2 and 8 characters", exception.Message); - } - - [Fact] - public void AnalysisOptionsValidator_InvalidPublisherPrefixTooLong_ThrowsValidationException() - { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "test.dll", - PublisherPrefix: "toolongprefix", // More than 8 characters - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Publisher prefix must be between 2 and 8 characters", exception.Message); - } - - [Fact] - public void AnalysisOptionsValidator_InvalidPublisherPrefixFormat_ThrowsValidationException() - { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "test.dll", - PublisherPrefix: "Con2so", // Contains uppercase and numbers not at end - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Publisher prefix must start with a lowercase letter and contain only lowercase letters and numbers", exception.Message); - } - - [Theory] - [InlineData("contoso")] - [InlineData("ms")] - [InlineData("contoso1")] - [InlineData("abc123")] - public void AnalysisOptionsValidator_ValidPublisherPrefixes_PassValidation(string publisherPrefix) - { - // Arrange - - // Create a test DLL file + // Arrange - Create a test DLL file var tempFile = Path.GetTempFileName(); File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); var dllPath = Path.ChangeExtension(tempFile, ".dll"); File.WriteAllText(dllPath, "test content"); - var options = new PluginAnalysisOptions( - AssemblyPath: dllPath, - PublisherPrefix: publisherPrefix, - PrettyPrint: true - ); - try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginAnalysisSyncItem(dllPath, "contoso", true) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); validator.Validate(ConfigurationScope.PluginAnalysis); // Should not throw } finally { - // Cleanup if (File.Exists(dllPath)) File.Delete(dllPath); } } - [Theory] - [InlineData("Contoso")] // Starts with uppercase - [InlineData("1contoso")] // Starts with number - [InlineData("con-toso")] // Contains hyphen - [InlineData("con_toso")] // Contains underscore - [InlineData("con.toso")] // Contains dot - public void AnalysisOptionsValidator_InvalidPublisherPrefixFormats_ThrowValidationException(string publisherPrefix) - { - // Arrange - var options = new PluginAnalysisOptions( - AssemblyPath: "test.dll", - PublisherPrefix: publisherPrefix, - PrettyPrint: true - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Plugin = PluginOptions.Empty with { Analysis = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.PluginAnalysis)); - Assert.Contains("Publisher prefix must start with a lowercase letter and contain only lowercase letters and numbers", exception.Message); - } - - // Webresource validation tests start here [Fact] - public void WebresourceSyncOptionsValidator_ValidOptions_PassesValidation() + public void PluginAnalysisValidator_EmptyPublisherPrefix_ThrowsValidationException() { // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: "TestSolution" - ); + var tempFile = Path.GetTempFileName(); + File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); + var dllPath = Path.ChangeExtension(tempFile, ".dll"); + File.WriteAllText(dllPath, "test content"); try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginAnalysisSyncItem(dllPath, "", true) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - validator.Validate(ConfigurationScope.WebresourceSync); // Should not throw + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginAnalysis)); + Assert.Contains("Publisher prefix is required", exception.Message); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); + if (File.Exists(dllPath)) + File.Delete(dllPath); } } [Fact] - public void WebresourceSyncOptionsValidator_EmptyFolderPath_ThrowsValidationException() - { - // Arrange - var options = new WebresourceSyncOptions( - FolderPath: "", - SolutionName: "TestSolution" - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Webresource root path is required", exception.Message); - } - - [Fact] - public void WebresourceSyncOptionsValidator_NullFolderPath_ThrowsValidationException() + public void PluginAnalysisValidator_InvalidPublisherPrefixTooShort_ThrowsValidationException() { // Arrange - var options = new WebresourceSyncOptions( - FolderPath: null!, - SolutionName: "TestSolution" - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Webresource root path is required", exception.Message); - } - - [Fact] - public void WebresourceSyncOptionsValidator_WhitespaceFolderPath_ThrowsValidationException() - { - // Arrange - var options = new WebresourceSyncOptions( - FolderPath: " ", - SolutionName: "TestSolution" - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Webresource root path is required", exception.Message); - } - - [Fact] - public void WebresourceSyncOptionsValidator_NonExistentFolderPath_ThrowsValidationException() - { - // Arrange - var options = new WebresourceSyncOptions( - FolderPath: "C:\\NonExistentPath\\Webresources", - SolutionName: "TestSolution" - ); - - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Webresource root path does not exist", exception.Message); - } - - [Fact] - public void WebresourceSyncOptionsValidator_EmptySolutionName_ThrowsValidationException() - { - // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: "" - ); + var tempFile = Path.GetTempFileName(); + File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); + var dllPath = Path.ChangeExtension(tempFile, ".dll"); + File.WriteAllText(dllPath, "test content"); try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginAnalysisSyncItem(dllPath, "a", true) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Solution name is required", exception.Message); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginAnalysis)); + Assert.Contains("Publisher prefix must be between 2 and 8 characters", exception.Message); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); + if (File.Exists(dllPath)) + File.Delete(dllPath); } } - [Fact] - public void WebresourceSyncOptionsValidator_NullSolutionName_ThrowsValidationException() + [Theory] + [InlineData("contoso")] + [InlineData("ms")] + [InlineData("contoso1")] + [InlineData("abc123")] + public void PluginAnalysisValidator_ValidPublisherPrefixes_PassValidation(string publisherPrefix) { - // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: null! - ); + // Arrange - Create a test DLL file + var tempFile = Path.GetTempFileName(); + File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); + var dllPath = Path.ChangeExtension(tempFile, ".dll"); + File.WriteAllText(dllPath, "test content"); try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginAnalysisSyncItem(dllPath, publisherPrefix, true) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Solution name is required", exception.Message); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + validator.Validate(ConfigurationScope.PluginAnalysis); // Should not throw } finally { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); + if (File.Exists(dllPath)) + File.Delete(dllPath); } } - [Fact] - public void WebresourceSyncOptionsValidator_WhitespaceSolutionName_ThrowsValidationException() + [Theory] + [InlineData("Contoso")] // Starts with uppercase + [InlineData("1contoso")] // Starts with number + [InlineData("con-toso")] // Contains hyphen + public void PluginAnalysisValidator_InvalidPublisherPrefixFormats_ThrowValidationException(string publisherPrefix) { // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: " " - ); + var tempFile = Path.GetTempFileName(); + File.Move(tempFile, Path.ChangeExtension(tempFile, ".dll")); + var dllPath = Path.ChangeExtension(tempFile, ".dll"); + File.WriteAllText(dllPath, "test content"); try { + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new PluginAnalysisSyncItem(dllPath, publisherPrefix, true) + }) + } + ); + // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Solution name is required", exception.Message); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginAnalysis)); + Assert.Contains("Publisher prefix must start with a lowercase letter", exception.Message); } finally { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); + if (File.Exists(dllPath)) + File.Delete(dllPath); } } [Fact] - public void WebresourceSyncOptionsValidator_SolutionNameTooLong_ThrowsValidationException() + public void WebresourceValidator_ValidOptions_PassesValidation() { - // Arrange - // Create a test directory + // Arrange - Create a test directory var tempDir = Path.GetTempFileName(); File.Delete(tempDir); Directory.CreateDirectory(tempDir); - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: new string('a', 66) // 66 characters - ); - try { - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - Assert.Contains("Solution name cannot exceed 65 characters", exception.Message); - } - finally - { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } - } + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new WebresourceSyncItem(tempDir) + }) + } + ); - [Theory] - [InlineData("ValidSolution")] - [InlineData("My_Solution")] - [InlineData("Solution123")] - [InlineData("A")] - [InlineData("AB")] - public void WebresourceSyncOptionsValidator_ValidSolutionNames_PassValidation(string solutionName) - { - // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: solutionName - ); - - try - { // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); validator.Validate(ConfigurationScope.WebresourceSync); // Should not throw } finally { - // Cleanup if (Directory.Exists(tempDir)) Directory.Delete(tempDir, true); } } [Fact] - public void WebresourceSyncOptionsValidator_RelativeFolderPath_PassesValidation() + public void WebresourceValidator_EmptyFolderPath_ThrowsValidationException() { // Arrange - // Create a relative test directory - var relativeDir = "TestWebresources"; - if (Directory.Exists(relativeDir)) - Directory.Delete(relativeDir, true); - Directory.CreateDirectory(relativeDir); - - var options = new WebresourceSyncOptions( - FolderPath: relativeDir, - SolutionName: "TestSolution" + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new WebresourceSyncItem("") + }) + } ); - try - { - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - validator.Validate(ConfigurationScope.WebresourceSync); // Should not throw - } - finally - { - // Cleanup - if (Directory.Exists(relativeDir)) - Directory.Delete(relativeDir, true); - } + // Act & Assert + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.WebresourceSync)); + Assert.Contains("Webresource root path is required", exception.Message); } [Fact] - public void WebresourceSyncOptionsValidator_DryRunFlags_PassValidation() + public void WebresourceValidator_NonExistentFolderPath_ThrowsValidationException() { // Arrange - // Create a test directory - var tempDir = Path.GetTempFileName(); - File.Delete(tempDir); - Directory.CreateDirectory(tempDir); - - var options = new WebresourceSyncOptions( - FolderPath: tempDir, - SolutionName: "TestSolution" + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List + { + new("default", "TestSolution", new List + { + new WebresourceSyncItem("C:\\NonExistentPath\\Webresources") + }) + } ); - try - { - // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - validator.Validate(ConfigurationScope.WebresourceSync); // Should not throw - } - finally - { - // Cleanup - if (Directory.Exists(tempDir)) - Directory.Delete(tempDir, true); - } + // Act & Assert + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions())); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.WebresourceSync)); + Assert.Contains("Webresource root path does not exist", exception.Message); } [Fact] - public void WebresourceSyncOptionsValidator_MultipleValidationErrors_ThrowsExceptionWithAllErrors() + public void Validator_ProfileNotFound_ThrowsValidationException() { // Arrange - var options = new WebresourceSyncOptions( - FolderPath: "", // Invalid: empty - SolutionName: new string('a', 66) // Invalid: too long + var config = new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Information, + CiMode: false, + Profiles: new List() ); // Act & Assert - var validator = new XrmSyncConfigurationValidator(Options.Create(XrmSyncConfiguration.Empty with { Webresource = WebresourceOptions.Empty with { Sync = options } })); - var exception = Assert.Throws(() => validator.Validate(ConfigurationScope.WebresourceSync)); - - // Verify both validation errors are present - Assert.Contains("Webresource root path is required", exception.Message); - Assert.Contains("Solution name cannot exceed 65 characters", exception.Message); + var validator = new XrmSyncConfigurationValidator( + Options.Create(config), + Options.Create(CreateSharedOptions("nonexistent"))); + var exception = Assert.Throws( + () => validator.Validate(ConfigurationScope.PluginSync)); + Assert.Contains("Profile 'nonexistent' not found", exception.Message); } -} \ No newline at end of file +} diff --git a/Tests/Logging/CIModeDemonstrationTests.cs b/Tests/Logging/CIModeDemonstrationTests.cs index 2509242..9590e51 100644 --- a/Tests/Logging/CIModeDemonstrationTests.cs +++ b/Tests/Logging/CIModeDemonstrationTests.cs @@ -16,14 +16,16 @@ public void Demonstrate_CIModePassedToFormatter() // Arrange - Create service provider with CI mode enabled via AddLogger parameter var services = new ServiceCollection() - .AddSingleton(Options.Create(XrmSyncConfiguration.Empty with - { - Logger = new (LogLevel.Debug, true) // Explicitly enable CI mode - })) + .AddSingleton(Options.Create(new XrmSyncConfiguration( + DryRun: false, + LogLevel: LogLevel.Debug, + CiMode: true, // Explicitly enable CI mode + Profiles: new List() + ))) .AddLogger() .BuildServiceProvider(); - var configOptions = services.GetRequiredService>(); + var configOptions = services.GetRequiredService>(); // Capture what the logger would send to the formatter var capturedMessages = new List<(LogLevel Level, string Message)>(); @@ -87,4 +89,4 @@ public void AddProvider(ILoggerProvider provider) { } public ILogger CreateLogger(string categoryName) => _logger; public void Dispose() { } } -} \ No newline at end of file +} diff --git a/Tests/Logging/SyncLoggerTests.cs b/Tests/Logging/SyncLoggerTests.cs index 6bf04d1..ab907e0 100644 --- a/Tests/Logging/SyncLoggerTests.cs +++ b/Tests/Logging/SyncLoggerTests.cs @@ -37,8 +37,8 @@ public void Log_PassesThroughDirectly() { // Arrange var loggerFactory = new TestLoggerFactory(); - var loggerOptions = new LoggerOptions(LogLevel.Information, false); - var syncLogger = new SyncLogger(loggerFactory, Options.Create(loggerOptions)); + var config = new XrmSyncConfiguration(false, LogLevel.Information, false, new List()); + var syncLogger = new SyncLogger(loggerFactory, Options.Create(config)); // Act syncLogger.LogWarning("This is a warning message"); @@ -57,8 +57,8 @@ public void Log_AllLogLevels_PassThroughDirectly() { // Arrange var loggerFactory = new TestLoggerFactory(); - var loggerOptions = new LoggerOptions(LogLevel.Information, false); - var syncLogger = new SyncLogger(loggerFactory, Options.Create(loggerOptions)); + var config = new XrmSyncConfiguration(false, LogLevel.Information, false, new List()); + var syncLogger = new SyncLogger(loggerFactory, Options.Create(config)); // Act syncLogger.LogWarning("This is a warning message"); diff --git a/Tests/Plugins/DifferenceUtilityTests.cs b/Tests/Plugins/DifferenceUtilityTests.cs index 81f18a7..570c318 100644 --- a/Tests/Plugins/DifferenceUtilityTests.cs +++ b/Tests/Plugins/DifferenceUtilityTests.cs @@ -21,7 +21,7 @@ public DifferenceUtilityTests() { var logger = new LoggerFactory().CreateLogger(); var description = new Description(); - var options = new ExecutionOptions(true); + var options = new ExecutionModeOptions(true); _differenceUtility = new DifferenceCalculator( new PluginDefinitionComparer(), new PluginStepComparer(), diff --git a/Tests/Plugins/PluginServiceTests.cs b/Tests/Plugins/PluginServiceTests.cs index db96b12..0995e74 100644 --- a/Tests/Plugins/PluginServiceTests.cs +++ b/Tests/Plugins/PluginServiceTests.cs @@ -29,7 +29,7 @@ public class PluginServiceTests private readonly IDifferenceCalculator _differenceUtility = Substitute.For(); private readonly IDescription _description = new Description(); private readonly IPrintService _printService = Substitute.For(); - private readonly PluginSyncOptions _options = new(string.Empty, "solution"); + private readonly PluginSyncCommandOptions _options = new(string.Empty, "solution"); private readonly PluginSyncService _plugin; diff --git a/Tests/Webresources/WebresourceSyncServiceTests.cs b/Tests/Webresources/WebresourceSyncServiceTests.cs index 8bde6a6..b25b391 100644 --- a/Tests/Webresources/WebresourceSyncServiceTests.cs +++ b/Tests/Webresources/WebresourceSyncServiceTests.cs @@ -21,7 +21,7 @@ public class WebresourceSyncServiceTests private readonly IWebresourceWriter _webresourceWriter = Substitute.For(); private readonly IPrintService _printService = Substitute.For(); private readonly IValidator _webresourceValidator = Substitute.For>(); - private readonly WebresourceSyncOptions _options = new("C:\\WebResources", "TestSolution"); + private readonly WebresourceSyncCommandOptions _options = new("C:\\WebResources", "TestSolution"); private readonly WebresourceSyncService _service; diff --git a/Tests/Webresources/WebresourceWriterTests.cs b/Tests/Webresources/WebresourceWriterTests.cs index ead2ac2..eacecbf 100644 --- a/Tests/Webresources/WebresourceWriterTests.cs +++ b/Tests/Webresources/WebresourceWriterTests.cs @@ -11,7 +11,7 @@ namespace Tests.Webresources; public class WebresourceWriterTests { private readonly IDataverseWriter _dataverseWriter = Substitute.For(); - private readonly WebresourceSyncOptions _options = new("C:\\WebResources", "TestSolution"); + private readonly WebresourceSyncCommandOptions _options = new("C:\\WebResources", "TestSolution"); private readonly WebresourceWriter _writer; public WebresourceWriterTests() diff --git a/XrmSync.slnx b/XrmSync.slnx index 16a6cd6..bd8e50b 100644 --- a/XrmSync.slnx +++ b/XrmSync.slnx @@ -10,6 +10,7 @@ + diff --git a/XrmSync/Commands/PluginAnalyzeCommand.cs b/XrmSync/Commands/PluginAnalyzeCommand.cs index 92c3b42..85d12b7 100644 --- a/XrmSync/Commands/PluginAnalyzeCommand.cs +++ b/XrmSync/Commands/PluginAnalyzeCommand.cs @@ -24,13 +24,13 @@ public PluginAnalyzeCommand() : base("analyze", "Analyze a plugin assembly and o _assemblyFile = new(CliOptions.Assembly.Primary, CliOptions.Assembly.Aliases) { Description = CliOptions.Assembly.Description, - Arity = ArgumentArity.ExactlyOne + Arity = ArgumentArity.ZeroOrOne }; _prefix = new(CliOptions.Analysis.Prefix.Primary, CliOptions.Analysis.Prefix.Aliases) { Description = CliOptions.Analysis.Prefix.Description, - Arity = ArgumentArity.ExactlyOne + Arity = ArgumentArity.ZeroOrOne }; _prettyPrint = new(CliOptions.Analysis.PrettyPrint.Primary, CliOptions.Analysis.PrettyPrint.Aliases) @@ -57,20 +57,55 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken // Build service provider var serviceProvider = GetAnalyzerServices() .AddXrmSyncConfiguration(sharedOptions) - .AddOptions( - baseOptions => baseOptions with + .AddOptions(baseOptions => baseOptions) + .AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + + // Determine assembly path, publisher prefix, and pretty print + string finalAssemblyPath; + string finalPublisherPrefix; + bool finalPrettyPrint; + + // If CLI options provided, use them (standalone mode) + if (!string.IsNullOrWhiteSpace(assemblyPath) && !string.IsNullOrWhiteSpace(publisherPrefix)) { - Plugin = baseOptions.Plugin with + finalAssemblyPath = assemblyPath; + finalPublisherPrefix = publisherPrefix; + finalPrettyPrint = prettyPrint; + } + // Otherwise try to get from profile + else + { + var profile = config.Profiles.FirstOrDefault(p => + p.Name.Equals(sharedOptions.ProfileName, StringComparison.OrdinalIgnoreCase)); + + if (profile == null) { - Analysis = new( - string.IsNullOrWhiteSpace(assemblyPath) ? baseOptions.Plugin.Analysis.AssemblyPath : assemblyPath, - string.IsNullOrWhiteSpace(publisherPrefix) ? baseOptions.Plugin.Analysis.PublisherPrefix : publisherPrefix, - prettyPrint || baseOptions.Plugin.Analysis.PrettyPrint - ) + throw new InvalidOperationException( + $"Profile '{sharedOptions.ProfileName}' not found. " + + "Either specify --assembly and --publisher-prefix, or use --profile with a valid profile name."); } + + var pluginAnalysisItem = profile.Sync.OfType().FirstOrDefault(); + if (pluginAnalysisItem == null) + { + throw new InvalidOperationException( + $"Profile '{profile.Name}' does not contain a PluginAnalysis sync item. " + + "Either specify --assembly and --publisher-prefix, or use a profile with a PluginAnalysis sync item."); + } + + finalAssemblyPath = !string.IsNullOrWhiteSpace(assemblyPath) + ? assemblyPath + : pluginAnalysisItem.AssemblyPath; + finalPublisherPrefix = !string.IsNullOrWhiteSpace(publisherPrefix) + ? publisherPrefix + : pluginAnalysisItem.PublisherPrefix; + finalPrettyPrint = prettyPrint || pluginAnalysisItem.PrettyPrint; } - ) - .AddCommandOptions(c => c.Plugin.Analysis) + + return Microsoft.Extensions.Options.Options.Create(new PluginAnalysisCommandOptions(finalAssemblyPath, finalPublisherPrefix, finalPrettyPrint)); + }) .AddLogger() .BuildServiceProvider(); @@ -86,7 +121,7 @@ private static async Task CommandAction(IServiceProvider serviceProvider, try { var analyzer = serviceProvider.GetRequiredService(); - var configuration = serviceProvider.GetRequiredService>(); + var configuration = serviceProvider.GetRequiredService>(); var pluginDto = analyzer.AnalyzeAssembly(configuration.Value.AssemblyPath, configuration.Value.PublisherPrefix); var jsonOptions = new JsonSerializerOptions(JsonSerializerOptions.Default) diff --git a/XrmSync/Commands/PluginSyncCommand.cs b/XrmSync/Commands/PluginSyncCommand.cs index f95618f..592e439 100644 --- a/XrmSync/Commands/PluginSyncCommand.cs +++ b/XrmSync/Commands/PluginSyncCommand.cs @@ -1,7 +1,9 @@ using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System.CommandLine; using XrmSync.Constants; using XrmSync.Extensions; +using XrmSync.Model; using XrmSync.Options; using XrmSync.SyncService.Extensions; @@ -16,7 +18,7 @@ public PluginSyncCommand() : base("plugins", "Synchronize plugins in a plugin as _assemblyFile = new(CliOptions.Assembly.Primary, CliOptions.Assembly.Aliases) { Description = CliOptions.Assembly.Description, - Arity = ArgumentArity.ExactlyOne + Arity = ArgumentArity.ZeroOrOne }; Add(_assemblyFile); @@ -32,31 +34,67 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken var assemblyPath = parseResult.GetValue(_assemblyFile); var (solutionName, dryRun, logLevel, ciMode) = GetSyncSharedOptionValues(parseResult); var sharedOptions = GetSharedOptionValues(parseResult); - + // Build service provider var serviceProvider = GetPluginSyncServices() .AddXrmSyncConfiguration(sharedOptions) .AddOptions( baseOptions => baseOptions with { - Logger = baseOptions.Logger with - { - LogLevel = logLevel ?? baseOptions.Logger.LogLevel, - CiMode = ciMode ?? baseOptions.Logger.CiMode - }, - Execution = baseOptions.Execution with + LogLevel = logLevel ?? baseOptions.LogLevel, + CiMode = ciMode ?? baseOptions.CiMode, + DryRun = dryRun ?? baseOptions.DryRun + }) + .AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + + // Determine assembly path and solution name + string finalAssemblyPath; + string finalSolutionName; + + // If CLI options provided, use them (standalone mode) + if (!string.IsNullOrWhiteSpace(assemblyPath) && !string.IsNullOrWhiteSpace(solutionName)) + { + finalAssemblyPath = assemblyPath; + finalSolutionName = solutionName; + } + // Otherwise try to get from profile + else + { + var profile = config.Profiles.FirstOrDefault(p => + p.Name.Equals(sharedOptions.ProfileName, StringComparison.OrdinalIgnoreCase)); + + if (profile == null) { - DryRun = dryRun ?? baseOptions.Execution.DryRun - }, - Plugin = baseOptions.Plugin with + throw new InvalidOperationException( + $"Profile '{sharedOptions.ProfileName}' not found. " + + "Either specify --assembly and --solution, or use --profile with a valid profile name."); + } + + var pluginSyncItem = profile.Sync.OfType().FirstOrDefault(); + if (pluginSyncItem == null) { - Sync = new( - string.IsNullOrWhiteSpace(assemblyPath) ? baseOptions.Plugin.Sync.AssemblyPath : assemblyPath, - string.IsNullOrWhiteSpace(solutionName) ? baseOptions.Plugin.Sync.SolutionName : solutionName - ) + throw new InvalidOperationException( + $"Profile '{profile.Name}' does not contain a Plugin sync item. " + + "Either specify --assembly and --solution, or use a profile with a Plugin sync item."); } - }) - .AddCommandOptions(c => c.Plugin.Sync) + + finalAssemblyPath = !string.IsNullOrWhiteSpace(assemblyPath) + ? assemblyPath + : pluginSyncItem.AssemblyPath; + finalSolutionName = !string.IsNullOrWhiteSpace(solutionName) + ? solutionName + : profile.SolutionName; + } + + return Microsoft.Extensions.Options.Options.Create(new PluginSyncCommandOptions(finalAssemblyPath, finalSolutionName)); + }) + .AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + return Microsoft.Extensions.Options.Options.Create(new ExecutionModeOptions(config.DryRun)); + }) .AddLogger() .BuildServiceProvider(); diff --git a/XrmSync/Commands/WebresourceSyncCommand.cs b/XrmSync/Commands/WebresourceSyncCommand.cs index 82770fa..793f9e6 100644 --- a/XrmSync/Commands/WebresourceSyncCommand.cs +++ b/XrmSync/Commands/WebresourceSyncCommand.cs @@ -1,7 +1,9 @@ -using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System.CommandLine; using XrmSync.Constants; using XrmSync.Extensions; +using XrmSync.Model; using XrmSync.Options; using XrmSync.SyncService.Extensions; @@ -16,7 +18,7 @@ public WebresourceSyncCommand() : base("webresources", "Synchronize webresources _webresourceRoot = new(CliOptions.Webresource.Primary, CliOptions.Webresource.Aliases) { Description = CliOptions.Webresource.Description, - Arity = ArgumentArity.ExactlyOne + Arity = ArgumentArity.ZeroOrOne }; Add(_webresourceRoot); @@ -39,25 +41,61 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken .AddOptions( options => options with { - Logger = options.Logger with - { - LogLevel = logLevel ?? options.Logger.LogLevel, - CiMode = ciMode ?? options.Logger.CiMode - }, - Execution = options.Execution with + LogLevel = logLevel ?? options.LogLevel, + CiMode = ciMode ?? options.CiMode, + DryRun = dryRun ?? options.DryRun + } + ) + .AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + + // Determine folder path and solution name + string finalFolderPath; + string finalSolutionName; + + // If CLI options provided, use them (standalone mode) + if (!string.IsNullOrWhiteSpace(folderPath) && !string.IsNullOrWhiteSpace(solutionName)) + { + finalFolderPath = folderPath; + finalSolutionName = solutionName; + } + // Otherwise try to get from profile + else + { + var profile = config.Profiles.FirstOrDefault(p => + p.Name.Equals(sharedOptions.ProfileName, StringComparison.OrdinalIgnoreCase)); + + if (profile == null) { - DryRun = dryRun ?? options.Execution.DryRun - }, - Webresource = options.Webresource with + throw new InvalidOperationException( + $"Profile '{sharedOptions.ProfileName}' not found. " + + "Either specify --folder and --solution, or use --profile with a valid profile name."); + } + + var webresourceSyncItem = profile.Sync.OfType().FirstOrDefault(); + if (webresourceSyncItem == null) { - Sync = new( - string.IsNullOrWhiteSpace(folderPath) ? options.Webresource.Sync.FolderPath : folderPath, - string.IsNullOrWhiteSpace(solutionName) ? options.Webresource.Sync.SolutionName : solutionName - ) + throw new InvalidOperationException( + $"Profile '{profile.Name}' does not contain a Webresource sync item. " + + "Either specify --folder and --solution, or use a profile with a Webresource sync item."); } + + finalFolderPath = !string.IsNullOrWhiteSpace(folderPath) + ? folderPath + : webresourceSyncItem.FolderPath; + finalSolutionName = !string.IsNullOrWhiteSpace(solutionName) + ? solutionName + : profile.SolutionName; } - ) - .AddCommandOptions(config => config.Webresource.Sync) + + return Microsoft.Extensions.Options.Options.Create(new WebresourceSyncCommandOptions(finalFolderPath, finalSolutionName)); + }) + .AddSingleton(sp => + { + var config = sp.GetRequiredService>().Value; + return Microsoft.Extensions.Options.Options.Create(new ExecutionModeOptions(config.DryRun)); + }) .AddLogger() .BuildServiceProvider(); diff --git a/XrmSync/Commands/XrmSyncCommandBase.cs b/XrmSync/Commands/XrmSyncCommandBase.cs index 0f07be3..50102df 100644 --- a/XrmSync/Commands/XrmSyncCommandBase.cs +++ b/XrmSync/Commands/XrmSyncCommandBase.cs @@ -16,38 +16,22 @@ internal abstract class XrmSyncCommandBase(string name, string description) : Co protected const int E_ERROR = 1; // Shared options available to all commands - protected Option SaveConfigOption { get; private set; } = null!; - protected Option SaveConfigToOption { get; private set; } = null!; - protected Option ConfigNameOption { get; private set; } = null!; + protected Option ProfileNameOption { get; private set; } = null!; public Command GetCommand() => this; /// - /// Adds shared options to the command (save-config, save-config-to, config-name) + /// Adds shared options to the command (profile) /// protected void AddSharedOptions() { - SaveConfigOption = new(CliOptions.Config.SaveConfig.Primary, CliOptions.Config.SaveConfig.Aliases) + ProfileNameOption = new(CliOptions.Config.Profile.Primary, CliOptions.Config.Profile.Aliases) { - Description = CliOptions.Config.SaveConfig.Description, + Description = CliOptions.Config.Profile.Description, Required = false }; - SaveConfigToOption = new(CliOptions.Config.SaveConfigTo.Primary) - { - Description = CliOptions.Config.SaveConfigTo.Description, - Required = false - }; - - ConfigNameOption = new(CliOptions.Config.LoadConfig.Primary, CliOptions.Config.LoadConfig.Aliases) - { - Description = CliOptions.Config.LoadConfig.Description, - Required = false - }; - - Add(SaveConfigOption); - Add(SaveConfigToOption); - Add(ConfigNameOption); + Add(ProfileNameOption); } /// @@ -55,15 +39,13 @@ protected void AddSharedOptions() /// protected SharedOptions GetSharedOptionValues(ParseResult parseResult) { - var saveConfig = parseResult.GetValue(SaveConfigOption); - var saveConfigTo = saveConfig ? parseResult.GetValue(SaveConfigToOption) ?? ConfigReader.CONFIG_FILE_BASE + ".json" : null; - var configName = parseResult.GetValue(ConfigNameOption) ?? XrmSyncConfigurationBuilder.DEFAULT_CONFIG_NAME; + var profileName = parseResult.GetValue(ProfileNameOption); - return new (saveConfig, saveConfigTo, configName); + return new (profileName); } /// - /// Validates configuration and runs the appropriate action (save config or execute) + /// Validates configuration and runs the action /// protected static async Task RunAction( IServiceProvider serviceProvider, @@ -83,22 +65,6 @@ protected static async Task RunAction( return false; } - var sharedOptions = serviceProvider.GetRequiredService>(); - - var (saveConfig, saveConfigTo, configName) = sharedOptions.Value; - if (saveConfig) - { - var configWriter = serviceProvider.GetRequiredService(); - - var configPath = string.IsNullOrWhiteSpace(saveConfigTo) ? null : saveConfigTo; - - await configWriter.SaveConfig(configPath, configName, cancellationToken); - Console.WriteLine($"Configuration saved to {saveConfigTo}"); - return true; - } - else - { - return await action(serviceProvider, cancellationToken); - } + return await action(serviceProvider, cancellationToken); } } diff --git a/XrmSync/Commands/XrmSyncRootCommand.cs b/XrmSync/Commands/XrmSyncRootCommand.cs index b025f13..af416a5 100644 --- a/XrmSync/Commands/XrmSyncRootCommand.cs +++ b/XrmSync/Commands/XrmSyncRootCommand.cs @@ -16,7 +16,7 @@ namespace XrmSync.Commands; internal record ArgumentOverrides(bool DryRun, bool CiMode, LogLevel? LogLevel); /// -/// Root command handler that executes all configured sub-commands for a given configuration +/// Root command handler that executes all sync items in a profile /// internal class XrmSyncRootCommand : XrmSyncCommandBase { @@ -66,20 +66,14 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken var ciModeOverride = parseResult.GetValue(_ciMode); var logLevelOverride = parseResult.GetValue(_logLevel); - // Build service provider using the same pattern as other commands + // Build service provider var serviceProvider = new ServiceCollection() .AddXrmSyncConfiguration(sharedOptions) .AddOptions(baseOptions => baseOptions with { - Logger = baseOptions.Logger with - { - LogLevel = logLevelOverride ?? baseOptions.Logger.LogLevel, - CiMode = ciModeOverride || baseOptions.Logger.CiMode - }, - Execution = baseOptions.Execution with - { - DryRun = dryRunOverride || baseOptions.Execution.DryRun - } + LogLevel = logLevelOverride ?? baseOptions.LogLevel, + CiMode = ciModeOverride || baseOptions.CiMode, + DryRun = dryRunOverride || baseOptions.DryRun }) .AddLogger() .BuildServiceProvider(); @@ -87,106 +81,149 @@ private async Task ExecuteAsync(ParseResult parseResult, CancellationToken var xrmSyncConfig = serviceProvider.GetRequiredService>().Value; var logger = serviceProvider.GetRequiredService>(); - logger.LogInformation("Running XrmSync with configuration: {configName} (DryRun: {dryRun})", - sharedOptions.ConfigName, xrmSyncConfig.Execution.DryRun); + // Find the profile + var profile = xrmSyncConfig.Profiles.FirstOrDefault(p => + p.Name.Equals(sharedOptions.ProfileName, StringComparison.OrdinalIgnoreCase)); + + if (profile == null) + { + logger.LogError("Profile '{profileName}' not found. Use 'xrmsync config list' to see available profiles.", sharedOptions.ProfileName); + return E_ERROR; + } + + logger.LogInformation("Running XrmSync with profile: {profileName} (DryRun: {dryRun})", + profile.Name, xrmSyncConfig.DryRun); + + if (profile.Sync.Count == 0) + { + logger.LogWarning("Profile '{profileName}' has no sync items configured. Nothing to execute.", profile.Name); + return E_ERROR; + } var success = true; - var executedAnyCommand = false; // Create argument overrides DTO var overrides = new ArgumentOverrides(dryRunOverride, ciModeOverride, logLevelOverride); - // Execute plugin sync if configured - if (!string.IsNullOrWhiteSpace(xrmSyncConfig.Plugin.Sync.AssemblyPath) && - !string.IsNullOrWhiteSpace(xrmSyncConfig.Plugin.Sync.SolutionName)) + // Execute each sync item in the profile + foreach (var syncItem in profile.Sync) { - logger.LogInformation("Executing plugin sync..."); - var pluginSyncArgs = BuildPluginSyncArgs(xrmSyncConfig, sharedOptions, overrides); - var result = await ExecuteSubCommand("plugins", pluginSyncArgs); - success = success && result == E_OK; - executedAnyCommand = true; - } + logger.LogInformation("Executing {syncType} sync item...", syncItem.SyncType); - // Execute webresource sync if configured - if (!string.IsNullOrWhiteSpace(xrmSyncConfig.Webresource.Sync.FolderPath) && - !string.IsNullOrWhiteSpace(xrmSyncConfig.Webresource.Sync.SolutionName)) - { - logger.LogInformation("Executing webresource sync..."); - var webresourceSyncArgs = BuildWebresourceSyncArgs(xrmSyncConfig, sharedOptions, overrides); - var result = await ExecuteSubCommand("webresources", webresourceSyncArgs); - success = success && result == E_OK; - executedAnyCommand = true; - } + int result = syncItem switch + { + PluginSyncItem plugin => await ExecutePluginSync(plugin, profile, sharedOptions, overrides, xrmSyncConfig), + PluginAnalysisSyncItem analysis => await ExecutePluginAnalysis(analysis, sharedOptions, overrides, xrmSyncConfig), + WebresourceSyncItem webresource => await ExecuteWebresourceSync(webresource, profile, sharedOptions, overrides, xrmSyncConfig), + _ => LogUnknownSyncItemType(logger, syncItem.SyncType) + }; - if (!executedAnyCommand) - { - logger.LogWarning("No sub-commands configured for configuration '{configName}'. Nothing to execute.", sharedOptions.ConfigName); - return E_ERROR; + success = success && result == E_OK; } return success ? E_OK : E_ERROR; } - private async Task ExecuteSubCommand(string commandName, string[] args) + private async Task ExecutePluginSync( + PluginSyncItem syncItem, + ProfileConfiguration profile, + SharedOptions sharedOptions, + ArgumentOverrides overrides, + XrmSyncConfiguration config) { - var command = _subCommands.FirstOrDefault(c => c.GetCommand().Name == commandName); - if (command == null) + var args = new List { - Console.Error.WriteLine($"Sub-command '{commandName}' not found."); - return E_ERROR; + CliOptions.Assembly.Primary, syncItem.AssemblyPath, + CliOptions.Solution.Primary, profile.SolutionName + }; + + if (!string.IsNullOrWhiteSpace(sharedOptions.ProfileName)) + { + args.Add(CliOptions.Config.Profile.Primary); + args.Add(sharedOptions.ProfileName); } - var parseResult = command.GetCommand().Parse(args); - return await parseResult.InvokeAsync(); + AddCommonArgs(args, overrides, config); + return await ExecuteSubCommand("plugins", [.. args]); } - private static string[] BuildPluginSyncArgs( - XrmSyncConfiguration config, + private async Task ExecutePluginAnalysis( + PluginAnalysisSyncItem syncItem, SharedOptions sharedOptions, - ArgumentOverrides overrides) + ArgumentOverrides overrides, + XrmSyncConfiguration config) { var args = new List { - CliOptions.Assembly.Primary, config.Plugin.Sync.AssemblyPath, - CliOptions.Solution.Primary, config.Plugin.Sync.SolutionName, - CliOptions.Config.LoadConfig.Primary, sharedOptions.ConfigName + CliOptions.Assembly.Primary, syncItem.AssemblyPath, + CliOptions.Analysis.Prefix.Primary, syncItem.PublisherPrefix }; - // Use override if provided, otherwise use config value - if (overrides.DryRun || config.Execution.DryRun) - args.Add(CliOptions.Execution.DryRun.Primary); - - if (overrides.CiMode || config.Logger.CiMode) - args.Add(CliOptions.Logging.CiMode.Aliases[0]); + if (!string.IsNullOrWhiteSpace(sharedOptions.ProfileName)) + { + args.Add(CliOptions.Config.Profile.Primary); + args.Add(sharedOptions.ProfileName); + } - var logLevel = overrides.LogLevel ?? config.Logger.LogLevel; - args.AddRange([CliOptions.Logging.LogLevel.Primary, logLevel.ToString()]); + if (syncItem.PrettyPrint) + args.Add(CliOptions.Analysis.PrettyPrint.Primary); - return [.. args]; + AddCommonArgs(args, overrides, config); + return await ExecuteSubCommand("analyze", [.. args]); } - private static string[] BuildWebresourceSyncArgs( - XrmSyncConfiguration config, + private async Task ExecuteWebresourceSync( + WebresourceSyncItem syncItem, + ProfileConfiguration profile, SharedOptions sharedOptions, - ArgumentOverrides overrides) + ArgumentOverrides overrides, + XrmSyncConfiguration config) { var args = new List { - CliOptions.Webresource.Primary, config.Webresource.Sync.FolderPath, - CliOptions.Solution.Primary, config.Webresource.Sync.SolutionName, - CliOptions.Config.LoadConfig.Primary, sharedOptions.ConfigName + CliOptions.Webresource.Primary, syncItem.FolderPath, + CliOptions.Solution.Primary, profile.SolutionName }; + if (!string.IsNullOrWhiteSpace(sharedOptions.ProfileName)) + { + args.Add(CliOptions.Config.Profile.Primary); + args.Add(sharedOptions.ProfileName); + } + + AddCommonArgs(args, overrides, config); + return await ExecuteSubCommand("webresources", [.. args]); + } + + private static void AddCommonArgs(List args, ArgumentOverrides overrides, XrmSyncConfiguration config) + { // Use override if provided, otherwise use config value - if (overrides.DryRun || config.Execution.DryRun) + if (overrides.DryRun || config.DryRun) args.Add(CliOptions.Execution.DryRun.Primary); - if (overrides.CiMode || config.Logger.CiMode) + if (overrides.CiMode || config.CiMode) args.Add(CliOptions.Logging.CiMode.Aliases[0]); - var logLevel = overrides.LogLevel ?? config.Logger.LogLevel; + var logLevel = overrides.LogLevel ?? config.LogLevel; args.AddRange([CliOptions.Logging.LogLevel.Primary, logLevel.ToString()]); + } - return [.. args]; + private static int LogUnknownSyncItemType(ILogger logger, string syncType) + { + logger.LogError("Unknown sync item type: {syncType}", syncType); + return E_ERROR; + } + + private async Task ExecuteSubCommand(string commandName, string[] args) + { + var command = _subCommands.FirstOrDefault(c => c.GetCommand().Name == commandName); + if (command == null) + { + Console.Error.WriteLine($"Sub-command '{commandName}' not found."); + return E_ERROR; + } + + var parseResult = command.GetCommand().Parse(args); + return await parseResult.InvokeAsync(); } } diff --git a/XrmSync/Constants/CliOptions.cs b/XrmSync/Constants/CliOptions.cs index 64b3821..26904a3 100644 --- a/XrmSync/Constants/CliOptions.cs +++ b/XrmSync/Constants/CliOptions.cs @@ -73,25 +73,11 @@ internal static class LogLevel /// internal static class Config { - public static class SaveConfig + public static class Profile { - public const string Primary = "--save-config"; - public static readonly string[] Aliases = ["--sc"]; - public const string Description = "Save current CLI options to appsettings.json"; - } - - - public static class SaveConfigTo - { - public const string Primary = "--save-config-to"; - public const string Description = $"If {SaveConfig.Primary} is set, save to this file instead of appsettings.json"; - } - - public static class LoadConfig - { - public const string Primary = "--config"; - public static readonly string[] Aliases = ["--config-name", "-c"]; - public const string Description = "Name of the configuration to load from appsettings.json (Default: 'default' or single config if only one exists)"; + public const string Primary = "--profile"; + public static readonly string[] Aliases = ["--profile-name", "-p"]; + public const string Description = "Name of the profile to load from appsettings.json (automatically uses single profile if only one exists)"; } } diff --git a/XrmSync/Extensions/ServiceCollectionExtensions.cs b/XrmSync/Extensions/ServiceCollectionExtensions.cs index 566219d..6361ae6 100644 --- a/XrmSync/Extensions/ServiceCollectionExtensions.cs +++ b/XrmSync/Extensions/ServiceCollectionExtensions.cs @@ -16,7 +16,6 @@ public static IServiceCollection AddXrmSyncConfiguration(this IServiceCollection { services .AddSingleton() - .AddSingleton() .AddSingleton() .AddSingleton(MSOptions.Create(sharedOptions)) .AddSingleton(sp => sp.GetRequiredService().GetConfiguration()) @@ -38,12 +37,6 @@ public static IServiceCollection AddOptions( return MSOptions.Create(configModifier(baseConfig)); }); - services.AddSingleton(sp => - { - var config = sp.GetRequiredService>(); - return MSOptions.Create(config.Value.Execution); - }); - return services; } @@ -63,31 +56,23 @@ public static IServiceCollection AddCommandOptions( public static IServiceCollection AddLogger(this IServiceCollection services) { - // Register IOptions for logging services.AddSingleton(sp => { var config = sp.GetRequiredService>(); - return MSOptions.Create(config.Value.Logger); - }); - - services.AddSingleton(sp => - { - var loggerOptions = sp.GetRequiredService>().Value; - var executionOptions = sp.GetRequiredService>().Value; return LoggerFactory.Create( builder => { builder.AddFilter(nameof(Microsoft), LogLevel.Warning) .AddFilter(nameof(System), LogLevel.Warning) - .AddFilter(nameof(XrmSync), loggerOptions.LogLevel) + .AddFilter(nameof(XrmSync), config.Value.LogLevel) .AddConsole(options => options.FormatterName = "ci-console") .AddConsoleFormatter(options => { options.IncludeScopes = false; options.SingleLine = true; options.TimestampFormat = "HH:mm:ss "; - options.CIMode = loggerOptions.CiMode; - options.DryRun = executionOptions.DryRun; + options.CIMode = config.Value.CiMode; + options.DryRun = config.Value.DryRun; }); }); }); diff --git a/XrmSync/Logging/SyncLogger.cs b/XrmSync/Logging/SyncLogger.cs index 2be646d..8d3d963 100644 --- a/XrmSync/Logging/SyncLogger.cs +++ b/XrmSync/Logging/SyncLogger.cs @@ -8,7 +8,7 @@ internal class SyncLogger : ILogger { private readonly ILogger _logger; - public SyncLogger(ILoggerFactory loggerFactory, IOptions configuration) + public SyncLogger(ILoggerFactory loggerFactory, IOptions configuration) { LogLevel minLogLevel = configuration.Value.LogLevel; diff --git a/XrmSync/Options/ConfigValidationOutput.cs b/XrmSync/Options/ConfigValidationOutput.cs index 5c09a7b..4676cfa 100644 --- a/XrmSync/Options/ConfigValidationOutput.cs +++ b/XrmSync/Options/ConfigValidationOutput.cs @@ -15,65 +15,57 @@ public Task OutputValidationResult(CancellationToken cancellationToken = default { if (configOptions == null || sharedOptions == null) { - throw new InvalidOperationException("ConfigValidationOutput requires XrmSyncConfiguration and SharedOptions to validate configuration. Use OutputConfigList for listing configurations."); + throw new InvalidOperationException("ConfigValidationOutput requires XrmSyncConfiguration and SharedOptions to validate configuration. Use OutputConfigList for listing profiles."); } - var configName = sharedOptions.Value.ConfigName; + var profileName = sharedOptions.Value.ProfileName; var configSource = GetConfigurationSource(); - Console.WriteLine($"Configuration: '{configName}' (from {configSource})"); - Console.WriteLine(); - var config = configOptions.Value; - var allValid = true; + var profile = config.Profiles.FirstOrDefault(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); - // Validate and display Plugin Sync Configuration - allValid &= OutputSectionValidation( - "Plugin Sync Configuration", - ConfigurationScope.PluginSync, - () => - { - Console.WriteLine($" Assembly Path: {config.Plugin.Sync.AssemblyPath}"); - Console.WriteLine($" Solution Name: {config.Plugin.Sync.SolutionName}"); - }, - $"{XrmSyncConfigurationBuilder.SectionName.Plugin}:{XrmSyncConfigurationBuilder.SectionName.Sync}"); - - // Validate and display Plugin Analysis Configuration - allValid &= OutputSectionValidation( - "Plugin Analysis Configuration", - ConfigurationScope.PluginAnalysis, - () => - { - Console.WriteLine($" Assembly Path: {config.Plugin.Analysis.AssemblyPath}"); - Console.WriteLine($" Publisher Prefix: {config.Plugin.Analysis.PublisherPrefix}"); - Console.WriteLine($" Pretty Print: {config.Plugin.Analysis.PrettyPrint}"); - }, - $"{XrmSyncConfigurationBuilder.SectionName.Plugin}:{XrmSyncConfigurationBuilder.SectionName.Analysis}"); - - // Validate and display Webresource Sync Configuration - allValid &= OutputSectionValidation( - "Webresource Sync Configuration", - ConfigurationScope.WebresourceSync, - () => - { - Console.WriteLine($" Folder Path: {config.Webresource.Sync.FolderPath}"); - Console.WriteLine($" Solution Name: {config.Webresource.Sync.SolutionName}"); - }, - $"{XrmSyncConfigurationBuilder.SectionName.Webresource}:{XrmSyncConfigurationBuilder.SectionName.Sync}"); - - // Display Logger Configuration (always valid) - Console.WriteLine("✓ Logger Configuration"); - Console.WriteLine($" Log Level: {config.Logger.LogLevel}"); - Console.WriteLine($" CI Mode: {config.Logger.CiMode}"); + if (profile == null) + { + Console.WriteLine($"Profile '{profileName}' not found in {configSource}"); + return Task.CompletedTask; + } + + Console.WriteLine($"Profile: '{profile.Name}' (from {configSource})"); Console.WriteLine(); - // Display Execution Configuration (always valid) - Console.WriteLine("✓ Execution Configuration"); - Console.WriteLine($" Dry Run: {config.Execution.DryRun}"); + // Display global settings + Console.WriteLine("✓ Global Configuration"); + Console.WriteLine($" Dry Run: {config.DryRun}"); + Console.WriteLine($" Log Level: {config.LogLevel}"); + Console.WriteLine($" CI Mode: {config.CiMode}"); Console.WriteLine(); - // Display available commands based on configuration - var availableCommands = GetAvailableCommands(); + // Display profile settings + Console.WriteLine($"✓ Profile '{profile.Name}'"); + Console.WriteLine($" Solution Name: {profile.SolutionName}"); + Console.WriteLine(); + + // Display and validate sync items + var allValid = true; + if (profile.Sync.Count == 0) + { + Console.WriteLine(" ⊘ No sync items configured"); + Console.WriteLine(); + } + else + { + Console.WriteLine($" Sync Items ({profile.Sync.Count}):"); + Console.WriteLine(); + + for (int i = 0; i < profile.Sync.Count; i++) + { + var syncItem = profile.Sync[i]; + allValid &= OutputSyncItemValidation(i + 1, syncItem, profile.Name); + } + } + + // Display available commands + var availableCommands = GetAvailableCommands(profile); if (availableCommands.Count != 0) { Console.WriteLine($"Available Commands: {string.Join(", ", availableCommands)}"); @@ -99,198 +91,193 @@ public Task OutputConfigList(CancellationToken cancellationToken = default) if (!xrmSyncSection.Exists()) { - Console.WriteLine("No XrmSync configurations found in appsettings.json"); + Console.WriteLine("No XrmSync configuration found in appsettings.json"); + return Task.CompletedTask; + } + + var profilesSection = xrmSyncSection.GetSection(XrmSyncConfigurationBuilder.SectionName.Profiles); + + if (!profilesSection.Exists()) + { + Console.WriteLine("No profiles found in XrmSync configuration"); return Task.CompletedTask; } - var configNames = xrmSyncSection.GetChildren() - .Select(c => c.Key) - .ToList(); + var profiles = profilesSection.GetChildren().ToList(); - if (configNames.Count == 0) + if (profiles.Count == 0) { - Console.WriteLine("No XrmSync configurations found in appsettings.json"); + Console.WriteLine("No profiles found in XrmSync configuration"); return Task.CompletedTask; } - Console.WriteLine($"Available configurations (from {GetConfigurationSource()}):"); + Console.WriteLine($"Available profiles (from {GetConfigurationSource()}):"); Console.WriteLine(); - foreach (var name in configNames) + foreach (var profileSection in profiles) { + var name = profileSection.GetValue("Name") ?? string.Empty; + var solutionName = profileSection.GetValue("SolutionName") ?? string.Empty; + var syncItems = profileSection.GetSection("Sync").GetChildren().ToList(); + Console.WriteLine($" - {name}"); + Console.WriteLine($" Solution: {solutionName}"); + + if (syncItems.Count > 0) + { + var syncTypes = syncItems + .Select(s => s.GetValue("Type")) + .Where(t => !string.IsNullOrEmpty(t)) + .ToList(); + Console.WriteLine($" Sync Items: {string.Join(", ", syncTypes)} ({syncItems.Count} total)"); + } + else + { + Console.WriteLine($" Sync Items: None"); + } - // Try to get a brief status for this config - var (isValid, summary) = GetConfigBriefStatus(name); - var statusSymbol = isValid ? "✓" : "✗"; - Console.WriteLine($" {statusSymbol} {summary}"); Console.WriteLine(); } return Task.CompletedTask; } - private bool OutputSectionValidation(string sectionName, ConfigurationScope scope, Action displayValues, string configSectionPath) + private bool OutputSyncItemValidation(int index, SyncItem syncItem, string profileName) { - if (configOptions == null || sharedOptions == null) - { - throw new InvalidOperationException("ConfigValidationOutput requires XrmSyncConfiguration and SharedOptions to validate configuration."); - } - - // Check if the section exists in the configuration - var configName = sharedOptions.Value.ConfigName; - var fullSectionPath = $"{XrmSyncConfigurationBuilder.SectionName.XrmSync}:{configName}:{configSectionPath}"; - var section = configuration.GetSection(fullSectionPath); - - // If section doesn't exist or has no children, it's not configured - if (!section.Exists() || !section.GetChildren().Any()) - { - Console.WriteLine($"⊘ {sectionName} (not configured)"); - Console.WriteLine(); - return true; // Not an error, just not configured - } + var itemLabel = $" [{index}] {syncItem.SyncType}"; try { - // Create a temporary validator to check this section - var validator = new XrmSyncConfigurationValidator(configOptions); - validator.Validate(scope); + var errors = syncItem switch + { + PluginSyncItem plugin => ValidatePluginSync(plugin), + PluginAnalysisSyncItem analysis => ValidatePluginAnalysis(analysis), + WebresourceSyncItem webresource => ValidateWebresource(webresource), + _ => new List { "Unknown sync item type" } + }; - // If we get here, validation passed - Console.WriteLine($"✓ {sectionName}"); - displayValues(); + if (errors.Count > 0) + { + Console.WriteLine($" ✗ {itemLabel}"); + DisplaySyncItemDetails(syncItem); + foreach (var error in errors) + { + Console.WriteLine($" Error: {error}"); + } + Console.WriteLine(); + return false; + } + + Console.WriteLine($" ✓ {itemLabel}"); + DisplaySyncItemDetails(syncItem); Console.WriteLine(); return true; } - catch (Model.Exceptions.OptionsValidationException ex) + catch (Exception ex) { - Console.WriteLine($"✗ {sectionName}"); - displayValues(); - Console.WriteLine($" {ex.Message}"); - Console.WriteLine(); - return false; - } - catch (AggregateException ex) - { - Console.WriteLine($"✗ {sectionName}"); - displayValues(); - Console.WriteLine($" Errors:"); - foreach (var innerEx in ex.InnerExceptions.OfType()) - { - Console.WriteLine($" {innerEx.Message}"); - } + Console.WriteLine($" ✗ {itemLabel}"); + Console.WriteLine($" Error: {ex.Message}"); Console.WriteLine(); return false; } } - private (bool isValid, string summary) GetConfigBriefStatus(string configName) + private void DisplaySyncItemDetails(SyncItem syncItem) { - try + switch (syncItem) { - // Create a temporary configuration builder for this config - var tempSharedOptions = Microsoft.Extensions.Options.Options.Create(new SharedOptions(false, null, configName)); - var tempBuilder = new XrmSyncConfigurationBuilder(configuration, tempSharedOptions); - var tempConfig = tempBuilder.Build(); - var tempConfigOptions = Microsoft.Extensions.Options.Options.Create(tempConfig); - var tempValidator = new XrmSyncConfigurationValidator(tempConfigOptions); - - var sections = new List(); - var hasErrors = false; - - // Helper to check if a section exists - bool SectionExists(string sectionPath) - { - var fullPath = $"{XrmSyncConfigurationBuilder.SectionName.XrmSync}:{configName}:{sectionPath}"; - var section = configuration.GetSection(fullPath); - return section.Exists() && section.GetChildren().Any(); - } - - // Check Plugin Sync section - if (SectionExists($"{XrmSyncConfigurationBuilder.SectionName.Plugin}:{XrmSyncConfigurationBuilder.SectionName.Sync}")) - { - try - { - tempValidator.Validate(ConfigurationScope.PluginSync); - sections.Add("plugins"); - } - catch { hasErrors = true; } - } - - // Check Plugin Analysis section - if (SectionExists($"{XrmSyncConfigurationBuilder.SectionName.Plugin}:{XrmSyncConfigurationBuilder.SectionName.Analysis}")) - { - try - { - tempValidator.Validate(ConfigurationScope.PluginAnalysis); - sections.Add("analyze"); - } - catch { hasErrors = true; } - } - - // Check Webresource Sync section - if (SectionExists($"{XrmSyncConfigurationBuilder.SectionName.Webresource}:{XrmSyncConfigurationBuilder.SectionName.Sync}")) - { - try - { - tempValidator.Validate(ConfigurationScope.WebresourceSync); - sections.Add("webresources"); - } - catch { hasErrors = true; } - } - - if (sections.Count == 0) - { - return (true, "No sections configured"); - } + case PluginSyncItem plugin: + Console.WriteLine($" Assembly Path: {plugin.AssemblyPath}"); + break; + case PluginAnalysisSyncItem analysis: + Console.WriteLine($" Assembly Path: {analysis.AssemblyPath}"); + Console.WriteLine($" Publisher Prefix: {analysis.PublisherPrefix}"); + Console.WriteLine($" Pretty Print: {analysis.PrettyPrint}"); + break; + case WebresourceSyncItem webresource: + Console.WriteLine($" Folder Path: {webresource.FolderPath}"); + break; + } + } - var summary = $"Configured: {string.Join(", ", sections)}"; - if (hasErrors) - { - summary += " (some sections have errors)"; - } + private List ValidatePluginSync(PluginSyncItem plugin) + { + var errors = new List(); - return (!hasErrors, summary); + if (string.IsNullOrWhiteSpace(plugin.AssemblyPath)) + { + errors.Add("Assembly path is required and cannot be empty."); } - catch (Exception ex) + else if (!File.Exists(Path.GetFullPath(plugin.AssemblyPath))) { - return (false, $"Error: {ex.Message}"); + errors.Add($"Assembly file does not exist: {plugin.AssemblyPath}"); } + else if (!Path.GetExtension(plugin.AssemblyPath).Equals(".dll", StringComparison.OrdinalIgnoreCase)) + { + errors.Add("Assembly file must have a .dll extension."); + } + + return errors; } - private List GetAvailableCommands() + private List ValidatePluginAnalysis(PluginAnalysisSyncItem analysis) { - if (configOptions == null) + var errors = ValidatePluginSync(new PluginSyncItem(analysis.AssemblyPath)); + + if (string.IsNullOrWhiteSpace(analysis.PublisherPrefix)) { - return new List(); + errors.Add("Publisher prefix is required and cannot be empty."); + } + else if (analysis.PublisherPrefix.Length < 2 || analysis.PublisherPrefix.Length > 8) + { + errors.Add("Publisher prefix must be between 2 and 8 characters."); + } + else if (!System.Text.RegularExpressions.Regex.IsMatch(analysis.PublisherPrefix, @"^[a-z][a-z0-9]{1,7}$")) + { + errors.Add("Publisher prefix must start with a lowercase letter and contain only lowercase letters and numbers."); } - var commands = new List(); - var validator = new XrmSyncConfigurationValidator(configOptions); + return errors; + } - // Check if plugin sync is configured - try + private List ValidateWebresource(WebresourceSyncItem webresource) + { + var errors = new List(); + + if (string.IsNullOrWhiteSpace(webresource.FolderPath)) { - validator.Validate(ConfigurationScope.PluginSync); - commands.Add("plugins"); + errors.Add("Webresource root path is required and cannot be empty."); } - catch { /* Not configured or invalid */ } - - // Check if plugin analysis is configured - try + else if (!Directory.Exists(Path.GetFullPath(webresource.FolderPath))) { - validator.Validate(ConfigurationScope.PluginAnalysis); - commands.Add("analyze"); + errors.Add($"Webresource root path does not exist: {webresource.FolderPath}"); } - catch { /* Not configured or invalid */ } - // Check if webresource sync is configured - try + return errors; + } + + private List GetAvailableCommands(ProfileConfiguration profile) + { + var commands = new List(); + + foreach (var syncItem in profile.Sync) { - validator.Validate(ConfigurationScope.WebresourceSync); - commands.Add("webresources"); + switch (syncItem) + { + case PluginSyncItem: + if (!commands.Contains("plugins")) + commands.Add("plugins"); + break; + case PluginAnalysisSyncItem: + if (!commands.Contains("analyze")) + commands.Add("analyze"); + break; + case WebresourceSyncItem: + if (!commands.Contains("webresources")) + commands.Add("webresources"); + break; + } } - catch { /* Not configured or invalid */ } return commands; } diff --git a/XrmSync/Options/ConfigWriter.cs b/XrmSync/Options/ConfigWriter.cs deleted file mode 100644 index 7666940..0000000 --- a/XrmSync/Options/ConfigWriter.cs +++ /dev/null @@ -1,78 +0,0 @@ -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; -using System.Text.Json; -using System.Text.Json.Serialization; -using XrmSync.Model; -using XrmSync.Model.Exceptions; - -namespace XrmSync.Options; - -public interface IConfigWriter -{ - Task SaveConfig(string? filePath = null, string configName = XrmSyncConfigurationBuilder.DEFAULT_CONFIG_NAME, CancellationToken cancellationToken = default); -} - -internal class ConfigWriter(IOptions options, ILogger logger) : IConfigWriter -{ - public async Task SaveConfig(string? filePath = null, string configName = XrmSyncConfigurationBuilder.DEFAULT_CONFIG_NAME, CancellationToken cancellationToken = default) - { - var targetFile = filePath ?? $"{ConfigReader.CONFIG_FILE_BASE}.json"; - - logger.LogInformation("Saving configuration to {FilePath} with config name {ConfigName}", targetFile, configName); - - var jsonOptions = new JsonSerializerOptions - { - WriteIndented = true, - PropertyNamingPolicy = null, // Keep original casing - Converters = { new JsonStringEnumConverter() } // Serialize enums as strings - }; - - try - { - Dictionary rootConfig; - - // Load existing file if it exists - if (File.Exists(targetFile)) - { - var existingContent = await File.ReadAllTextAsync(targetFile, cancellationToken); - rootConfig = JsonSerializer.Deserialize>(existingContent, jsonOptions) - ?? []; - } - else - { - rootConfig = []; - } - - // Get or create XrmSync section - if (!rootConfig.TryGetValue(XrmSyncConfigurationBuilder.SectionName.XrmSync, out var xrmSyncObj)) - { - xrmSyncObj = new Dictionary(); - rootConfig[XrmSyncConfigurationBuilder.SectionName.XrmSync] = xrmSyncObj; - } - - // Deserialize the XrmSync section to work with it - var xrmSyncJson = JsonSerializer.Serialize(xrmSyncObj, jsonOptions); - var xrmSyncSection = JsonSerializer.Deserialize>(xrmSyncJson, jsonOptions) - ?? []; - - // Serialize the configuration to a structure we can work with - var configJson = JsonSerializer.Serialize(options.Value, jsonOptions); - - // Save to appropriate location based on config name - // Named structure: save under XrmSync.{configName} - xrmSyncSection[configName] = JsonSerializer.Deserialize>(configJson, jsonOptions); - - rootConfig[XrmSyncConfigurationBuilder.SectionName.XrmSync] = xrmSyncSection; - - // Serialize and save - var json = JsonSerializer.Serialize(rootConfig, jsonOptions); - await File.WriteAllTextAsync(targetFile, json, cancellationToken); - - logger.LogInformation("Configuration saved successfully to {FilePath}", targetFile); - } - catch (Exception ex) - { - throw new XrmSyncException("Failed to save configuration", ex); - } - } -} \ No newline at end of file diff --git a/XrmSync/Options/IConfigurationBuilder.cs b/XrmSync/Options/IConfigurationBuilder.cs index 62ee22d..24eb6b1 100644 --- a/XrmSync/Options/IConfigurationBuilder.cs +++ b/XrmSync/Options/IConfigurationBuilder.cs @@ -21,4 +21,5 @@ internal interface IConfigurationValidator internal interface IConfigurationBuilder { XrmSyncConfiguration Build(); + ProfileConfiguration? GetProfile(string? profileName); } \ No newline at end of file diff --git a/XrmSync/Options/XrmSyncConfigurationBuilder.cs b/XrmSync/Options/XrmSyncConfigurationBuilder.cs index 58826d6..f00d2bd 100644 --- a/XrmSync/Options/XrmSyncConfigurationBuilder.cs +++ b/XrmSync/Options/XrmSyncConfigurationBuilder.cs @@ -6,122 +6,121 @@ namespace XrmSync.Options; +#pragma warning disable CS9113 // Parameter is unread - false positive, used in GetProfile method internal class XrmSyncConfigurationBuilder(IConfiguration configuration, IOptions options) : IConfigurationBuilder +#pragma warning restore CS9113 { - internal const string DEFAULT_CONFIG_NAME = "default"; public static class SectionName { public const string XrmSync = nameof(XrmSync); - public const string Plugin = nameof(Plugin); - public const string Webresource = nameof(Webresource); - public const string Sync = nameof(Sync); - public const string Analysis = nameof(Analysis); - public const string Logger = nameof(Logger); - public const string Execution = nameof(Execution); + public const string Profiles = nameof(Profiles); + public const string DryRun = nameof(DryRun); + public const string LogLevel = nameof(LogLevel); + public const string CiMode = nameof(CiMode); } public XrmSyncConfiguration Build() { + var xrmSyncSection = configuration.GetSection(SectionName.XrmSync); + return new XrmSyncConfiguration( - new( - BuildPluginSyncOptions(), - BuildAnalysisOptions() - ), - new( - BuildWebresourceSyncOptions() - ), - BuildLoggerOptions(), - BuildExecutionOptions() + xrmSyncSection.GetValue(SectionName.DryRun), + xrmSyncSection.GetValue(SectionName.LogLevel) ?? Microsoft.Extensions.Logging.LogLevel.Information, + xrmSyncSection.GetValue(SectionName.CiMode), + BuildProfiles(xrmSyncSection) ); } - private PluginSyncOptions BuildPluginSyncOptions() + private List BuildProfiles(IConfigurationSection xrmSyncSection) { - var pluginSyncSection = GetConfigurationSection($"{SectionName.Plugin}:{SectionName.Sync}"); - return new PluginSyncOptions( - pluginSyncSection.GetValue(nameof(PluginSyncOptions.AssemblyPath)) ?? string.Empty, - pluginSyncSection.GetValue(nameof(PluginSyncOptions.SolutionName)) ?? string.Empty - ); - } + var profilesSection = xrmSyncSection.GetSection(SectionName.Profiles); - private WebresourceSyncOptions BuildWebresourceSyncOptions() - { - var webresourceSyncSection = GetConfigurationSection($"{SectionName.Webresource}:{SectionName.Sync}"); - return new WebresourceSyncOptions( - webresourceSyncSection.GetValue(nameof(WebresourceSyncOptions.FolderPath)) ?? string.Empty, - webresourceSyncSection.GetValue(nameof(WebresourceSyncOptions.SolutionName)) ?? string.Empty - ); - } + if (!profilesSection.Exists()) + { + return new List(); + } - private PluginAnalysisOptions BuildAnalysisOptions() - { - var analysisSection = GetConfigurationSection($"{SectionName.Plugin}:{SectionName.Analysis}"); - return new ( - analysisSection.GetValue(nameof(PluginAnalysisOptions.AssemblyPath)) ?? string.Empty, - analysisSection.GetValue(nameof(PluginAnalysisOptions.PublisherPrefix)) ?? "new", - analysisSection.GetValue(nameof(PluginAnalysisOptions.PrettyPrint)) - ); - } + var profiles = new List(); - private LoggerOptions BuildLoggerOptions() - { - var loggerSection = GetConfigurationSection(SectionName.Logger); - return new ( - loggerSection.GetValue(nameof(LoggerOptions.LogLevel)) ?? LogLevel.Information, - loggerSection.GetValue(nameof(LoggerOptions.CiMode)) - ); + foreach (var profileSection in profilesSection.GetChildren()) + { + var name = profileSection.GetValue(nameof(ProfileConfiguration.Name)) ?? string.Empty; + var solutionName = profileSection.GetValue(nameof(ProfileConfiguration.SolutionName)) ?? string.Empty; + var syncItems = BuildSyncItems(profileSection.GetSection(nameof(ProfileConfiguration.Sync))); + + profiles.Add(new ProfileConfiguration(name, solutionName, syncItems)); + } + + return profiles; } - private ExecutionOptions BuildExecutionOptions() + private List BuildSyncItems(IConfigurationSection syncSection) { - var executionSection = GetConfigurationSection(SectionName.Execution); - return new ( - executionSection.GetValue(nameof(ExecutionOptions.DryRun)) - ); + var syncItems = new List(); + + if (!syncSection.Exists()) + { + return syncItems; + } + + foreach (var itemSection in syncSection.GetChildren()) + { + var type = itemSection.GetValue("Type") ?? string.Empty; + + SyncItem? syncItem = type switch + { + "Plugin" => new PluginSyncItem( + itemSection.GetValue(nameof(PluginSyncItem.AssemblyPath)) ?? string.Empty + ), + "PluginAnalysis" => new PluginAnalysisSyncItem( + itemSection.GetValue(nameof(PluginAnalysisSyncItem.AssemblyPath)) ?? string.Empty, + itemSection.GetValue(nameof(PluginAnalysisSyncItem.PublisherPrefix)) ?? "new", + itemSection.GetValue(nameof(PluginAnalysisSyncItem.PrettyPrint)) + ), + "Webresource" => new WebresourceSyncItem( + itemSection.GetValue(nameof(WebresourceSyncItem.FolderPath)) ?? string.Empty + ), + _ => null + }; + + if (syncItem != null) + { + syncItems.Add(syncItem); + } + } + + return syncItems; } - private IConfigurationSection GetConfigurationSection(string sectionPath) + public ProfileConfiguration? GetProfile(string? profileName) { - // If a config name is specified, use named configuration - var resolvedConfigName = ResolveConfigurationName(options.Value.ConfigName); - - return configuration.GetSection($"{SectionName.XrmSync}:{resolvedConfigName}:{sectionPath}"); + var config = Build(); + var resolvedProfileName = ResolveProfileName(profileName, config.Profiles); + + return config.Profiles.FirstOrDefault(p => p.Name.Equals(resolvedProfileName, StringComparison.OrdinalIgnoreCase)); } - private string ResolveConfigurationName(string requestedName) + private string? ResolveProfileName(string? requestedName, List profiles) { - var xrmSyncSection = configuration.GetSection(SectionName.XrmSync); - - if (!xrmSyncSection.Exists()) + if (profiles.Count == 0) { - return DEFAULT_CONFIG_NAME; + return null; } - // Get all configuration names (direct children of XrmSync) - var configNames = xrmSyncSection.GetChildren() - .Select(c => c.Key) - .ToList(); - // If requested name exists, use it - if (configNames.Contains(requestedName)) + if (!string.IsNullOrWhiteSpace(requestedName) && profiles.Any(p => p.Name.Equals(requestedName, StringComparison.OrdinalIgnoreCase))) { return requestedName; } - // If only one named config exists, use it - if (configNames.Count == 1) - { - return configNames[0]; - } - - // If multiple configs exist, try to use "default" - if (configNames.Contains(DEFAULT_CONFIG_NAME)) + // If only one profile exists, use it automatically + if (profiles.Count == 1) { - return DEFAULT_CONFIG_NAME; + return profiles[0].Name; } - // Throw exception if we cannot resolve the configuration - throw new XrmSyncException($"Multiple configuration sections found under '{SectionName.XrmSync}', but no {DEFAULT_CONFIG_NAME} configuration section found, or name wasn't specified."); + // If multiple profiles exist and no profile specified, require explicit selection + throw new XrmSyncException($"Multiple profiles found. Use --profile to specify which profile to use, or run 'xrmsync config list' to see available profiles."); } } diff --git a/XrmSync/Options/XrmSyncConfigurationValidator.cs b/XrmSync/Options/XrmSyncConfigurationValidator.cs index 56a8d28..5aae29a 100644 --- a/XrmSync/Options/XrmSyncConfigurationValidator.cs +++ b/XrmSync/Options/XrmSyncConfigurationValidator.cs @@ -4,7 +4,7 @@ namespace XrmSync.Options; -internal partial class XrmSyncConfigurationValidator(IOptions configuration) : IConfigurationValidator +internal partial class XrmSyncConfigurationValidator(IOptions configuration, IOptions sharedOptions) : IConfigurationValidator { public void Validate(ConfigurationScope scope) { @@ -13,7 +13,7 @@ public void Validate(ConfigurationScope scope) throw new Model.Exceptions.OptionsValidationException("No configuration scope specified for validation."); } - var exceptions = ValidateInternal(scope, configuration.Value).ToList(); + var exceptions = ValidateInternal(scope, configuration.Value, sharedOptions.Value.ProfileName).ToList(); if (exceptions.Count == 1) { throw exceptions[0]; @@ -23,60 +23,84 @@ public void Validate(ConfigurationScope scope) } } - private static IEnumerable ValidateInternal(ConfigurationScope scope, XrmSyncConfiguration configuration) + private static IEnumerable ValidateInternal(ConfigurationScope scope, XrmSyncConfiguration configuration, string? profileName) { - if (scope.HasFlag(ConfigurationScope.PluginSync)) - { - var errors = Validate(configuration.Plugin.Sync).ToList(); + // Find the profile to validate + var profile = configuration.Profiles.FirstOrDefault(p => p.Name.Equals(profileName, StringComparison.OrdinalIgnoreCase)); - if (errors.Count != 0) + if (profile == null) + { + // If a specific profile name was requested but not found, throw an error + if (!string.IsNullOrWhiteSpace(profileName)) { - yield return new Model.Exceptions.OptionsValidationException("Plugin sync", errors); + yield return new Model.Exceptions.OptionsValidationException("Profile", new[] { $"Profile '{profileName}' not found in configuration." }); + yield break; } + + // No profiles configured and no specific profile requested, validation passes (CLI mode) + yield break; } - if (scope.HasFlag(ConfigurationScope.PluginAnalysis)) + // Validate solution name at profile level + var profileErrors = ValidateSolutionName(profile.SolutionName).ToList(); + if (profileErrors.Count != 0) { - var errors = Validate(configuration.Plugin.Analysis).ToList(); - - if (errors.Count != 0) - { - yield return new Model.Exceptions.OptionsValidationException("Plugin analysis", errors); - } + yield return new Model.Exceptions.OptionsValidationException($"Profile '{profile.Name}'", profileErrors); } - if (scope.HasFlag(ConfigurationScope.WebresourceSync)) + // Validate each sync item in the profile based on scope + foreach (var syncItem in profile.Sync) { - var errors = Validate(configuration.Webresource.Sync).ToList(); + List errors = new(); - if (errors.Count != 0) + switch (syncItem) { - yield return new Model.Exceptions.OptionsValidationException("Webresource sync", errors); + case PluginSyncItem pluginSync when scope.HasFlag(ConfigurationScope.PluginSync): + errors = Validate(pluginSync).ToList(); + if (errors.Count != 0) + { + yield return new Model.Exceptions.OptionsValidationException($"Plugin sync in profile '{profile.Name}'", errors); + } + break; + + case PluginAnalysisSyncItem pluginAnalysis when scope.HasFlag(ConfigurationScope.PluginAnalysis): + errors = Validate(pluginAnalysis).ToList(); + if (errors.Count != 0) + { + yield return new Model.Exceptions.OptionsValidationException($"Plugin analysis in profile '{profile.Name}'", errors); + } + break; + + case WebresourceSyncItem webresource when scope.HasFlag(ConfigurationScope.WebresourceSync): + errors = Validate(webresource).ToList(); + if (errors.Count != 0) + { + yield return new Model.Exceptions.OptionsValidationException($"Webresource sync in profile '{profile.Name}'", errors); + } + break; } } } - private static IEnumerable Validate(PluginSyncOptions options) + private static IEnumerable Validate(PluginSyncItem syncItem) { return [ - ..ValidateAssemblyPath(options.AssemblyPath), - ..ValidateSolutionName(options.SolutionName) + ..ValidateAssemblyPath(syncItem.AssemblyPath) ]; } - private static IEnumerable Validate(PluginAnalysisOptions options) + private static IEnumerable Validate(PluginAnalysisSyncItem syncItem) { return [ - ..ValidateAssemblyPath(options.AssemblyPath), - ..ValidatePublisherPrefix(options.PublisherPrefix) + ..ValidateAssemblyPath(syncItem.AssemblyPath), + ..ValidatePublisherPrefix(syncItem.PublisherPrefix) ]; } - private static IEnumerable Validate(WebresourceSyncOptions options) + private static IEnumerable Validate(WebresourceSyncItem syncItem) { return [ - ..ValidateFolderPath(options.FolderPath), - ..ValidateSolutionName(options.SolutionName) + ..ValidateFolderPath(syncItem.FolderPath) ]; } diff --git a/XrmSync/Properties/launchSettings.json b/XrmSync/Properties/launchSettings.json index 737c4b8..b456f82 100644 --- a/XrmSync/Properties/launchSettings.json +++ b/XrmSync/Properties/launchSettings.json @@ -1,60 +1,8 @@ { "profiles": { - "Sync": { + "XrmSync": { "commandName": "Project", - "commandLineArgs": "plugins" - }, - "DryRunSyncDAXIF": { - "commandName": "Project", - "commandLineArgs": "plugins --dry-run --assembly \"..\\..\\..\\..\\Samples\\1-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" -n \"Plugins\" -l debug" - }, - "DryRunSyncDAXIFFromFile": { - "commandName": "Project", - "commandLineArgs": "plugins --dry-run -l Debug" - }, - "SyncDAXIF": { - "commandName": "Project", - "commandLineArgs": "plugins --assembly \"..\\..\\..\\..\\Samples\\1-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" -n \"Plugins\" -l debug" - }, - "DryRunSyncHybrid": { - "commandName": "Project", - "commandLineArgs": "plugins --dry-run --assembly \"..\\..\\..\\..\\Samples\\2-Hybrid\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" -n \"Plugins\" -l debug" - }, - "SyncHybrid": { - "commandName": "Project", - "commandLineArgs": "plugins --assembly \"..\\..\\..\\..\\Samples\\2-Hybrid\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" -n \"Plugins\" -l debug" - }, - "AnalyzeSampleDAXIF": { - "commandName": "Project", - "commandLineArgs": "analyze --pretty-print --assembly \"..\\..\\..\\..\\Samples\\1-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" --prefix \"dg\"" - }, - "AnalyzeSampleHybrid": { - "commandName": "Project", - "commandLineArgs": "analyze --pretty-print --assembly \"..\\..\\..\\..\\Samples\\2-Hybrid\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" --prefix \"dg\"" - }, - "AnalyzeSampleXrmPluginCore": { - "commandName": "Project", - "commandLineArgs": "analyze --pretty-print --assembly \"..\\..\\..\\..\\Samples\\3-XrmPluginCore\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" --prefix \"dg\"" - }, - "AnalyzeSampleFullDAXIF": { - "commandName": "Project", - "commandLineArgs": "analyze --pretty-print --assembly \"..\\..\\..\\..\\Samples\\4-Full-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" --prefix \"dg\"" - }, - "XrmSyncHelp": { - "commandName": "Project", - "commandLineArgs": "" - }, - "SaveConfigDryRun": { - "commandName": "Project", - "commandLineArgs": "plugins --save-config --dry-run --assembly \"..\\..\\..\\..\\Samples\\1-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" -n \"Plugins\" -l debug" - }, - "SaveConfigAnalyze": { - "commandName": "Project", - "commandLineArgs": "analyze --save-config --pretty-print --assembly \"..\\..\\..\\..\\Samples\\1-DAXIF\\bin\\Debug\\net462\\ILMerged.SamplePlugins.dll\" --prefix \"dg\"" - }, - "WebresourceSync": { - "commandName": "Project", - "commandLineArgs": "webresources --dry-run --path \"..\\..\\..\\..\\Samples\\5-Webresources\\Webresources\" -l debug" + "commandLineArgs": "--profile hybrid" } } -} \ No newline at end of file +} diff --git a/XrmSync/XrmSync.csproj b/XrmSync/XrmSync.csproj index 748abac..041fdc9 100644 --- a/XrmSync/XrmSync.csproj +++ b/XrmSync/XrmSync.csproj @@ -38,6 +38,9 @@ + + Never + PreserveNewest diff --git a/XrmSync/appsettings.example.json b/XrmSync/appsettings.example.json new file mode 100644 index 0000000..1952e77 --- /dev/null +++ b/XrmSync/appsettings.example.json @@ -0,0 +1,88 @@ +{ + "DATAVERSE_URL": "https://aarsleff-udv.crm4.dynamics.com", + "XrmSync": { + "DryRun": true, + "LogLevel": "Debug", + "CiMode": false, + "Profiles": [ + { + "Name": "daxif", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "../../../../Samples/1-DAXIF/bin/Debug/net462/ILMerged.SamplePlugins.dll" + } + ] + }, + { + "Name": "hybrid", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "../../../../Samples/2-Hybrid/bin/Debug/net462/ILMerged.SamplePlugins.dll" + } + ] + }, + { + "Name": "analyze-daxif", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "PluginAnalysis", + "AssemblyPath": "../../../../Samples/1-DAXIF/bin/Debug/net462/ILMerged.SamplePlugins.dll", + "PublisherPrefix": "dg", + "PrettyPrint": true + } + ] + }, + { + "Name": "analyze-hybrid", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "PluginAnalysis", + "AssemblyPath": "../../../../Samples/2-Hybrid/bin/Debug/net462/ILMerged.SamplePlugins.dll", + "PublisherPrefix": "dg", + "PrettyPrint": true + } + ] + }, + { + "Name": "analyze-xrmplugincore", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "PluginAnalysis", + "AssemblyPath": "../../../../Samples/3-XrmPluginCore/bin/Debug/net462/ILMerged.SamplePlugins.dll", + "PublisherPrefix": "dg", + "PrettyPrint": true + } + ] + }, + { + "Name": "analyze-full-daxif", + "SolutionName": "Plugins", + "Sync": [ + { + "Type": "PluginAnalysis", + "AssemblyPath": "../../../../Samples/4-Full-DAXIF/bin/Debug/net462/ILMerged.SamplePlugins.dll", + "PublisherPrefix": "dg", + "PrettyPrint": true + } + ] + }, + { + "Name": "webresources-sample", + "SolutionName": "WebresourceSolution", + "Sync": [ + { + "Type": "Webresource", + "FolderPath": "../../../../Samples/5-Webresources/Webresources" + } + ] + } + ] + } +} diff --git a/appsettings.example.json b/appsettings.example.json index 51c1802..834eb95 100644 --- a/appsettings.example.json +++ b/appsettings.example.json @@ -1,65 +1,44 @@ { "DATAVERSE_URL": "https://your-org.crm4.dynamics.com", "XrmSync": { - "default": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Debug/net462/MyPlugin.dll", - "SolutionName": "DefaultSolution", - "LogLevel": "Information", - "DryRun": false - }, - "Analysis": { - "AssemblyPath": "bin/Debug/net462/MyPlugin.dll", - "PublisherPrefix": "new", - "PrettyPrint": false - } + "DryRun": true, + "LogLevel": "Information", + "CiMode": false, + "Profiles": [ + { + "Name": "plugins", + "SolutionName": "PluginSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "bin/Release/net462/MyPlugin.dll" + } + ] + }, + { + "Name": "webresources", + "SolutionName": "WebresourceSolution", + "Sync": [ + { + "Type": "Webresource", + "FolderPath": "src/webresources" + } + ] + }, + { + "Name": "all", + "SolutionName": "SharedSolution", + "Sync": [ + { + "Type": "Plugin", + "AssemblyPath": "bin/Release/net462/MyPlugin.dll" + }, + { + "Type": "Webresource", + "FolderPath": "src/webresources" + } + ] } - }, - "dev": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Debug/net462/MyPlugin.dll", - "SolutionName": "DevSolution", - "LogLevel": "Debug", - "DryRun": true - }, - "Analysis": { - "AssemblyPath": "bin/Debug/net462/MyPlugin.dll", - "PublisherPrefix": "dev", - "PrettyPrint": true - } - } - }, - "staging": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "SolutionName": "StagingSolution", - "LogLevel": "Information", - "DryRun": false - }, - "Analysis": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "PublisherPrefix": "stg", - "PrettyPrint": false - } - } - }, - "prod": { - "Plugin": { - "Sync": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "SolutionName": "ProdSolution", - "LogLevel": "Warning", - "DryRun": false - }, - "Analysis": { - "AssemblyPath": "bin/Release/net462/MyPlugin.dll", - "PublisherPrefix": "prod", - "PrettyPrint": false - } - } - } + ] } } diff --git a/scripts/Test-Samples.ps1 b/scripts/Test-Samples.ps1 index eabd735..6ff5d18 100644 --- a/scripts/Test-Samples.ps1 +++ b/scripts/Test-Samples.ps1 @@ -76,7 +76,7 @@ function Run-Analyzer { } # Run the analyzer using dotnet run - $analyzeOutput = dotnet run --project $xrmSyncPath -- analyze --assembly $AssemblyPath --pretty-print 2>&1 + $analyzeOutput = dotnet run --project $xrmSyncPath -- analyze --assembly $AssemblyPath --publisher-prefix new --pretty-print 2>&1 if ($LASTEXITCODE -ne 0) { Write-Error "Failed to analyze $SampleName. Output: $analyzeOutput" }